diff --git a/README.rst b/README.rst index 97408f7..2572ae2 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,52 @@ Requires Changes ======= +*6.0.11 - 16.09.2022* + +- add: hierarchy for cashbooks + +*6.0.10 - 13.09.2022* + +- add: split-booking with transfer + +*6.0.9 - 08.09.2022* + +- updt: allow negative amounts + +*6.0.8 - 07.09.2022* + +- updt: enter-booking form optimized + +*6.0.7 - 07.09.2022* + +- add: enter-booking-wizard + +*6.0.6 - 06.09.2022* + +- updt: optimized form - line, line-context +- updt: extended search in cashbook-lines + +*6.0.5 - 05.09.2022* + +- updt: view of book + line optimized + +*6.0.4 - 05.09.2022* + +- fix: write number at state-change 'check' -> 'done' +- updt: speedup transaction view + +*6.0.3 - 31.08.2022* + +- updt: checks, sorting + +*6.0.2 - 25.08.2022* + +- add: split-booking + +*6.0.1 - 23.08.2022* + +- works + *6.0.0 - 05.08.2022* - init diff --git a/__init__.py b/__init__.py index cb57d15..6115d74 100644 --- a/__init__.py +++ b/__init__.py @@ -10,6 +10,7 @@ from .line import Line, LineContext from .splitline import SplitLine from .wizard_openline import OpenCashBook, OpenCashBookStart from .wizard_runreport import RunCbReport, RunCbReportStart +from .wizard_booking import EnterBookingWizard, EnterBookingStart from .configuration import Configuration, UserConfiguration from .category import Category from .reconciliation import Reconciliation @@ -28,6 +29,7 @@ def register(): Reconciliation, OpenCashBookStart, RunCbReportStart, + EnterBookingStart, module='cashbook', type_='model') Pool.register( ReconciliationReport, @@ -35,4 +37,5 @@ def register(): Pool.register( OpenCashBook, RunCbReport, + EnterBookingWizard, module='cashbook', type_='wizard') diff --git a/book.py b/book.py index dd92f35..38fa020 100644 --- a/book.py +++ b/book.py @@ -3,7 +3,7 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. -from trytond.model import Workflow, ModelView, ModelSQL, fields, Check +from trytond.model import Workflow, ModelView, ModelSQL, fields, Check, tree from trytond.pyson import Eval, Or, Bool, Id from trytond.exceptions import UserError from trytond.i18n import gettext @@ -20,6 +20,16 @@ STATES = { } DEPENDS=['state'] +# states in case of 'btype'!=None +STATES2 = { + 'readonly': Or( + Eval('state', '') != 'open', + ~Bool(Eval('btype')), + ), + 'invisible': ~Bool(Eval('btype')), + } +DEPENDS2 = ['state', 'btype'] + sel_state_book = [ ('open', 'Open'), ('closed', 'Closed'), @@ -27,7 +37,7 @@ sel_state_book = [ ] -class Book(Workflow, ModelSQL, ModelView): +class Book(tree(separator='/'), Workflow, ModelSQL, ModelView): 'Cashbook' __name__ = 'cashbook.book' @@ -35,9 +45,17 @@ class Book(Workflow, ModelSQL, ModelView): required=True, ondelete="RESTRICT") name = fields.Char(string='Name', required=True, states=STATES, depends=DEPENDS) - btype = fields.Many2One(string='Type', required=True, - model_name='cashbook.type', ondelete='RESTRICT', + description = fields.Text(string='Description', states=STATES, depends=DEPENDS) + btype = fields.Many2One(string='Type', + help='A cash book with type can contain postings. Without type is a view.', + model_name='cashbook.type', ondelete='RESTRICT', + states={ + 'readonly': Or( + STATES['readonly'], + Bool(Eval('lines')), + ), + }, depends=DEPENDS+['lines']) owner = fields.Many2One(string='Owner', required=True, select=True, model_name='res.user', ondelete='SET NULL', states=STATES, depends=DEPENDS) @@ -54,8 +72,8 @@ class Book(Workflow, ModelSQL, ModelView): states=STATES, depends=DEPENDS) reconciliations = fields.One2Many(string='Reconciliations', field='cashbook', model_name='cashbook.recon', - states=STATES, depends=DEPENDS) - number_sequ = fields.Many2One(string='Line numbering', required=True, + states=STATES2, depends=DEPENDS2) + number_sequ = fields.Many2One(string='Line numbering', help='Number sequence for numbering of the cash book lines.', model_name='ir.sequence', domain=[ @@ -65,37 +83,56 @@ class Book(Workflow, ModelSQL, ModelView): ('company', '=', Eval('company', -1)), ], ], - states=STATES, depends=DEPENDS+['company']) + states={ + 'readonly': STATES2['readonly'], + 'required': Bool(Eval('btype')), + }, depends=DEPENDS2+['company']) number_atcheck = fields.Boolean(string="number when 'Checking'", - help="The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done.") - start_date = fields.Date(string='Initial Date', required=True, + help="The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done.", + states=STATES2, depends=DEPENDS2) + start_date = fields.Date(string='Initial Date', states={ 'readonly': Or( - STATES['readonly'], + STATES2['readonly'], Bool(Eval('lines')), ), - }, depends=DEPENDS+['lines']) - start_balance = fields.Numeric(string='Initial Amount', required=True, - states={ - 'readonly': Or( - STATES['readonly'], - Bool(Eval('lines')), - ), - }, depends=DEPENDS+['lines']) - balance = fields.Function(fields.Numeric(string='Balance', readonly=True), - 'on_change_with_balance') - currency = fields.Many2One(string='Currency', required=True, + 'invisible': STATES2['invisible'], + 'required': Bool(Eval('btype')), + }, depends=DEPENDS2+['lines']) + balance = fields.Function(fields.Numeric(string='Balance', readonly=True, + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'on_change_with_balance') + currency = fields.Many2One(string='Currency', model_name='currency.currency', states={ 'readonly': Or( - STATES['readonly'], + STATES2['readonly'], Bool(Eval('lines', [])), ), - }, depends=DEPENDS+['lines']) + 'invisible': STATES2['invisible'], + 'required': Bool(Eval('btype')), + }, depends=DEPENDS2+['lines']) + currency_digits = fields.Function(fields.Integer(string='Currency Digits', + readonly=True), 'on_change_with_currency_digits') state = fields.Selection(string='State', required=True, readonly=True, selection=sel_state_book) state_string = state.translated('state') + parent = fields.Many2One(string="Parent", + model_name='cashbook.book', ondelete='RESTRICT', + left='left', right='right') + childs = fields.One2Many(string='Children', field='parent', + model_name='cashbook.book') + left = fields.Integer(string='Left', required=True, select=True) + right = fields.Integer(string='Right', required=True, select=True) + + @classmethod + def __register__(cls, module_name): + super(Book, cls).__register__(module_name) + + table = cls.__table_handler__(module_name) + table.drop_column('start_balance') + @classmethod def __setup__(cls): super(Book, cls).__setup__() @@ -127,16 +164,18 @@ class Book(Workflow, ModelSQL, ModelView): }, }) + @staticmethod + def default_left(): + return 0 + + @staticmethod + def default_right(): + return 0 + @classmethod def default_number_atcheck(cls): return True - @classmethod - def default_start_balance(cls): - """ zero - """ - return Decimal('0.0') - @classmethod def default_currency(cls): """ currency of company @@ -189,35 +228,55 @@ class Book(Workflow, ModelSQL, ModelView): def get_rec_name(self, name): """ name, balance, state """ - return '%(name)s | %(balance)s %(symbol)s | %(state)s' % { - 'name': self.name or '-', - 'balance': Report.format_number(self.balance or 0.0, None), - 'symbol': getattr(self.currency, 'symbol', '-'), - 'state': self.state_string, - } + recname = super(Book, self).get_rec_name(name) + if self.btype: + return '%(name)s | %(balance)s %(symbol)s | %(state)s' % { + 'name': recname or '-', + 'balance': Report.format_number(self.balance or 0.0, None), + 'symbol': getattr(self.currency, 'symbol', '-'), + 'state': self.state_string, + } + return recname - @fields.depends('id', 'start_balance') + @fields.depends('currency') + def on_change_with_currency_digits(self, name=None): + """ currency of cashbook + """ + if self.currency: + return self.currency.digits + else: + return 2 + + @fields.depends('id') def on_change_with_balance(self, name=None): """ compute balance """ - Line = Pool().get('cashbook.line') + pool = Pool() + Book2 = pool.get('cashbook.book') + Line = pool.get('cashbook.line') tab_line = Line.__table__() cursor = Transaction().connection.cursor() - query = tab_line.select( + line_query = Line.search([ + ('cashbook.id', 'in', Book2.search([ + ('parent', 'child_of', [self.id]), + ], query=True)), + ], query=True) + + query = line_query.join(tab_line, + condition=tab_line.id==line_query.id, + ).select( Sum(tab_line.credit - tab_line.debit).as_('balance'), - group_by=[tab_line.cashbook], - where=tab_line.cashbook == self.id ) if self.id: - if self.start_balance is not None: - balance = self.start_balance - cursor.execute(*query) - result = cursor.fetchone() - if result: + balance = Decimal('0.0') + cursor.execute(*query) + result = cursor.fetchone() + if result: + if result[0] is not None: balance += result[0] - return balance + return balance @classmethod @ModelView.button @@ -250,12 +309,15 @@ class Book(Workflow, ModelSQL, ModelView): actions = iter(args) for books, values in zip(actions, actions): for book in books: - if 'start_balance' in values.keys(): - if len(book.lines) > 0: + # deny btype-->None if lines not empty + if 'btype' in values.keys(): + if (values['btype'] is None) and (len(book.lines) > 0): raise UserError(gettext( - 'cashbook.msg_book_err_startamount_with_lines', - bookname = book.rec_name, + 'cashbook.msg_book_btype_with_lines', + cbname = book.rec_name, + numlines = len(book.lines), )) + if book.state != 'open': # allow state-update, if its the only action if not (('state' in values.keys()) and (len(values.keys()) == 1)): diff --git a/book.xml b/book.xml index 465bcaf..810cf96 100644 --- a/book.xml +++ b/book.xml @@ -12,6 +12,13 @@ full copyright notices and license terms. --> book_list + + cashbook.book + tree + + childs + book_tree + cashbook.book form @@ -19,7 +26,7 @@ full copyright notices and license terms. --> book_form - + Cashbook cashbook.book @@ -35,6 +42,23 @@ full copyright notices and license terms. --> + + + Cashbook + cashbook.book + + + + + + + + + + + + + @@ -251,18 +275,24 @@ full copyright notices and license terms. --> - - - - - + + + + + + + + + + @@ -335,16 +365,23 @@ full copyright notices and license terms. --> - + + search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'currency')]"/> - + + search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'parent')]"/> + + + + + + @@ -421,16 +458,23 @@ full copyright notices and license terms. --> - + + search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'currency')]"/> - + + search="[('model.model', '=', 'cashbook.book'), ('name', '=', 'parent')]"/> + + + + + + diff --git a/category.py b/category.py index 0d23eb8..477016c 100644 --- a/category.py +++ b/category.py @@ -3,12 +3,46 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. -from trytond.model import ModelView, ModelSQL, fields, Unique, tree, sequence_ordered +from trytond.model import ModelView, ModelSQL, fields, Unique, Exclude, tree from trytond.transaction import Transaction from trytond.pool import Pool from trytond.pyson import Eval, If, Bool from trytond.exceptions import UserError from trytond.i18n import gettext +from sql.operators import Equal +from sql.functions import Function +from sql import With, Literal + + +class ArrayApppend(Function): + """ sql: array_append + """ + __slots__ = () + _function = 'ARRAY_APPEND' + +# end ArrayApppend + + +class ArrayToString(Function): + """ sql: array_to_string + """ + __slots__ = () + _function = 'ARRAY_TO_STRING' + +# end ArrayToString + + +class Array(Function): + """ sql: array-type + """ + __slots__ = () + _function = 'ARRAY' + + def __str__(self): + return self._function + '[' + ', '.join( + map(self._format, self.args)) + ']' + +# end Array sel_categorytype = [ @@ -17,7 +51,7 @@ sel_categorytype = [ ] -class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): +class Category(tree(separator='/'), ModelSQL, ModelView): 'Category' __name__ = 'cashbook.category' @@ -36,7 +70,6 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): company = fields.Many2One(string='Company', model_name='company.company', required=True, ondelete="RESTRICT") - sequence = fields.Integer(string='Sequence', select=True) parent = fields.Many2One(string="Parent", model_name='cashbook.category', ondelete='RESTRICT', left='left', right='right') @@ -45,15 +78,35 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): left = fields.Integer(string='Left', required=True, select=True) right = fields.Integer(string='Right', required=True, select=True) + @classmethod + def __register__(cls, module_name): + super(Category, cls).__register__(module_name) + cls.migrate_sequence(module_name) + @classmethod def __setup__(cls): super(Category, cls).__setup__() - cls._order.insert(0, ('name', 'ASC')) + cls._order.insert(0, ('rec_name', 'ASC')) t = cls.__table__() cls._sql_constraints.extend([ - ('name_uniq', Unique(t, t.name, t.company, t.parent), 'cashbook.msg_category_name_unique'), + ('name_uniq', + Unique(t, t.name, t.company, t.parent), + 'cashbook.msg_category_name_unique'), + ('name2_uniq', + Exclude(t, + (t.name, Equal), + (t.cattype, Equal), + where=(t.parent == None)), + 'cashbook.msg_category_name_unique'), ]) + @classmethod + def migrate_sequence(cls, module_name): + """ remove colum 'sequence' + """ + table = cls.__table_handler__(module_name) + table.drop_column('sequence') + @classmethod def default_cattype(cls): return 'out' @@ -70,6 +123,34 @@ class Category(tree(separator='/'), sequence_ordered(), ModelSQL, ModelView): def default_right(): return 0 + @staticmethod + def order_rec_name(tables): + """ order by pos + a recursive sorting + """ + Category2 = Pool().get('cashbook.category') + tab_cat = Category2.__table__() + tab_cat2 = Category2.__table__() + table, _ = tables[None] + + categories = With('id', 'name', 'name_path', recursive=True) + categories.query = tab_cat.select( + tab_cat.id, tab_cat.name, Array(tab_cat.name), + where = tab_cat.parent==None, + ) + categories.query |= tab_cat2.join(categories, + condition=categories.id==tab_cat2.parent, + ).select( + tab_cat2.id, tab_cat2.name, ArrayApppend(categories.name_path, tab_cat2.name), + ) + categories.query.all_ = True + + query = categories.select( + ArrayToString(categories.name_path, '/').as_('rec_name'), + where = table.id==categories.id, + with_ = [categories]) + return [query] + @fields.depends('parent', '_parent_parent.cattype') def on_change_with_parent_cattype(self, name=None): """ get type of parent category or None diff --git a/category.xml b/category.xml index a5dcc66..516e4c7 100644 --- a/category.xml +++ b/category.xml @@ -42,6 +42,20 @@ full copyright notices and license terms. --> + + + Revenue + + + + + + Expense + + + + + Category @@ -59,6 +73,21 @@ full copyright notices and license terms. --> + + + Revenue + + + + + + Expense + + + + + + diff --git a/configuration.py b/configuration.py index e0e460f..2dc986e 100644 --- a/configuration.py +++ b/configuration.py @@ -15,6 +15,10 @@ field_done = fields.Boolean(string='Done', help='Show cashbook lines in Done-state.') field_catnamelong = fields.Boolean(string='Category: Show long name', help='Shows the long name of the category in the Category field of a cash book line.') +field_defbook = fields.Many2One(string='Default Cashbook', + help='The default cashbook is selected when you open the booking wizard.', + model_name='cashbook.book', ondelete='SET NULL', + domain=[('btype', '!=', None)]) class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin): @@ -36,6 +40,7 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin): checked = fields.MultiValue(field_checked) done = fields.MultiValue(field_done) catnamelong = fields.MultiValue(field_catnamelong) + defbook = fields.MultiValue(field_defbook) @classmethod def multivalue_model(cls, field): @@ -44,7 +49,7 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, UserMultiValueMixin): pool = Pool() if field in ['date_from', 'date_to', 'checked', 'done', - 'catnamelong']: + 'catnamelong', 'defbook']: return pool.get('cashbook.configuration_user') return super(Configuration, cls).multivalue_model(field) @@ -82,6 +87,7 @@ class UserConfiguration(ModelSQL, UserValueMixin): checked = field_checked done = field_done catnamelong = field_catnamelong + defbook = field_defbook @classmethod def default_checked(cls): diff --git a/line.py b/line.py index a8ccda4..db402e9 100644 --- a/line.py +++ b/line.py @@ -51,14 +51,17 @@ class Line(Workflow, ModelSQL, ModelView): __name__ = 'cashbook.line' cashbook = fields.Many2One(string='Cashbook', required=True, select=True, - model_name='cashbook.book', ondelete='CASCADE', readonly=True) + model_name='cashbook.book', ondelete='CASCADE', readonly=True, + domain=[('btype', '!=', None)]) date = fields.Date(string='Date', required=True, select=True, states=STATES, depends=DEPENDS) month = fields.Function(fields.Integer(string='Month', readonly=True), 'on_change_with_month', searcher='search_month') number = fields.Char(string='Number', readonly=True) - description = fields.Text(string='Description', + description = fields.Text(string='Description', select=True, states=STATES, depends=DEPENDS) + descr_short = fields.Function(fields.Char(string='Description', readonly=True), + 'on_change_with_descr_short', searcher='search_descr_short') category = fields.Many2One(string='Category', model_name='cashbook.category', ondelete='RESTRICT', states={ @@ -171,6 +174,13 @@ class Line(Workflow, ModelSQL, ModelView): #image = fields.Binary... + @classmethod + def __register__(cls, module_name): + super(Line, cls).__register__(module_name) + + table = cls.__table_handler__(module_name) + table.drop_constraint('amount_val') + @classmethod def __setup__(cls): super(Line, cls).__setup__() @@ -270,19 +280,44 @@ class Line(Workflow, ModelSQL, ModelView): 'cashbook.msg_line_err_write_to_reconciled', datetxt = Report.format_date(line.date), )) - # in case of 'mvin' or 'mvout' - create counterpart - if (line.bookingtype in ['mvout', 'mvin']) and (line.reference is None): - values = { - 'cashbook': line.booktransf.id, - 'bookingtype': 'mvin' if line.bookingtype == 'mvout' else 'mvout', - 'date': line.date, - 'description': line.description, - 'booktransf': line.cashbook.id, - 'reference': line.id, - } - values.update(line.get_amount_by_second_currency(line.booktransf.currency)) - values.update(cls.get_debit_credit(values)) - to_create_line.append(values) + + if line.reference is None: + if line.bookingtype in ['mvout', 'mvin']: + # in case of 'mvin' or 'mvout' - add counterpart + values = { + 'cashbook': line.booktransf.id, + 'bookingtype': 'mvin' if line.bookingtype == 'mvout' else 'mvout', + 'date': line.date, + 'description': line.description, + 'booktransf': line.cashbook.id, + 'reference': line.id, + } + values.update(line.get_amount_by_second_currency(line.booktransf.currency)) + values.update(cls.get_debit_credit(values)) + to_create_line.append(values) + elif line.bookingtype in ['spout', 'spin']: + # splitbooking can have a transfer - add counterpart + for sp_line in line.splitlines: + if sp_line.splittype != 'tr': + continue + + values = { + 'cashbook': sp_line.booktransf.id, + 'date': line.date, + 'description': sp_line.description, + 'booktransf': line.cashbook.id, + 'reference': line.id, + } + if line.bookingtype.endswith('out'): + values['bookingtype'] = 'mvin' + else : + values['bookingtype'] = 'mvout' + values.update(line.get_amount_by_second_currency( + sp_line.booktransf.currency, + amount = sp_line.amount, + )) + values.update(cls.get_debit_credit(values)) + to_create_line.append(values) # add number to line if line.cashbook.number_atcheck == True: @@ -345,7 +380,12 @@ class Line(Workflow, ModelSQL, ModelView): def search_rec_name(cls, name, clause): """ search in description +... """ - return [('description',) + tuple(clause[1:])] + return cls.search_payee(name, clause) + [ + ('description',) + tuple(clause[1:]), + ('category.rec_name',) + tuple(clause[1:]), + ('splitlines.description',) + tuple(clause[1:]), + ('splitlines.category.rec_name',) + tuple(clause[1:]), + ] def get_rec_name(self, name): """ short + name @@ -379,7 +419,7 @@ class Line(Workflow, ModelSQL, ModelView): }): values['amount'] = Currency.compute( self.cashbook.currency, - self.amount, + values['amount'], to_currency) return values @@ -414,6 +454,13 @@ class Line(Workflow, ModelSQL, ModelView): return [tab2] + @staticmethod + def order_descr_short(tables): + """ order by 'description' + """ + table, _ = tables[None] + return [table.description] + @classmethod def search_payee(cls, names, clause): """ search in payee for party or cashbook @@ -454,6 +501,12 @@ class Line(Workflow, ModelSQL, ModelView): """ return [('cashbook.state',) + tuple(clause[1:])] + @classmethod + def search_descr_short(cls, names, clause): + """ search in description + """ + return [('description',) + tuple(clause[1:])] + @fields.depends('amount', 'splitlines') def on_change_splitlines(self): """ update amount if splitlines change @@ -481,6 +534,13 @@ class Line(Workflow, ModelSQL, ModelView): else : self.splitlines = [] + @fields.depends('description') + def on_change_with_descr_short(self, name=None): + """ to speed up list-view + """ + if self.description: + return self.description[:50].replace('\n', '; ') + @fields.depends('party', 'booktransf', 'bookingtype') def on_change_with_payee(self, name=None): """ get party or cashbook @@ -548,8 +608,9 @@ class Line(Workflow, ModelSQL, ModelView): return 2 @fields.depends('id', 'date', 'cashbook', \ - '_parent_cashbook.start_balance', '_parent_cashbook.id',\ - 'reconciliation', '_parent_reconciliation.start_amount') + '_parent_cashbook.id', 'reconciliation', \ + '_parent_reconciliation.start_amount',\ + '_parent_reconciliation.state') def on_change_with_balance(self, name=None): """ compute balance until current line, with current sort order, try to use a reconciliation as start to speed up calculation @@ -558,31 +619,55 @@ class Line(Workflow, ModelSQL, ModelView): Reconciliation = pool.get('cashbook.recon') Line = pool.get('cashbook.line') + def get_from_last_recon(line2): + """ search last reconciliation in state 'done', + generate query + """ + query2 = [] + end_amount = None + + recons = Reconciliation.search([ + ('cashbook.id', '=', self.cashbook.id), + ('date_to', '<=', line2.date), + ('state', '=', 'done'), + ], order=[('date_from', 'DESC')], limit=1) + if len(recons) > 0: + query2.append([ + ('date', '>=', recons[0].date_to), + ('date', '<=', line2.date), + ['OR', + ('reconciliation', '=', None), + ('reconciliation.id', '!=', recons[0]), + ], + ]) + end_amount = recons[0].end_amount + return (query2, end_amount) + if self.cashbook: query = [ ('cashbook.id', '=', self.cashbook.id), ] - balance = self.cashbook.start_balance + balance = Decimal('0.0') # get existing reconciliation, starting before current line # this will speed up calculation of by-line-balance if self.date is not None: - recons = Reconciliation.search([ - ('cashbook.id', '=', self.cashbook.id), - ('date_from', '<=', self.date), - ('state', '=', 'done'), - ], order=[('date_from', 'DESC')], limit=1) - if len(recons) > 0: - query.extend([ - ['OR', - ('date', '>', recons[0].date_from), - [ - ('date', '=', recons[0].date_from), - ('reconciliation.id', '=',recons[0].id), - ], - ] - ]) - balance = recons[0].start_amount + if self.reconciliation: + if self.reconciliation.state == 'done': + query.append( + ('reconciliation.id', '=', self.reconciliation.id), + ) + balance = self.reconciliation.start_amount + else : + (query2, balance2) = get_from_last_recon(self) + query.extend(query2) + if balance2 is not None: + balance = balance2 + else : + (query2, balance2) = get_from_last_recon(self) + query.extend(query2) + if balance2 is not None: + balance = balance2 lines = Line.search(query) for line in lines: @@ -689,6 +774,8 @@ class Line(Workflow, ModelSQL, ModelView): # splitline: category <--> bookingtype? for spline in line.splitlines: + if spline.splittype != 'cat': + continue if not line.bookingtype in types[spline.category.cattype]: raise UserError(gettext( 'cashbook.msg_line_split_invalid_category', @@ -722,7 +809,7 @@ class Line(Workflow, ModelSQL, ModelView): # deny write if line is not 'Edit' if line.state != 'edit': # allow state-update, if its the only action - if not ((len(set({'state', 'reconciliation'}).intersection(values.keys())) > 0) \ + if not ((len(set({'state', 'reconciliation', 'number'}).intersection(values.keys())) > 0) \ and (len(values.keys()) == 1)): raise UserError(gettext( 'cashbook.msg_line_deny_write', @@ -844,16 +931,19 @@ class LineContext(ModelView): cashbook = fields.Many2One(string='Cashbook', required=True, model_name='cashbook.book', + domain=[('btype', '!=', None)], states={ 'readonly': Eval('num_cashbook', 0) < 2, }, depends=['num_cashbook']) date_from = fields.Date(string='Start Date', depends=['date_to'], + help='Limits the date range for the displayed entries.', domain=[ If(Eval('date_to') & Eval('date_from'), ('date_from', '<=', Eval('date_to')), ()), ]) date_to = fields.Date(string='End Date', depends=['date_from'], + help='Limits the date range for the displayed entries.', domain=[ If(Eval('date_to') & Eval('date_from'), ('date_from', '<=', Eval('date_to')), @@ -904,7 +994,7 @@ class LineContext(ModelView): with Transaction().set_context({ '_check_access': True, }): - return CashBook.search_count([]) + return CashBook.search_count([('btype', '!=', None)]) @classmethod def default_done(cls): diff --git a/locale/de.po b/locale/de.po index 073d916..ba68660 100644 --- a/locale/de.po +++ b/locale/de.po @@ -70,10 +70,6 @@ msgctxt "model:ir.message,text:msg_category_type_not_like_parent" msgid "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'." msgstr "Der Typ der aktuellen Kategorie '%(catname)s' muß gleich dem Typ der übergeordneten Kategorie '%(parentname)s' sein." -msgctxt "model:ir.message,text:msg_book_err_startamount_with_lines" -msgid "The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings." -msgstr "Der Anfangsbetrag des Kassenbuchs '%(bookname)s' kann nicht geändert werden, da es bereits Buchungen enthält." - msgctxt "model:ir.message,text:msg_line_deny_recon_by_state" msgid "For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'." msgstr "Für die Abstimmung muss die Zeile '%(recname)s' im Status 'Prüfen' oder 'Fertig' sein." @@ -150,6 +146,10 @@ msgctxt "model:ir.message,text:msg_line_invalid_category" msgid "The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'." msgstr "Die Kategorie der Buchungszeile '%(recname)s' paßt nicht zum Buchungstyp '%(booktype)s'." +msgctxt "model:ir.message,text:msg_book_btype_with_lines" +msgid "The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines." +msgstr "Der Typ kann am Kassenbuch '%(cbname)s' nicht gelöscht werden, da es noch %(numlines)s Zeilen enthält." + ############# # res.group # @@ -258,6 +258,10 @@ msgctxt "model:ir.ui.menu,name:menu_booklist" msgid "Cashbook" msgstr "Kassenbuch" +msgctxt "model:ir.ui.menu,name:menu_booktree" +msgid "Cashbook" +msgstr "Kassenbuch" + msgctxt "model:ir.ui.menu,name:menu_open_lines" msgid "Open Cashbook" msgstr "Kassenbuch öffnen" @@ -274,6 +278,10 @@ msgctxt "model:ir.ui.menu,name:act_category_view" msgid "Category" msgstr "Kategorie" +msgctxt "model:ir.ui.menu,name:menu_enter_booking" +msgid "Enter Booking" +msgstr "Buchung eingeben" + ############# # ir.action # @@ -282,6 +290,10 @@ msgctxt "model:ir.action,name:act_book_view" msgid "Cashbook" msgstr "Kassenbuch" +msgctxt "model:ir.action,name:act_book_tree" +msgid "Cashbook" +msgstr "Kassenbuch" + msgctxt "model:ir.action,name:act_type_view" msgid "Cashbook Type" msgstr "Kassenbuchtyp" @@ -302,6 +314,10 @@ msgctxt "model:ir.action,name:act_wizard_report" msgid "Cashbook Report" msgstr "Kassenbuch Bericht" +msgctxt "model:ir.action,name:act_enterbooking_wiz" +msgid "Enter Booking" +msgstr "Buchung eingeben" + ############################### # ir.action.act_window.domain # @@ -318,6 +334,22 @@ msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all" msgid "All" msgstr "Alle" +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_in" +msgid "Revenue" +msgstr "Einnahmen" + +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out" +msgid "Expense" +msgstr "Ausgaben" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in" +msgid "Revenue" +msgstr "Einnahmen" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out" +msgid "Expense" +msgstr "Ausgaben" + ################### # ir.model.button # @@ -390,18 +422,42 @@ msgctxt "view:cashbook.book:" msgid "Owner and Authorizeds" msgstr "Eigentümer und Autorisierte" +msgctxt "view:cashbook.book:" +msgid "General Information" +msgstr "Allgemein" + +msgctxt "view:cashbook.book:" +msgid "Amount and Numbering" +msgstr "Betrag und Nummerierung" + +msgctxt "view:cashbook.book:" +msgid "Balance" +msgstr "Saldo" + msgctxt "view:cashbook.book:" msgid "Reconciliations" msgstr "Abstimmungen" +msgctxt "view:cashbook.book:" +msgid "Description" +msgstr "Beschreibung" + msgctxt "field:cashbook.book,name:" msgid "Name" msgstr "Name" +msgctxt "field:cashbook.book,description:" +msgid "Description" +msgstr "Beschreibung" + msgctxt "field:cashbook.book,btype:" msgid "Type" msgstr "Typ" +msgctxt "help:cashbook.book,btype:" +msgid "A cash book with type can contain postings. Without type is a view." +msgstr "Ein Kassenbuch mit Typ kann Buchungen enthalten. Ohne Typ ist eine Sicht." + msgctxt "field:cashbook.book,state:" msgid "State" msgstr "Status" @@ -446,9 +502,9 @@ msgctxt "field:cashbook.book,currency:" msgid "Currency" msgstr "Währung" -msgctxt "field:cashbook.book,start_balance:" -msgid "Initial Amount" -msgstr "Anfangsbetrag" +msgctxt "field:cashbook.book,currency_digits:" +msgid "Currency Digits" +msgstr "Nachkommastellen Währung" msgctxt "field:cashbook.book,start_date:" msgid "Initial Date" @@ -482,6 +538,22 @@ msgctxt "help:cashbook.book,number_atcheck:" msgid "The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done." msgstr "Die Nummerierung der Zeilen wird beim Schritt 'Prüfen' erledigt. Bei inaktivem Häkchen passiert dies erst bei 'Fertig'." +msgctxt "field:cashbook.book,parent:" +msgid "Parent" +msgstr "Übergeordnet" + +msgctxt "field:cashbook.book,childs:" +msgid "Children" +msgstr "Untergeordnet" + +msgctxt "field:cashbook.book,left:" +msgid "Left" +msgstr "Links" + +msgctxt "field:cashbook.book,right:" +msgid "Right" +msgstr "Rechts" + ################## # cashbook.split # @@ -570,6 +642,46 @@ msgctxt "field:cashbook.split,state_cashbook:" msgid "State of Cashbook" msgstr "Kassenbuchstatus" +msgctxt "field:cashbook.split,splittype:" +msgid "Type" +msgstr "Typ" + +msgctxt "help:cashbook.split,splittype:" +msgid "Type of split booking line" +msgstr "Typ der Splitbuchungszeile" + +msgctxt "selection:cashbook.split,splittype:" +msgid "Category" +msgstr "Kategorie" + +msgctxt "selection:cashbook.split,splittype:" +msgid "Transfer" +msgstr "Umbuchung" + +msgctxt "field:cashbook.split,target:" +msgid "Target" +msgstr "Ziel" + +msgctxt "selection:cashbook.split,target:" +msgid "Cashbook" +msgstr "Kassenbuch" + +msgctxt "selection:cashbook.split,target:" +msgid "Category" +msgstr "Kategorie" + +msgctxt "field:cashbook.split,cashbook:" +msgid "Cashbook" +msgstr "Kassenbuch" + +msgctxt "field:cashbook.split,owner_cashbook:" +msgid "Owner" +msgstr "Eigentümer" + +msgctxt "field:cashbook.split,booktransf:" +msgid "Source/Dest" +msgstr "Quelle/Ziel" + ################# # cashbook.line # @@ -618,6 +730,10 @@ msgctxt "field:cashbook.line,description:" msgid "Description" msgstr "Beschreibung" +msgctxt "field:cashbook.line,descr_short:" +msgid "Description" +msgstr "Beschreibung" + msgctxt "field:cashbook.line,state:" msgid "State" msgstr "Status" @@ -934,10 +1050,18 @@ msgctxt "field:cashbook.line.context,date_from:" msgid "Start Date" msgstr "Beginndatum" +msgctxt "help:cashbook.line.context,date_from:" +msgid "Limits the date range for the displayed entries." +msgstr "Begrenzt den Datumsbereich für die angezeigten Einträge." + msgctxt "field:cashbook.line.context,date_to:" msgid "End Date" msgstr "Endedatum" +msgctxt "help:cashbook.line.context,date_to:" +msgid "Limits the date range for the displayed entries." +msgstr "Begrenzt den Datumsbereich für die angezeigten Einträge." + ########################## # cashbook.configuration # @@ -946,6 +1070,10 @@ msgctxt "model:cashbook.configuration,name:" msgid "Configuration" msgstr "Konfiguration" +msgctxt "view:cashbook.configuration:" +msgid "Enter Booking Wizard" +msgstr "Dialog: Buchung eingeben" + msgctxt "view:cashbook.configuration:" msgid "Open Cashbook Wizard" msgstr "Dialog: Kassenbuch öffnen" @@ -954,6 +1082,14 @@ msgctxt "view:cashbook.configuration:" msgid "Cashbook" msgstr "Kassenbuch" +msgctxt "field:cashbook.configuration,defbook:" +msgid "Default Cashbook" +msgstr "Standardkassenbuch" + +msgctxt "help:cashbook.configuration,defbook:" +msgid "The default cashbook is selected when you open the booking wizard." +msgstr "Das Standardkassenbuch wird beim Öffnen des Buchungswizards ausgewählt." + msgctxt "field:cashbook.configuration,date_from:" msgid "Start Date" msgstr "Beginndatum" @@ -1026,6 +1162,14 @@ msgctxt "help:cashbook.configuration_user,catnamelong:" msgid "Shows the long name of the category in the Category field of a cash book line." msgstr "Zeigt im Feld 'Kategorie' einer Kassenbuchzeile den langen Namen der Kategorie." +msgctxt "field:cashbook.configuration_user,defbook:" +msgid "Default Cashbook" +msgstr "Standardkassenbuch" + +msgctxt "help:cashbook.configuration_user,defbook:" +msgid "The default cashbook is selected when you open the booking wizard." +msgstr "Das Standardkassenbuch wird beim Öffnen des Buchungswizards ausgewählt." + ################## # cashbook.recon # @@ -1209,3 +1353,103 @@ msgstr "Gesamt" msgctxt "report:cashbook.reprecon:" msgid "Payee" msgstr "Empfänger" + + +############################### +# cashbook.enterbooking.start # +############################### +msgctxt "model:cashbook.enterbooking.start,name:" +msgid "Enter Booking" +msgstr "Buchung eingeben" + +msgctxt "view:cashbook.enterbooking.start:" +msgid "Description" +msgstr "Beschreibung" + +msgctxt "view:cashbook.enterbooking.start:" +msgid "Booking" +msgstr "Buchung" + +msgctxt "field:cashbook.enterbooking.start,cashbook:" +msgid "Cashbook" +msgstr "Kassenbuch" + +msgctxt "field:cashbook.enterbooking.start,cashbooks:" +msgid "Cashbooks" +msgstr "Kassenbücher" + +msgctxt "field:cashbook.enterbooking.start,currency_digits:" +msgid "Currency Digits" +msgstr "Nachkommastellen Währung" + +msgctxt "field:cashbook.enterbooking.start,currency:" +msgid "Currency" +msgstr "Währung" + +msgctxt "field:cashbook.enterbooking.start,bookingtype:" +msgid "Type" +msgstr "Typ" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Revenue" +msgstr "Einnahme" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Revenue Splitbooking" +msgstr "Einnahme Splitbuchung" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Expense" +msgstr "Ausgabe" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Expense Splitbooking" +msgstr "Ausgabe Splitbuchung" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Transfer from" +msgstr "Umbuchung von" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Transfer to" +msgstr "Umbuchung nach" + +msgctxt "field:cashbook.enterbooking.start,amount:" +msgid "Amount" +msgstr "Betrag" + +msgctxt "field:cashbook.enterbooking.start,owner_cashbook:" +msgid "Owner" +msgstr "Eigentümer" + +msgctxt "field:cashbook.enterbooking.start,category:" +msgid "Category" +msgstr "Kategorie" + +msgctxt "field:cashbook.enterbooking.start,booktransf:" +msgid "Source/Dest" +msgstr "Quelle/Ziel" + +msgctxt "field:cashbook.enterbooking.start,party:" +msgid "Party" +msgstr "Partei" + + +######################### +# cashbook.enterbooking # +######################### +msgctxt "model:cashbook.enterbooking,name:" +msgid "Enter Booking" +msgstr "Buchung eingeben" + +msgctxt "wizard_button:cashbook.enterbooking,start,end:" +msgid "Cancel" +msgstr "Abbruch" + +msgctxt "wizard_button:cashbook.enterbooking,start,save_:" +msgid "Save" +msgstr "Speichern" + +msgctxt "wizard_button:cashbook.enterbooking,start,savenext_:" +msgid "Save & Next" +msgstr "Speichern & Weiter" diff --git a/locale/en.po b/locale/en.po index c6f7965..5d58001 100644 --- a/locale/en.po +++ b/locale/en.po @@ -66,10 +66,6 @@ msgctxt "model:ir.message,text:msg_category_type_not_like_parent" msgid "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'." msgstr "The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'." -msgctxt "model:ir.message,text:msg_book_err_startamount_with_lines" -msgid "The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings." -msgstr "The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings." - msgctxt "model:ir.message,text:msg_line_deny_recon_by_state" msgid "For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'." msgstr "For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'." @@ -146,6 +142,10 @@ msgctxt "model:ir.message,text:msg_line_invalid_category" msgid "The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'." msgstr "The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'." +msgctxt "model:ir.message,text:msg_book_btype_with_lines" +msgid "The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines." +msgstr "The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines." + msgctxt "model:res.group,name:group_cashbook" msgid "Cashbook" msgstr "Cashbook" @@ -242,6 +242,10 @@ msgctxt "model:ir.ui.menu,name:menu_booklist" msgid "Cashbook" msgstr "Cashbook" +msgctxt "model:ir.ui.menu,name:menu_booktree" +msgid "Cashbook" +msgstr "Cashbook" + msgctxt "model:ir.ui.menu,name:menu_open_lines" msgid "Open Cashbook" msgstr "Open Cashbook" @@ -258,10 +262,18 @@ msgctxt "model:ir.ui.menu,name:act_category_view" msgid "Category" msgstr "Category" +msgctxt "model:ir.ui.menu,name:menu_enter_booking" +msgid "Enter Booking" +msgstr "Enter Booking" + msgctxt "model:ir.action,name:act_book_view" msgid "Cashbook" msgstr "Cashbook" +msgctxt "model:ir.action,name:act_book_tree" +msgid "Cashbook" +msgstr "Cashbook" + msgctxt "model:ir.action,name:act_type_view" msgid "Cashbook Type" msgstr "Cashbook Type" @@ -282,6 +294,10 @@ msgctxt "model:ir.action,name:act_wizard_report" msgid "Cashbook Report" msgstr "Cashbook Report" +msgctxt "model:ir.action,name:act_enterbooking_wiz" +msgid "Enter Booking" +msgstr "Enter Booking" + msgctxt "model:ir.action.act_window.domain,name:act_line_domain_current" msgid "Current Month" msgstr "Current Month" @@ -294,6 +310,22 @@ msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all" msgid "All" msgstr "All" +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_in" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "model:ir.action.act_window.domain,name:act_category_tree_domain_out" +msgid "Expense" +msgstr "Expense" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_in" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "model:ir.action.act_window.domain,name:act_category_list_domain_out" +msgid "Expense" +msgstr "Expense" + msgctxt "model:ir.model.button,string:line_wfedit_button" msgid "Edit" msgstr "Edit" @@ -354,18 +386,42 @@ msgctxt "view:cashbook.book:" msgid "Owner and Authorizeds" msgstr "Owner and Authorizeds" +msgctxt "view:cashbook.book:" +msgid "General Information" +msgstr "General Information" + +msgctxt "view:cashbook.book:" +msgid "Amount and Numbering" +msgstr "Amount and Numbering" + +msgctxt "view:cashbook.book:" +msgid "Balance" +msgstr "Balance" + msgctxt "view:cashbook.book:" msgid "Reconciliations" msgstr "Reconciliations" +msgctxt "view:cashbook.book:" +msgid "Description" +msgstr "Description" + msgctxt "field:cashbook.book,name:" msgid "Name" msgstr "Name" +msgctxt "field:cashbook.book,description:" +msgid "Description" +msgstr "Description" + msgctxt "field:cashbook.book,btype:" msgid "Type" msgstr "Type" +msgctxt "help:cashbook.book,btype:" +msgid "A cash book with type can contain postings. Without type is a view." +msgstr "A cash book with type can contain postings. Without type is a view." + msgctxt "field:cashbook.book,state:" msgid "State" msgstr "State" @@ -410,9 +466,9 @@ msgctxt "field:cashbook.book,currency:" msgid "Currency" msgstr "Currency" -msgctxt "field:cashbook.book,start_balance:" -msgid "Initial Amount" -msgstr "Initial Amount" +msgctxt "field:cashbook.book,currency_digits:" +msgid "Currency Digits" +msgstr "Currency Digits" msgctxt "field:cashbook.book,start_date:" msgid "Initial Date" @@ -446,6 +502,22 @@ msgctxt "help:cashbook.book,number_atcheck:" msgid "The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done." msgstr "The numbering of the lines is done in the step Check. If the check mark is inactive, this happens with Done." +msgctxt "field:cashbook.book,parent:" +msgid "Parent" +msgstr "Parent" + +msgctxt "field:cashbook.book,childs:" +msgid "Children" +msgstr "Children" + +msgctxt "field:cashbook.book,left:" +msgid "Left" +msgstr "Left" + +msgctxt "field:cashbook.book,right:" +msgid "Right" +msgstr "Right" + msgctxt "model:cashbook.split,name:" msgid "Split booking line" msgstr "Split booking line" @@ -530,6 +602,46 @@ msgctxt "field:cashbook.split,state_cashbook:" msgid "State of Cashbook" msgstr "State of Cashbook" +msgctxt "field:cashbook.split,splittype:" +msgid "Type" +msgstr "Type" + +msgctxt "help:cashbook.split,splittype:" +msgid "Type of split booking line" +msgstr "Type of split booking line" + +msgctxt "selection:cashbook.split,splittype:" +msgid "Category" +msgstr "Category" + +msgctxt "selection:cashbook.split,splittype:" +msgid "Transfer" +msgstr "Transfer" + +msgctxt "field:cashbook.split,target:" +msgid "Target" +msgstr "Target" + +msgctxt "selection:cashbook.split,target:" +msgid "Cashbook" +msgstr "Cashbook" + +msgctxt "selection:cashbook.split,target:" +msgid "Category" +msgstr "Category" + +msgctxt "field:cashbook.split,cashbook:" +msgid "Cashbook" +msgstr "Cashbook" + +msgctxt "field:cashbook.split,owner_cashbook:" +msgid "Owner" +msgstr "Owner" + +msgctxt "field:cashbook.split,booktransf:" +msgid "Source/Dest" +msgstr "Source/Dest" + msgctxt "model:cashbook.line,name:" msgid "Cashbook Line" msgstr "Cashbook Line" @@ -574,6 +686,10 @@ msgctxt "field:cashbook.line,description:" msgid "Description" msgstr "Description" +msgctxt "field:cashbook.line,descr_short:" +msgid "Description" +msgstr "Description" + msgctxt "field:cashbook.line,state:" msgid "State" msgstr "State" @@ -870,14 +986,26 @@ msgctxt "field:cashbook.line.context,date_from:" msgid "Start Date" msgstr "Start Date" +msgctxt "help:cashbook.line.context,date_from:" +msgid "Limits the date range for the displayed entries." +msgstr "Limits the date range for the displayed entries." + msgctxt "field:cashbook.line.context,date_to:" msgid "End Date" msgstr "End Date" +msgctxt "help:cashbook.line.context,date_to:" +msgid "Limits the date range for the displayed entries." +msgstr "Limits the date range for the displayed entries." + msgctxt "model:cashbook.configuration,name:" msgid "Configuration" msgstr "Configuration" +msgctxt "view:cashbook.configuration:" +msgid "Enter Booking Wizard" +msgstr "Enter Booking Wizard" + msgctxt "view:cashbook.configuration:" msgid "Open Cashbook Wizard" msgstr "Open Cashbook Wizard" @@ -886,6 +1014,14 @@ msgctxt "view:cashbook.configuration:" msgid "Cashbook" msgstr "Cashbook" +msgctxt "field:cashbook.configuration,defbook:" +msgid "Default Cashbook" +msgstr "Default Cashbook" + +msgctxt "help:cashbook.configuration,defbook:" +msgid "The default cashbook is selected when you open the booking wizard." +msgstr "The default cashbook is selected when you open the booking wizard." + msgctxt "field:cashbook.configuration,date_from:" msgid "Start Date" msgstr "Start Date" @@ -954,6 +1090,14 @@ msgctxt "help:cashbook.configuration_user,catnamelong:" msgid "Shows the long name of the category in the Category field of a cash book line." msgstr "Shows the long name of the category in the Category field of a cash book line." +msgctxt "field:cashbook.configuration_user,defbook:" +msgid "Default Cashbook" +msgstr "Default Cashbook" + +msgctxt "help:cashbook.configuration_user,defbook:" +msgid "The default cashbook is selected when you open the booking wizard." +msgstr "The default cashbook is selected when you open the booking wizard." + msgctxt "model:cashbook.recon,name:" msgid "Cashbook Reconciliation" msgstr "Cashbook Reconciliation" @@ -1122,3 +1266,95 @@ msgctxt "report:cashbook.reprecon:" msgid "Total" msgstr "Total" +msgctxt "report:cashbook.reprecon:" +msgid "Payee" +msgstr "Payee" + +msgctxt "model:cashbook.enterbooking.start,name:" +msgid "Enter Booking" +msgstr "Enter Booking" + +msgctxt "view:cashbook.enterbooking.start:" +msgid "Description" +msgstr "Description" + +msgctxt "view:cashbook.enterbooking.start:" +msgid "Booking" +msgstr "Booking" + +msgctxt "field:cashbook.enterbooking.start,cashbook:" +msgid "Cashbook" +msgstr "Cashbook" + +msgctxt "field:cashbook.enterbooking.start,cashbooks:" +msgid "Cashbooks" +msgstr "Cashbooks" + +msgctxt "field:cashbook.enterbooking.start,currency_digits:" +msgid "Currency Digits" +msgstr "Currency Digits" + +msgctxt "field:cashbook.enterbooking.start,currency:" +msgid "Currency" +msgstr "Currency" + +msgctxt "field:cashbook.enterbooking.start,bookingtype:" +msgid "Type" +msgstr "Type" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Revenue Splitbooking" +msgstr "Revenue Splitbooking" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Expense" +msgstr "Expense" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Expense Splitbooking" +msgstr "Expense Splitbooking" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Transfer from" +msgstr "Transfer from" + +msgctxt "selection:cashbook.enterbooking.start,bookingtype:" +msgid "Transfer to" +msgstr "Transfer to" + +msgctxt "field:cashbook.enterbooking.start,amount:" +msgid "Amount" +msgstr "Amount" + +msgctxt "field:cashbook.enterbooking.start,owner_cashbook:" +msgid "Owner" +msgstr "Owner" + +msgctxt "field:cashbook.enterbooking.start,category:" +msgid "Category" +msgstr "Category" + +msgctxt "field:cashbook.enterbooking.start,booktransf:" +msgid "Source/Dest" +msgstr "Source/Dest" + +msgctxt "field:cashbook.enterbooking.start,party:" +msgid "Party" +msgstr "Party" + +msgctxt "model:cashbook.enterbooking,name:" +msgid "Enter Booking" +msgstr "Enter Booking" + +msgctxt "wizard_button:cashbook.enterbooking,start,end:" +msgid "Cancel" +msgstr "Cancel" + +msgctxt "wizard_button:cashbook.enterbooking,start,save_:" +msgid "Save" +msgstr "Save" + diff --git a/menu.xml b/menu.xml index c37eb13..f05d611 100644 --- a/menu.xml +++ b/menu.xml @@ -56,10 +56,23 @@ full copyright notices and license terms. --> + + + + + + + + + + + + parent="menu_cashbook" sequence="30"/> @@ -70,9 +83,21 @@ full copyright notices and license terms. --> + + + + + + + + + + + parent="menu_booktree" sequence="10"/> diff --git a/message.xml b/message.xml index 8fea230..14bc840 100644 --- a/message.xml +++ b/message.xml @@ -53,9 +53,6 @@ full copyright notices and license terms. --> The type of the current category '%(catname)s' must be equal to the type of the parent category '%(parentname)s'. - - The initial amount of the cash book '%(bookname)s' cannot be changed because it already contains bookings. - For reconciliation, the line '%(recname)s' must be in the status 'Check' or 'Done'. @@ -113,6 +110,9 @@ full copyright notices and license terms. --> The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'. + + The type cannot be deleted on the cash book '%(cbname)s' because it still contains %(numlines)s lines. + diff --git a/reconciliation.py b/reconciliation.py index 72040ee..efa9e4d 100644 --- a/reconciliation.py +++ b/reconciliation.py @@ -96,7 +96,7 @@ class Reconciliation(Workflow, ModelSQL, ModelView): @classmethod def __setup__(cls): super(Reconciliation, cls).__setup__() - cls._order.insert(0, ('date_from', 'ASC')) + cls._order.insert(0, ('date_from', 'DESC')) cls._transitions |= set(( ('edit', 'check'), ('check', 'done'), @@ -226,7 +226,7 @@ class Reconciliation(Workflow, ModelSQL, ModelView): )) values['start_amount'] = reconciliation.predecessor.end_amount else : - values['start_amount'] = reconciliation.cashbook.start_balance + values['start_amount'] = Decimal('0.0') values['end_amount'] = values['start_amount'] # add 'checked'-lines to reconciliation diff --git a/splitline.py b/splitline.py index 0c691ad..f956b82 100644 --- a/splitline.py +++ b/splitline.py @@ -14,6 +14,17 @@ from .line import sel_linetype, sel_bookingtype, STATES, DEPENDS from .book import sel_state_book +sel_linetype = [ + ('cat', 'Category'), + ('tr', 'Transfer'), + ] + +sel_target = [ + ('cashbook.book', 'Cashbook'), + ('cashbook.category', 'Category'), + ] + + class SplitLine(ModelSQL, ModelView): 'Split booking line' __name__ = 'cashbook.split' @@ -23,9 +34,16 @@ class SplitLine(ModelSQL, ModelView): readonly=True) description = fields.Text(string='Description', states=STATES, depends=DEPENDS) + splittype = fields.Selection(string='Type', required=True, + help='Type of split booking line', selection=sel_linetype, + states=STATES, depends=DEPENDS) category = fields.Many2One(string='Category', model_name='cashbook.category', ondelete='RESTRICT', - states=STATES, depends=DEPENDS+['bookingtype'], required=True, + states={ + 'readonly': STATES['readonly'], + 'required': Eval('splittype', '') == 'cat', + 'invisible': Eval('splittype', '') != 'cat', + }, depends=DEPENDS+['bookingtype', 'splittype'], domain=[ If( Eval('bookingtype', '') == 'spin', @@ -34,9 +52,23 @@ class SplitLine(ModelSQL, ModelView): )]) category_view = fields.Function(fields.Char(string='Category', readonly=True), 'on_change_with_category_view') + booktransf = fields.Many2One(string='Source/Dest', + ondelete='RESTRICT', model_name='cashbook.book', + domain=[ + ('owner.id', '=', Eval('owner_cashbook', -1)), + ('id', '!=', Eval('cashbook', -1)), + ], + states={ + 'readonly': STATES['readonly'], + 'invisible': Eval('splittype', '') != 'tr', + 'required': Eval('splittype', '') == 'tr', + }, depends=DEPENDS+['bookingtype', 'owner_cashbook', 'cashbook']) + amount = fields.Numeric(string='Amount', digits=(16, Eval('currency_digits', 2)), required=True, states=STATES, depends=DEPENDS+['currency_digits']) + target = fields.Function(fields.Reference(string='Target', readonly=True, + selection=sel_target), 'on_change_with_target') currency = fields.Function(fields.Many2One(model_name='currency.currency', string="Currency", readonly=True), 'on_change_with_currency') currency_digits = fields.Function(fields.Integer(string='Currency Digits', @@ -45,18 +77,30 @@ class SplitLine(ModelSQL, ModelView): selection=sel_bookingtype), 'on_change_with_bookingtype') state = fields.Function(fields.Selection(string='State', readonly=True, selection=sel_linetype), 'on_change_with_state') + cashbook = fields.Function(fields.Many2One(string='Cashbook', + readonly=True, states={'invisible': True}, model_name='cashbook.book'), + 'on_change_with_cashbook') state_cashbook = fields.Function(fields.Selection(string='State of Cashbook', readonly=True, states={'invisible': True}, selection=sel_state_book), 'on_change_with_state_cashbook') + owner_cashbook = fields.Function(fields.Many2One(string='Owner', readonly=True, + states={'invisible': True}, model_name='res.user'), + 'on_change_with_owner_cashbook') + + @classmethod + def default_splittype(cls): + """ default category + """ + return 'cat' def get_rec_name(self, name): """ short + name """ - return '%(type)s|%(amount)s %(symbol)s|%(desc)s [%(category)s]' % { + return '%(type)s|%(amount)s %(symbol)s|%(desc)s [%(target)s]' % { 'desc': (self.description or '-')[:40], 'amount': Report.format_number(self.amount, None), 'symbol': getattr(self.currency, 'symbol', '-'), - 'category': self.category_view, + 'target': self.category_view if self.splittype == 'cat' else self.booktransf.rec_name, 'type': gettext('cashbook.msg_line_bookingtype_%s' % self.line.bookingtype), } @@ -80,6 +124,28 @@ class SplitLine(ModelSQL, ModelView): to_currency) return values + @fields.depends('splittype', 'category', 'booktransf') + def on_change_splittype(self): + """ clear category if not valid type + """ + if self.splittype: + if self.splittype == 'cat': + self.booktransf = None + if self.splittype == 'tr': + self.category = None + + @fields.depends('splittype', 'category', 'booktransf') + def on_change_with_target(self, name=None): + """ get category or cashbook + """ + if self.splittype: + if self.splittype == 'cat': + if self.category: + return 'cashbook.category,%d' % self.category.id + elif self.splittype == 'tr': + if self.booktransf: + return 'cashbook.book,%d' % self.booktransf.id + @fields.depends('category') def on_change_with_category_view(self, name=None): """ show optimizef form of category for list-view @@ -101,6 +167,13 @@ class SplitLine(ModelSQL, ModelView): if self.line: return self.line.state + @fields.depends('line', '_parent_line.cashbook') + def on_change_with_cashbook(self, name=None): + """ get cashbook + """ + if self.line: + return self.line.cashbook.id + @fields.depends('line', '_parent_line.cashbook') def on_change_with_state_cashbook(self, name=None): """ get state of cashbook @@ -108,6 +181,13 @@ class SplitLine(ModelSQL, ModelView): if self.line: return self.line.cashbook.state + @fields.depends('line', '_parent_line.cashbook') + def on_change_with_owner_cashbook(self, name=None): + """ get current owner + """ + if self.line: + return self.line.cashbook.owner.id + @fields.depends('line', '_parent_line.bookingtype') def on_change_with_bookingtype(self, name=None): """ get type diff --git a/tests/__init__.py b/tests/__init__.py index f557851..dea659d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,11 +11,14 @@ from trytond.modules.cashbook.tests.test_splitline import SplitLineTestCase from trytond.modules.cashbook.tests.test_config import ConfigTestCase from trytond.modules.cashbook.tests.test_category import CategoryTestCase from trytond.modules.cashbook.tests.test_reconciliation import ReconTestCase +from trytond.modules.cashbook.tests.test_bookingwiz import BookingWizardTestCase + __all__ = ['suite'] class CashbookTestCase(\ + BookingWizardTestCase,\ ReconTestCase,\ CategoryTestCase,\ ConfigTestCase,\ diff --git a/tests/test_book.py b/tests/test_book.py index 34e5c3e..67fea04 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -51,10 +51,37 @@ class BookTestCase(ModuleTestCase): 'number_sequ': self.prep_sequence().id, }]) self.assertEqual(book.name, 'Book 1') + self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open') self.assertEqual(book.btype.rec_name, 'CAS - Cash') self.assertEqual(book.state, 'open') self.assertEqual(book.state_string, 'Open') + @with_transaction() + def test_book_create_hierarchy(self): + """ create cashbook, hierarchical + """ + pool = Pool() + Book = pool.get('cashbook.book') + + types = self.prep_type() + company = self.prep_company() + book, = Book.create([{ + 'name': 'Level 1', + 'btype': None, + 'company': company.id, + 'childs': [('create', [{ + 'name': 'Level 2', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + }])], + }]) + self.assertEqual(book.name, 'Level 1') + self.assertEqual(book.rec_name, 'Level 1') + self.assertEqual(len(book.childs), 1) + self.assertEqual(book.childs[0].rec_name, 'Level 1/Level 2 | 0.00 usd | Open') + @with_transaction() def test_book_deny_delete_open(self): """ create cashbook, add lines, try to delete in state 'open' @@ -90,6 +117,62 @@ class BookTestCase(ModuleTestCase): Book.delete, [book]) + @with_transaction() + def test_book_deny_btype_set_none(self): + """ create cashbook, add lines, + try to set btype to None with lines + """ + pool = Pool() + Book = pool.get('cashbook.book') + + types = self.prep_type() + category = self.prep_category(cattype='in') + company = self.prep_company() + party = self.prep_party() + book, = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'test 1', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + 'party': party.id, + }])], + }]) + self.assertEqual(book.name, 'Book 1') + self.assertEqual(book.btype.rec_name, 'CAS - Cash') + + self.assertRaisesRegex(UserError, + "The type cannot be deleted on the cash book 'Book 1 | 1.00 usd | Open' because it still contains 1 lines.", + Book.write, + *[ + [book], + { + 'btype': None, + }, + ]) + + Book.write(*[ + [book], + { + 'lines': [('delete', [book.lines[0].id])], + }]) + self.assertEqual(len(book.lines), 0) + self.assertEqual(book.btype.rec_name, 'CAS - Cash') + + Book.write(*[ + [book], + { + 'btype': None, + }]) + self.assertEqual(book.btype, None) + @with_transaction() def test_book_deny_delete_closed(self): """ create cashbook, add lines, try to delete in state 'closed' @@ -225,62 +308,6 @@ class BookTestCase(ModuleTestCase): }, ]) - @with_transaction() - def test_book_deny_update_start_amount(self): - """ create cashbook, add lines, update start-amount - """ - pool = Pool() - Book = pool.get('cashbook.book') - - types = self.prep_type() - company = self.prep_company() - category = self.prep_category(cattype='in') - party = self.prep_party() - book, = Book.create([{ - 'name': 'Book 1', - 'btype': types.id, - 'company': company.id, - 'currency': company.currency.id, - 'number_sequ': self.prep_sequence().id, - }]) - self.assertEqual(book.name, 'Book 1') - self.assertEqual(book.start_balance, Decimal('0.0')) - self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open') - - Book.write(*[ - [book], - { - 'start_balance': Decimal('1.0'), - }]) - self.assertEqual(book.start_balance, Decimal('1.0')) - self.assertEqual(book.balance, Decimal('1.0')) - - Book.write(*[ - [book], - { - 'lines': [('create', [{ - 'amount': Decimal('2.0'), - 'description': 'Test', - 'category': category.id, - 'bookingtype': 'in', - 'party': party.id, - }])], - }]) - self.assertEqual(book.start_balance, Decimal('1.0')) - self.assertEqual(book.balance, Decimal('3.0')) - self.assertEqual(len(book.lines), 1) - self.assertEqual(book.lines[0].balance, Decimal('3.0')) - - self.assertRaisesRegex(UserError, - "The initial amount of the cash book 'Fridas book | 3.00 usd | Open' cannot be changed because it already contains bookings.", - Book.write, - *[ - [book], - { - 'start_balance': Decimal('1.5'), - }, - ]) - @with_transaction() def test_book_permission_owner(self): """ create book + 2x users, add users to group, check access diff --git a/tests/test_bookingwiz.py b/tests/test_bookingwiz.py new file mode 100644 index 0000000..6a6f1e1 --- /dev/null +++ b/tests/test_bookingwiz.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# This file is part of the cashbook-module from m-ds for Tryton. +# The COPYRIGHT file at the top level of this repository contains the +# full copyright notices and license terms. + +from trytond.tests.test_tryton import ModuleTestCase, with_transaction +from trytond.pool import Pool +from trytond.transaction import Transaction +from trytond.exceptions import UserError +from datetime import date +from decimal import Decimal +from unittest.mock import MagicMock + + +class BookingWizardTestCase(ModuleTestCase): + 'Test cashbook booking wizard module' + module = 'cashbook' + + @with_transaction() + def test_bookwiz_expense(self): + """ run booking-wizard to store expense + """ + pool = Pool() + BookingWiz = pool.get('cashbook.enterbooking', type='wizard') + Book = pool.get('cashbook.book') + Category = pool.get('cashbook.category') + Party = pool.get('party.party') + IrDate = pool.get('ir.date') + + company = self.prep_company() + with Transaction().set_context({ + 'company': company.id, + }): + types = self.prep_type() + book, = Book.create([{ + 'name': 'Cash Book', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 1, 1), + }]) + + party, = Party.create([{ + 'name': 'Foodshop Zehlendorf', + 'addresses':[('create', [{}])], + }]) + + categories = Category.create([{ + 'name':'Income', + 'cattype': 'in', + }, { + 'name': 'Food', + 'cattype': 'out', + }]) + + (sess_id, start_state, end_state) = BookingWiz.create() + w_obj = BookingWiz(sess_id) + self.assertEqual(start_state, 'start') + self.assertEqual(end_state, 'end') + + result = BookingWiz.execute(sess_id, {}, start_state) + self.assertEqual(list(result.keys()), ['view']) + self.assertEqual(result['view']['defaults']['bookingtype'], 'out') + self.assertEqual(result['view']['defaults']['cashbook'], None) + self.assertEqual(result['view']['defaults']['amount'], None) + self.assertEqual(result['view']['defaults']['party'], None) + self.assertEqual(result['view']['defaults']['booktransf'], None) + self.assertEqual(result['view']['defaults']['description'], None) + self.assertEqual(result['view']['defaults']['category'], None) + + self.assertEqual(len(book.lines), 0) + + r1 = { + 'amount': Decimal('10.0'), + 'cashbook': book.id, + 'party': party.id, + 'description': 'Test 1', + 'category': categories[1].id, + 'bookingtype': 'out', + } + for x in r1.keys(): + setattr(w_obj.start, x, r1[x]) + + IrDate.today = MagicMock(return_value=date(2022, 5, 1)) + result = BookingWiz.execute(sess_id, {'start': r1}, 'save_') + BookingWiz.delete(sess_id) + IrDate.today = MagicMock(return_value=date.today()) + + self.assertEqual(len(book.lines), 1) + self.assertEqual(book.lines[0].rec_name, '05/01/2022|Exp|-10.00 usd|Test 1 [Food]') + + @with_transaction() + def test_bookwiz_transfer(self): + """ run booking-wizard to store expense + """ + pool = Pool() + BookingWiz = pool.get('cashbook.enterbooking', type='wizard') + Book = pool.get('cashbook.book') + Category = pool.get('cashbook.category') + Party = pool.get('party.party') + IrDate = pool.get('ir.date') + + company = self.prep_company() + with Transaction().set_context({ + 'company': company.id, + }): + types = self.prep_type() + books = Book.create([{ + 'name': 'Cash Book', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 1, 1), + }, { + 'name': 'Bank', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 1, 1), + }]) + + party, = Party.create([{ + 'name': 'Foodshop Zehlendorf', + 'addresses':[('create', [{}])], + }]) + + categories = Category.create([{ + 'name':'Income', + 'cattype': 'in', + }, { + 'name': 'Food', + 'cattype': 'out', + }]) + + (sess_id, start_state, end_state) = BookingWiz.create() + w_obj = BookingWiz(sess_id) + self.assertEqual(start_state, 'start') + self.assertEqual(end_state, 'end') + + result = BookingWiz.execute(sess_id, {}, start_state) + self.assertEqual(list(result.keys()), ['view']) + self.assertEqual(result['view']['defaults']['bookingtype'], 'out') + self.assertEqual(result['view']['defaults']['cashbook'], None) + self.assertEqual(result['view']['defaults']['amount'], None) + self.assertEqual(result['view']['defaults']['party'], None) + self.assertEqual(result['view']['defaults']['booktransf'], None) + self.assertEqual(result['view']['defaults']['description'], None) + self.assertEqual(result['view']['defaults']['category'], None) + + self.assertEqual(len(books[0].lines), 0) + self.assertEqual(len(books[1].lines), 0) + + r1 = { + 'amount': Decimal('10.0'), + 'cashbook': books[0].id, + 'description': 'Test 1', + 'booktransf': books[1].id, + 'bookingtype': 'mvout', + } + for x in r1.keys(): + setattr(w_obj.start, x, r1[x]) + + IrDate.today = MagicMock(return_value=date(2022, 5, 1)) + result = BookingWiz.execute(sess_id, {'start': r1}, 'save_') + BookingWiz.delete(sess_id) + IrDate.today = MagicMock(return_value=date.today()) + + self.assertEqual(len(books[0].lines), 1) + self.assertEqual(len(books[1].lines), 0) + self.assertEqual(books[0].lines[0].rec_name, + '05/01/2022|to|-10.00 usd|Test 1 [Bank | 0.00 usd | Open]') + +# end BookingWizardTestCase diff --git a/tests/test_category.py b/tests/test_category.py index bfea57e..898e699 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -28,6 +28,72 @@ class CategoryTestCase(ModuleTestCase): }]) return category + @with_transaction() + def test_category_check_rec_name(self): + """ create category, test rec_name, search, order + """ + pool = Pool() + Category = pool.get('cashbook.category') + company = self.prep_company() + + Category.create([{ + 'company': company.id, + 'name': 'Level 1', + 'cattype': 'in', + 'childs': [('create', [{ + 'company': company.id, + 'name': 'Level 2a', + 'cattype': 'in', + }, { + 'company': company.id, + 'name': 'Level 2b', + 'cattype': 'in', + }])], + }, { + 'company': company.id, + 'name': 'Level 1b', + 'cattype': 'in', + 'childs': [('create', [{ + 'company': company.id, + 'name': 'Level 1b.2a', + 'cattype': 'in', + }, { + 'company': company.id, + 'name': 'Level 1b.2b', + 'cattype': 'in', + }])], + }]) + + self.assertEqual(Category.search_count([ + ('rec_name', 'ilike', '%1b.2b%'), + ]), 1) + self.assertEqual(Category.search_count([ + ('rec_name', 'ilike', '%1b.2%'), + ]), 2) + self.assertEqual(Category.search_count([ + ('rec_name', '=', 'Level 1b/Level 1b.2b'), + ]), 1) + + # ordering #1 + categories = Category.search([], order=[('rec_name', 'ASC')]) + self.assertEqual(len(categories), 6) + self.assertEqual(categories[0].rec_name, 'Level 1') + self.assertEqual(categories[1].rec_name, 'Level 1b') + self.assertEqual(categories[2].rec_name, 'Level 1b/Level 1b.2a') + self.assertEqual(categories[3].rec_name, 'Level 1b/Level 1b.2b') + self.assertEqual(categories[4].rec_name, 'Level 1/Level 2a') + self.assertEqual(categories[5].rec_name, 'Level 1/Level 2b') + + # ordering #2 + categories = Category.search([], order=[('rec_name', 'DESC')]) + self.assertEqual(len(categories), 6) + self.assertEqual(categories[0].rec_name, 'Level 1/Level 2b') + self.assertEqual(categories[1].rec_name, 'Level 1/Level 2a') + self.assertEqual(categories[2].rec_name, 'Level 1b/Level 1b.2b') + self.assertEqual(categories[3].rec_name, 'Level 1b/Level 1b.2a') + self.assertEqual(categories[4].rec_name, 'Level 1b') + self.assertEqual(categories[5].rec_name, 'Level 1') + @with_transaction() def test_category_create_check_category_type(self): """ create category, update type of category @@ -90,6 +156,7 @@ class CategoryTestCase(ModuleTestCase): cat1, = Category.create([{ 'name': 'Test 1', 'description': 'Info', + 'cattype': 'in', }]) self.assertEqual(cat1.name, 'Test 1') self.assertEqual(cat1.rec_name, 'Test 1') @@ -97,10 +164,11 @@ class CategoryTestCase(ModuleTestCase): self.assertEqual(cat1.company.rec_name, 'm-ds') self.assertEqual(cat1.parent, None) - # duplicate, allowed + # duplicate of different type, allowed cat2, = Category.create([{ 'name': 'Test 1', 'description': 'Info', + 'cattype': 'out', }]) self.assertEqual(cat2.name, 'Test 1') self.assertEqual(cat2.rec_name, 'Test 1') @@ -108,6 +176,16 @@ class CategoryTestCase(ModuleTestCase): self.assertEqual(cat2.company.rec_name, 'm-ds') self.assertEqual(cat2.parent, None) + # deny duplicate of same type + self.assertRaisesRegex(UserError, + 'The category name already exists at this level.', + Category.create, + [{ + 'name': 'Test 1', + 'description': 'Info', + 'cattype': 'in', + }]) + @with_transaction() def test_category_create_nodupl_diff_level(self): """ create category diff --git a/tests/test_config.py b/tests/test_config.py index 7932229..368ad10 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -42,6 +42,7 @@ class ConfigTestCase(ModuleTestCase): self.assertEqual(cfg2.checked, True) self.assertEqual(cfg2.done, False) self.assertEqual(cfg2.catnamelong, True) + self.assertEqual(cfg2.defbook, None) return cfg2 def prep_party(self, name='Party'): @@ -105,6 +106,37 @@ class ConfigTestCase(ModuleTestCase): """ self.prep_config() + @with_transaction() + def test_config_defbook(self): + """ create config, add default-cashbook + """ + pool = Pool() + Configuration = pool.get('cashbook.configuration') + Book = pool.get('cashbook.book') + + self.prep_config() + types = self.prep_type() + company = self.prep_company() + book, = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + }]) + self.assertEqual(book.name, 'Book 1') + + cfg1 = Configuration.get_singleton() + + Configuration.write(*[ + [cfg1], + { + 'defbook': book.id, + }]) + + cfg2 = Configuration.get_singleton() + self.assertEqual(cfg2.defbook.rec_name, 'Book 1 | 0.00 usd | Open') + @with_transaction() def test_config_create_multi_user(self): """ create config, multi-user diff --git a/tests/test_line.py b/tests/test_line.py index e08d0b1..450fc9c 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -366,6 +366,93 @@ class LineTestCase(ModuleTestCase): self.assertEqual(book.lines[3].reconciliation, None) self.assertEqual(book.lines[3].state, 'edit') + @with_transaction() + def test_line_set_number_with_done(self): + """ create cashbook + line, write number to line + at state-change check->done + """ + pool = Pool() + Book = pool.get('cashbook.book') + Lines = pool.get('cashbook.line') + Reconciliation = pool.get('cashbook.recon') + + types = self.prep_type() + category = self.prep_category(cattype='in') + company = self.prep_company() + party = self.prep_party() + with Transaction().set_context({ + 'company': company.id, + }): + book, = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'number_atcheck': False, + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Text 1', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + 'party': party.id, + }, { + 'date': date(2022, 5, 2), + 'description': 'Text 2', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + 'party': party.id, + }])], + }]) + self.assertEqual(book.name, 'Book 1') + self.assertEqual(book.btype.rec_name, 'CAS - Cash') + self.assertEqual(book.state, 'open') + self.assertEqual(book.number_atcheck, False) + self.assertEqual(len(book.lines), 2) + self.assertEqual(book.lines[0].date, date(2022, 5, 1)) + self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') + self.assertEqual(book.lines[0].state_cashbook, 'open') + self.assertEqual(book.lines[1].date, date(2022, 5, 2)) + self.assertEqual(book.lines[1].rec_name, '05/02/2022|Rev|1.00 usd|Text 2 [Cat1]') + + # add reconciliation + Book.write(*[ + [book], + { + 'reconciliations': [('create', [{ + 'date': date(2022, 5, 1), + 'date_from': date(2022, 5, 1), + 'date_to': date(2022, 5, 30), + }])], + }]) + self.assertEqual(len(book.reconciliations), 1) + self.assertEqual(len(book.reconciliations[0].lines), 0) + self.assertEqual(book.reconciliations[0].date_from, date(2022, 5, 1)) + self.assertEqual(book.reconciliations[0].date_to, date(2022, 5, 30)) + self.assertEqual(book.reconciliations[0].state, 'edit') + + Lines.wfcheck(book.lines) + self.assertEqual(book.lines[0].state, 'check') + self.assertEqual(book.lines[0].number, None) + self.assertEqual(book.lines[1].state, 'check') + self.assertEqual(book.lines[1].number, None) + + Reconciliation.wfcheck(book.reconciliations) + self.assertEqual(len(book.reconciliations[0].lines), 2) + self.assertEqual(book.reconciliations[0].lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') + self.assertEqual(book.reconciliations[0].lines[1].rec_name, '05/02/2022|Rev|1.00 usd|Text 2 [Cat1]') + self.assertEqual(book.reconciliations[0].lines[0].number, None) + self.assertEqual(book.reconciliations[0].lines[1].number, None) + + Reconciliation.wfdone(book.reconciliations) + self.assertEqual(book.reconciliations[0].lines[0].number, '1') + self.assertEqual(book.reconciliations[0].lines[0].state, 'done') + self.assertEqual(book.reconciliations[0].lines[1].number, '2') + self.assertEqual(book.reconciliations[0].lines[1].state, 'done') + @with_transaction() def test_line_create_check_names_search(self): """ create cashbook + line @@ -373,10 +460,18 @@ class LineTestCase(ModuleTestCase): pool = Pool() Book = pool.get('cashbook.book') Lines = pool.get('cashbook.line') + Category = pool.get('cashbook.category') types = self.prep_type() - category = self.prep_category(cattype='in') company = self.prep_company() + category = self.prep_category(cattype='in') + category2, = Category.create([{ + 'name': 'sp-cat1', + 'cattype': 'in', + 'company': company.id, + }]) + self.assertEqual(category2.rec_name, 'sp-cat1') + party = self.prep_party() book, = Book.create([{ 'name': 'Book 1', @@ -399,33 +494,60 @@ class LineTestCase(ModuleTestCase): 'bookingtype': 'in', 'amount': Decimal('1.0'), 'party': party.id, + }, { + 'date': date(2022, 5, 3), + 'description': 'Text 3', + 'bookingtype': 'spin', + 'amount': Decimal('1.0'), + 'party': party.id, + 'splitlines': [('create', [{ + 'amount': Decimal('1.0'), + 'description': 'text3-spline1', + 'category': category2.id, + }])], }])], }]) self.assertEqual(book.name, 'Book 1') self.assertEqual(book.btype.rec_name, 'CAS - Cash') self.assertEqual(book.state, 'open') - self.assertEqual(len(book.lines), 2) + self.assertEqual(len(book.lines), 3) self.assertEqual(book.lines[0].date, date(2022, 5, 1)) self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') self.assertEqual(book.lines[0].state_cashbook, 'open') self.assertEqual(book.lines[1].date, date(2022, 5, 2)) self.assertEqual(book.lines[1].rec_name, '05/02/2022|Rev|1.00 usd|Text 2 [Cat1]') + self.assertEqual(book.lines[2].date, date(2022, 5, 3)) + self.assertEqual(book.lines[2].rec_name, '05/03/2022|Rev/Sp|1.00 usd|Text 3 [-]') self.assertEqual(Lines.search_count([('rec_name', '=', 'Text 1')]), 1) self.assertEqual(Lines.search_count([('rec_name', '=', 'Text 1a')]), 0) - self.assertEqual(Lines.search_count([('rec_name', 'ilike', 'text%')]), 2) + self.assertEqual(Lines.search_count([('rec_name', 'ilike', 'text%')]), 3) + # search in category of split-line + self.assertEqual(Lines.search_count([('rec_name', '=', 'sp-cat1')]), 1) + # search in description of split-line + self.assertEqual(Lines.search_count([('rec_name', '=', 'text3-spline1')]), 1) + # ilike fails in fields.Text to find subtext... + self.assertEqual(Lines.search_count([('rec_name', 'ilike', '%spline%')]), 0) + # ...but it uses separator-chars + self.assertEqual(Lines.search_count([('rec_name', 'ilike', 'text3%')]), 1) + self.assertEqual(Lines.search_count([('rec_name', 'ilike', 'spline1')]), 1) + self.assertEqual(Lines.search_count([('rec_name', 'ilike', '%spline1')]), 1) + self.assertEqual(Lines.search_count([('rec_name', 'ilike', 'spline1%')]), 0) + self.assertEqual(Lines.search_count([('rec_name', 'ilike', 'text3')]), 1) - self.assertEqual(Lines.search_count([('state_cashbook', '=', 'open')]), 2) + self.assertEqual(Lines.search_count([('state_cashbook', '=', 'open')]), 3) self.assertEqual(Lines.search_count([('state_cashbook', '=', 'closed')]), 0) - self.assertEqual(Lines.search_count([('cashbook.state', '=', 'open')]), 2) + self.assertEqual(Lines.search_count([('cashbook.state', '=', 'open')]), 3) self.assertEqual(Lines.search_count([('cashbook.state', '=', 'closed')]), 0) # sorting: date -> state -> id - self.assertEqual(len(book.lines), 2) + self.assertEqual(len(book.lines), 3) self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') self.assertEqual(book.lines[0].state, 'edit') self.assertEqual(book.lines[1].rec_name, '05/02/2022|Rev|1.00 usd|Text 2 [Cat1]') self.assertEqual(book.lines[1].state, 'edit') + self.assertEqual(book.lines[2].rec_name, '05/03/2022|Rev/Sp|1.00 usd|Text 3 [-]') + self.assertEqual(book.lines[2].state, 'edit') # set to same date Lines.write(*[ @@ -439,6 +561,8 @@ class LineTestCase(ModuleTestCase): self.assertEqual(book.lines[0].state, 'edit') self.assertEqual(book.lines[1].rec_name, '05/01/2022|Rev|1.00 usd|Text 2 [Cat1]') self.assertEqual(book.lines[1].state, 'edit') + self.assertEqual(book.lines[2].rec_name, '05/01/2022|Rev/Sp|1.00 usd|Text 3 [-]') + self.assertEqual(book.lines[2].state, 'edit') # set to 'check', will sort first Lines.wfcheck([book.lines[1]]) @@ -447,6 +571,38 @@ class LineTestCase(ModuleTestCase): self.assertEqual(book.lines[0].state, 'check') self.assertEqual(book.lines[1].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') self.assertEqual(book.lines[1].state, 'edit') + self.assertEqual(book.lines[2].rec_name, '05/01/2022|Rev/Sp|1.00 usd|Text 3 [-]') + self.assertEqual(book.lines[2].state, 'edit') + + @with_transaction() + def test_line_to_non_type_book(self): + """ create cashbook w/o type + """ + pool = Pool() + Book = pool.get('cashbook.book') + Line = pool.get('cashbook.line') + + category = self.prep_category(cattype='in') + company = self.prep_company() + party = self.prep_party() + book, = Book.create([{ + 'name': 'Book 1', + 'btype': None, + 'company': company.id, + }]) + self.assertEqual(book.name, 'Book 1') + self.assertEqual(book.state, 'open') + + self.assertRaisesRegex(UserError, + 'The value for field "Cashbook" in "Cashbook Line" is not valid according to its domain.', + Line.create, + [{ + 'cashbook': book.id, + 'date': date(2022, 5, 1), + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('0.0'), + }]) @with_transaction() def test_line_create_check_deny_write(self): @@ -783,11 +939,25 @@ class LineTestCase(ModuleTestCase): 'bookingtype': 'mvout', 'amount': Decimal('1.0'), 'booktransf': book2.id, + }, { + 'date': date(2022, 6, 1), # in-category, return + 'description': 'in-return', # amount negative + 'category': category_in.id, + 'bookingtype': 'in', + 'amount': Decimal('-1.0'), + 'party': party.id, + }, { + 'date': date(2022, 6, 1), # out-category, return + 'description': 'out-return', # amount negative + 'category': category_out.id, + 'bookingtype': 'out', + 'amount': Decimal('-1.0'), + 'party': party.id, }])], }]) self.assertEqual(book.name, 'Book 1') self.assertEqual(book.state, 'open') - self.assertEqual(len(book.lines), 4) + self.assertEqual(len(book.lines), 6) self.assertEqual(book.lines[0].amount, Decimal('1.0')) self.assertEqual(book.lines[0].bookingtype, 'in') @@ -796,7 +966,7 @@ class LineTestCase(ModuleTestCase): # check payee self.assertEqual(book.lines[0].payee.rec_name, 'Party') - self.assertEqual(Line.search_count([('payee', 'ilike', 'party%')]), 2) + self.assertEqual(Line.search_count([('payee', 'ilike', 'party%')]), 4) self.assertEqual(Line.search_count([('payee', 'ilike', 'book%')]), 2) self.assertEqual(book.lines[1].amount, Decimal('1.0')) @@ -814,6 +984,16 @@ class LineTestCase(ModuleTestCase): self.assertEqual(book.lines[3].credit, Decimal('0.0')) self.assertEqual(book.lines[3].debit, Decimal('1.0')) + self.assertEqual(book.lines[4].amount, Decimal('-1.0')) + self.assertEqual(book.lines[4].bookingtype, 'in') + self.assertEqual(book.lines[4].credit, Decimal('-1.0')) + self.assertEqual(book.lines[4].debit, Decimal('0.0')) + + self.assertEqual(book.lines[5].amount, Decimal('-1.0')) + self.assertEqual(book.lines[5].bookingtype, 'out') + self.assertEqual(book.lines[5].credit, Decimal('0.0')) + self.assertEqual(book.lines[5].debit, Decimal('-1.0')) + Line.write(*[ [book.lines[0]], { diff --git a/tests/test_reconciliation.py b/tests/test_reconciliation.py index 10eacdb..98b77e4 100644 --- a/tests/test_reconciliation.py +++ b/tests/test_reconciliation.py @@ -49,14 +49,14 @@ class ReconTestCase(ModuleTestCase): 'date_to': date(2022, 6, 30), }])], }]) - self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') - self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[0].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[1].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') self.assertRaisesRegex(UserError, 'The date range overlaps with another reconciliation.', Reconciliation.write, *[ - [book.reconciliations[1]], + [book.reconciliations[0]], { 'date_from': date(2022, 4, 15), 'date_to': date(2022, 5, 2), @@ -97,14 +97,14 @@ class ReconTestCase(ModuleTestCase): 'date_to': date(2022, 6, 30), }])], }]) - self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') - self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[0].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[1].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') self.assertRaisesRegex(UserError, 'The date range overlaps with another reconciliation.', Reconciliation.write, *[ - [book.reconciliations[1]], + [book.reconciliations[0]], { 'date_from': date(2022, 5, 30), }, @@ -144,14 +144,14 @@ class ReconTestCase(ModuleTestCase): 'date_to': date(2022, 6, 30), }])], }]) - self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') - self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[0].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[1].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') self.assertRaisesRegex(UserError, 'The date range overlaps with another reconciliation.', Reconciliation.write, *[ - [book.reconciliations[1]], + [book.reconciliations[0]], { 'date_from': date(2022, 5, 5), 'date_to': date(2022, 5, 15), @@ -192,8 +192,8 @@ class ReconTestCase(ModuleTestCase): 'date_to': date(2022, 6, 30), }])], }]) - self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') - self.assertEqual(book.reconciliations[1].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[0].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[1].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') self.assertRaisesRegex(UserError, 'The date range overlaps with another reconciliation.', @@ -221,7 +221,6 @@ class ReconTestCase(ModuleTestCase): 'btype': types.id, 'company': company.id, 'currency': company.currency.id, - 'start_balance': Decimal('12.50'), 'start_date': date(2022, 5, 1), 'number_sequ': self.prep_sequence().id, 'reconciliations': [('create', [{ @@ -234,7 +233,7 @@ class ReconTestCase(ModuleTestCase): self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') Reconciliation.wfcheck(list(book.reconciliations)) - self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 12.50 usd - 12.50 usd [0]') + self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') @with_transaction() def test_recon_set_start_amount_by_predecessor(self): @@ -254,7 +253,6 @@ class ReconTestCase(ModuleTestCase): 'btype': types.id, 'company': company.id, 'currency': company.currency.id, - 'start_balance': Decimal('12.50'), 'start_date': date(2022, 5, 1), 'number_sequ': self.prep_sequence().id, 'reconciliations': [('create', [{ @@ -287,7 +285,7 @@ class ReconTestCase(ModuleTestCase): Reconciliation.wfcheck(list(book.reconciliations)) self.assertEqual(book.reconciliations[0].state, 'check') - self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 12.50 usd - 24.50 usd [2]') + self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 12.00 usd [2]') Reconciliation.wfdone(list(book.reconciliations)) self.assertEqual(book.reconciliations[0].state, 'done') @@ -298,7 +296,7 @@ class ReconTestCase(ModuleTestCase): }]) self.assertEqual(recons[0].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') Reconciliation.wfcheck(recons) - self.assertEqual(recons[0].rec_name, '05/31/2022 - 06/30/2022 | 24.50 usd - 24.50 usd [0]') + self.assertEqual(recons[0].rec_name, '05/31/2022 - 06/30/2022 | 12.00 usd - 12.00 usd [0]') @with_transaction() def test_recon_predecessor_done(self): @@ -536,9 +534,11 @@ class ReconTestCase(ModuleTestCase): self.assertEqual(len(book.reconciliations[0].lines), 1) self.assertEqual(book.reconciliations[0].lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') + self.assertEqual(book.lines[0].state, 'check') self.assertEqual(book.lines[1].rec_name, '06/01/2022|Rev|1.00 usd|Text 2 [Cat1]') + self.assertEqual(book.lines[1].state, 'edit') - # move 2nd line into date-range of checked-reconciliation, wf-check + # move 1st line into date-range of checked-reconciliation, wf-check Lines.write(*[ [book.lines[1]], { @@ -570,7 +570,9 @@ class ReconTestCase(ModuleTestCase): 'date_from': date(2022, 5, 31), 'date_to': date(2022, 6, 30), }]) - Reconciliation.wfdone([book.reconciliations[0]]) + self.assertEqual(book.reconciliations[0].rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(book.reconciliations[1].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 1.00 usd [1]') + Reconciliation.wfdone([book.reconciliations[1]]) Reconciliation.wfcheck([recon2]) Lines.write(*[ diff --git a/tests/test_splitline.py b/tests/test_splitline.py index f34eb21..c04d242 100644 --- a/tests/test_splitline.py +++ b/tests/test_splitline.py @@ -16,6 +16,96 @@ class SplitLineTestCase(ModuleTestCase): 'Test split line module' module = 'cashbook' + @with_transaction() + def test_splitline_category_and_transfer(self): + """ add book, line, two split-lines, + category + transfer + """ + pool = Pool() + Book = pool.get('cashbook.book') + Line = pool.get('cashbook.line') + + types = self.prep_type() + category1 = self.prep_category(cattype='in') + company = self.prep_company() + party = self.prep_party() + books = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Text 1', + 'category': category1.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + 'party': party.id, + }])], + }, { + 'name': 'Book 2', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + }]) + self.assertEqual(books[0].rec_name, 'Book 1 | 1.00 usd | Open') + self.assertEqual(len(books[0].lines), 1) + self.assertEqual(books[0].lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') + self.assertEqual(books[1].rec_name, 'Book 2 | 0.00 usd | Open') + + Book.write(*[ + [books[0]], + { + 'lines': [('write', [books[0].lines[0]], { + 'bookingtype': 'spin', + 'splitlines': [('create', [{ + 'amount': Decimal('5.0'), + 'splittype': 'cat', + 'description': 'from category', + 'category': category1.id, + }, { + 'amount': Decimal('6.0'), + 'splittype': 'tr', + 'description': 'from cashbook', + 'booktransf': books[1].id, + }])], + })] + }]) + self.assertEqual(len(books[0].lines), 1) + self.assertEqual(books[0].lines[0].rec_name, '05/01/2022|Rev/Sp|11.00 usd|Text 1 [-]') + self.assertEqual(books[0].lines[0].category, None) + self.assertEqual(len(books[0].lines[0].splitlines), 2) + self.assertEqual(books[0].lines[0].splitlines[0].rec_name, + 'Rev/Sp|5.00 usd|from category [Cat1]') + self.assertEqual(books[0].lines[0].splitlines[1].rec_name, + 'Rev/Sp|6.00 usd|from cashbook [Book 2 | 0.00 usd | Open]') + self.assertEqual(len(books[1].lines), 0) + + # wf: edit -> check + Line.wfcheck(books[0].lines) + self.assertEqual(len(books[0].lines), 1) + self.assertEqual(books[0].lines[0].state, 'check') + self.assertEqual(books[0].lines[0].number, '1') + self.assertEqual(len(books[0].lines[0].references), 1) + self.assertEqual(books[0].lines[0].references[0].rec_name, + '05/01/2022|to|-6.00 usd|from cashbook [Book 1 | 11.00 usd | Open]') + + self.assertEqual(len(books[1].lines), 1) + self.assertEqual(books[1].lines[0].reference.rec_name, + '05/01/2022|Rev/Sp|11.00 usd|Text 1 [-]') + self.assertEqual(books[1].lines[0].rec_name, + '05/01/2022|to|-6.00 usd|from cashbook [Book 1 | 11.00 usd | Open]') + + # wf: check --> edit + Line.wfedit(books[0].lines) + self.assertEqual(len(books[0].lines), 1) + self.assertEqual(len(books[0].lines[0].references), 0) + self.assertEqual(len(books[1].lines), 0) + @with_transaction() def test_splitline_check_clear_by_bookingtype(self): """ add book, line, category, set line to 'in', diff --git a/tryton.cfg b/tryton.cfg index dab4bf6..ed8ad8c 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=6.0.0 +version=6.0.11 depends: res currency @@ -19,4 +19,5 @@ xml: splitline.xml wizard_openline.xml wizard_runreport.xml + wizard_booking.xml menu.xml diff --git a/view/book_form.xml b/view/book_form.xml index 9689b01..d2ffaad 100644 --- a/view/book_form.xml +++ b/view/book_form.xml @@ -2,10 +2,10 @@ -
-