diff --git a/line.py b/line.py index dbc79cb..a8ccda4 100644 --- a/line.py +++ b/line.py @@ -81,8 +81,15 @@ class Line(Workflow, ModelSQL, ModelView): bookingtype = fields.Selection(string='Type', required=True, help='Type of Booking', selection=sel_bookingtype, states=STATES, depends=DEPENDS) + bookingtype_string = bookingtype.translated('bookingtype') amount = fields.Numeric(string='Amount', digits=(16, Eval('currency_digits', 2)), - required=True, states=STATES, depends=DEPENDS+['currency_digits']) + required=True, + states={ + 'readonly': Or( + STATES['readonly'], + Eval('bookingtype', '').in_(['spin', 'spout']), + ), + }, depends=DEPENDS+['currency_digits', 'bookingtype']) debit = fields.Numeric(string='Debit', digits=(16, Eval('currency_digits', 2)), required=True, readonly=True, depends=['currency_digits']) credit = fields.Numeric(string='Credit', digits=(16, Eval('currency_digits', 2)), @@ -123,10 +130,15 @@ class Line(Workflow, ModelSQL, ModelView): }, field='reference', readonly=True) splitlines = fields.One2Many(string='Split booking lines', model_name='cashbook.split', - help='The rows are created and managed by the current record.', + help='Rows with different categories form the total sum of the booking', states={ - 'invisible': ~Bool(Eval('splitlines')), - }, field='line', readonly=True) + 'invisible': ~Eval('bookingtype' '').in_(['spin', 'spout']), + 'readonly': Or( + ~Eval('bookingtype' '').in_(['spin', 'spout']), + STATES['readonly'], + ), + 'required': Eval('bookingtype' '').in_(['spin', 'spout']), + }, field='line', depends=DEPENDS+['bookingtype']) reconciliation = fields.Many2One(string='Reconciliation', readonly=True, model_name='cashbook.recon', ondelete='SET NULL', @@ -143,9 +155,9 @@ class Line(Workflow, ModelSQL, ModelView): 'on_change_with_balance') currency = fields.Function(fields.Many2One(model_name='currency.currency', - string="Currency"), 'on_change_with_currency') - currency_digits = fields.Function(fields.Integer(string='Currency Digits'), - 'on_change_with_currency_digits') + string="Currency", readonly=True), 'on_change_with_currency') + 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, select=True, selection=sel_linetype) @@ -351,14 +363,14 @@ class Line(Workflow, ModelSQL, ModelView): 'type': gettext('cashbook.msg_line_bookingtype_%s' % self.bookingtype), } - def get_amount_by_second_currency(self, to_currency): + def get_amount_by_second_currency(self, to_currency, amount=None): """ get amount, calculate credit/debit from currency of current cashbook to 'to_currency' """ Currency = Pool().get('currency.currency') values = { - 'amount': self.amount, + 'amount': amount if amount is not None else self.amount, } if to_currency.id != self.cashbook.currency.id: @@ -402,18 +414,6 @@ class Line(Workflow, ModelSQL, ModelView): return [tab2] - @fields.depends('party', 'booktransf', 'bookingtype') - def on_change_with_payee(self, name=None): - """ get party or cashbook - """ - if self.bookingtype: - if self.bookingtype in ['in', 'out', 'spin', 'spout']: - if self.party: - return 'party.party,%d' % self.party.id - elif self.bookingtype in ['mvin', 'mvout']: - if self.booktransf: - return 'cashbook.book,%d' % self.booktransf.id - @classmethod def search_payee(cls, names, clause): """ search in payee for party or cashbook @@ -423,36 +423,12 @@ class Line(Workflow, ModelSQL, ModelView): ('booktransf.rec_name',) + tuple(clause[1:]), ] - @fields.depends('category') - def on_change_with_category_view(self, name=None): - """ show optimizef form of category for list-view - """ - Configuration = Pool().get('cashbook.configuration') - - if self.category: - cfg1 = Configuration.get_singleton() - - if getattr(cfg1, 'catnamelong', True) == True: - return self.category.rec_name - else : - return self.category.name - @classmethod def search_category_view(cls, name, clause): """ search in category """ return [('category.rec_name',) + tuple(clause[1:])] - @fields.depends('date') - def on_change_with_month(self, name=None): - """ get difference of month to current date - """ - IrDate = Pool().get('ir.date') - if self.date is not None: - dt1 = IrDate.today() - return (12 * dt1.year + dt1.month) - \ - (12 * self.date.year + self.date.month) - @classmethod def search_month(cls, names, clause): """ search in month @@ -472,6 +448,75 @@ class Line(Workflow, ModelSQL, ModelView): ) return [('id', 'in', query)] + @classmethod + def search_state_cashbook(cls, names, clause): + """ search in state of cashbook + """ + return [('cashbook.state',) + tuple(clause[1:])] + + @fields.depends('amount', 'splitlines') + def on_change_splitlines(self): + """ update amount if splitlines change + """ + self.amount = sum([x.amount for x in self.splitlines if x.amount is not None]) + + @fields.depends('bookingtype', 'category', 'splitlines') + def on_change_bookingtype(self): + """ clear category if not valid type + """ + types = { + 'in': ['in', 'mvin', 'spin'], + 'out': ['out', 'mvout', 'spout'], + } + + if self.bookingtype: + if self.category: + if not self.bookingtype in types.get(self.category.cattype, ''): + self.category = None + + if self.bookingtype in ['spin', 'spout']: + for spline in self.splitlines: + if not self.bookingtype in types.get(getattr(spline.category, 'cattype', '-'), ''): + spline.category = None + else : + self.splitlines = [] + + @fields.depends('party', 'booktransf', 'bookingtype') + def on_change_with_payee(self, name=None): + """ get party or cashbook + """ + if self.bookingtype: + if self.bookingtype in ['in', 'out', 'spin', 'spout']: + if self.party: + return 'party.party,%d' % self.party.id + elif self.bookingtype in ['mvin', 'mvout']: + 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 + """ + Configuration = Pool().get('cashbook.configuration') + + if self.category: + cfg1 = Configuration.get_singleton() + + if getattr(cfg1, 'catnamelong', True) == True: + return self.category.rec_name + else : + return self.category.name + + @fields.depends('date') + def on_change_with_month(self, name=None): + """ get difference of month to current date + """ + IrDate = Pool().get('ir.date') + if self.date is not None: + dt1 = IrDate.today() + return (12 * dt1.year + dt1.month) - \ + (12 * self.date.year + self.date.month) + @fields.depends('cashbook', '_parent_cashbook.owner') def on_change_with_owner_cashbook(self, name=None): """ get current owner @@ -486,26 +531,6 @@ class Line(Workflow, ModelSQL, ModelView): if self.cashbook: return self.cashbook.state - @classmethod - def search_state_cashbook(cls, names, clause): - """ search in state of cashbook - """ - return [('cashbook.state',) + tuple(clause[1:])] - - @fields.depends('bookingtype', 'category') - def on_change_bookingtype(self): - """ clear category if not valid type - """ - types = { - 'in': ['in', 'mvin', 'spin'], - 'out': ['out', 'mvout', 'spout'], - } - - if self.bookingtype: - if self.category: - if not self.bookingtype in types.get(self.category.cattype, ''): - self.category = None - @fields.depends('cashbook', '_parent_cashbook.currency') def on_change_with_currency(self, name=None): """ currency of cashbook @@ -566,6 +591,31 @@ class Line(Workflow, ModelSQL, ModelView): break return balance + @classmethod + def clear_by_bookingtype(cls, values, line=None): + """ clear some fields by value of bookingtype + """ + values2 = {} + values2.update(values) + + bookingtype = values2.get('bookingtype', getattr(line, 'bookingtype', None)) + if (bookingtype in ['in', 'out', 'mvin', 'mvout']) and \ + ('splitlines' not in values2.keys()): + if line: + if len(line.splitlines) > 0: + values2['splitlines'] = [('delete', [x.id for x in line.splitlines])] + + if bookingtype in ['in', 'out']: + values2['booktransf'] = None + + if bookingtype in ['spin', 'spout']: + values2['category'] = None + values2['booktransf'] = None + + if bookingtype in ['mvin', 'mvout']: + values2['category'] = None + return values2 + @classmethod def get_debit_credit(cls, values): """ compute debit/credit from amount @@ -593,11 +643,33 @@ class Line(Workflow, ModelSQL, ModelView): raise ValueError('invalid "bookingtype"') return {} + @classmethod + def update_amount_by_splitlines(cls, lines): + """ update amounts from split-lines + """ + Line2 = Pool().get('cashbook.line') + + to_write = [] + for line in lines: + to_write.extend([ + [line], + { + 'amount': sum([x.amount for x in line.splitlines]), + }]) + if len(to_write) > 0: + Line2.write(*to_write) + @classmethod def validate(cls, lines): """ deny date before 'start_date' of cashbook """ super(Line, cls).validate(lines) + + types = { + 'in': ['in', 'mvin', 'spin'], + 'out': ['out', 'mvout', 'spout'], + } + for line in lines: if line.date < line.cashbook.start_date: raise UserError(gettext( @@ -606,6 +678,77 @@ class Line(Workflow, ModelSQL, ModelView): recname = line.rec_name, )) + # line: category <--> bookingtype? + if line.category: + if not line.bookingtype in types[line.category.cattype]: + raise UserError(gettext( + 'cashbook.msg_line_invalid_category', + recname = line.rec_name, + booktype = line.bookingtype_string, + )) + + # splitline: category <--> bookingtype? + for spline in line.splitlines: + if not line.bookingtype in types[spline.category.cattype]: + raise UserError(gettext( + 'cashbook.msg_line_split_invalid_category', + recname = line.rec_name, + splitrecname = spline.rec_name, + booktype = line.bookingtype_string, + )) + + @classmethod + def check_permission_write(cls, lines, values={}): + """ deny update if cashbook.line!='open', + """ + for line in lines: + # deny write if cashbook is not open + if line.cashbook.state != 'open': + raise UserError(gettext( + 'cashbook.msg_book_deny_write', + bookname = line.cashbook.rec_name, + state_txt = line.cashbook.state_string, + )) + + # deny write if reconciliation is 'check' or 'done' + if line.reconciliation: + if line.reconciliation.state == 'done': + raise UserError(gettext( + 'cashbook.msg_line_deny_write_by_reconciliation', + recname = line.rec_name, + reconame = line.reconciliation.rec_name, + )) + + # 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) \ + and (len(values.keys()) == 1)): + raise UserError(gettext( + 'cashbook.msg_line_deny_write', + recname = line.rec_name, + state_txt = line.state_string, + )) + + @classmethod + def check_permission_delete(cls, lines): + """ deny delete if book is not 'open' or wf is not 'edit' + """ + for line in lines: + if line.cashbook.state == 'closed': + raise UserError(gettext( + 'cashbook.msg_line_deny_delete1', + linetxt = line.rec_name, + bookname = line.cashbook.rec_name, + bookstate = line.cashbook.state_string, + )) + if line.state != 'edit': + raise UserError(gettext( + 'cashbook.msg_line_deny_delete2', + linetxt = line.rec_name, + linestate = line.state_string, + )) + @classmethod def copy(cls, lines, default=None): """ reset values @@ -625,6 +768,7 @@ class Line(Workflow, ModelSQL, ModelView): vlist = [x.copy() for x in vlist] for values in vlist: values.update(cls.get_debit_credit(values)) + values.update(cls.clear_by_bookingtype(values)) # deny add to reconciliation if state is not 'check' or 'done' if values.get('reconciliation', None): @@ -650,14 +794,9 @@ class Line(Workflow, ModelSQL, ModelView): actions = iter(args) to_write = [] for lines, values in zip(actions, actions): + cls.check_permission_write(lines, values) + for line in lines: - # deny write if cashbook is not open - if line.cashbook.state != 'open': - raise UserError(gettext( - 'cashbook.msg_book_deny_write', - bookname = line.cashbook.rec_name, - state_txt = line.cashbook.state_string, - )) if line.reconciliation: # deny state-change to 'edit' if line is linked to reconciliation if values.get('state', '-') == 'edit': @@ -666,24 +805,6 @@ class Line(Workflow, ModelSQL, ModelView): recname = line.rec_name, )) - # deny write if reconciliation is 'check' or 'done' - if line.reconciliation.state == 'done': - raise UserError(gettext( - 'cashbook.msg_line_deny_write_by_reconciliation', - recname = line.rec_name, - reconame = line.reconciliation.rec_name, - )) - # 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) \ - and (len(values.keys()) == 1)): - raise UserError(gettext( - 'cashbook.msg_line_deny_write', - recname = line.rec_name, - state_txt = line.state_string, - )) - # deny add to reconciliation if state is not 'check' or 'done' if values.get('reconciliation', None): for line in lines: @@ -698,6 +819,7 @@ class Line(Workflow, ModelSQL, ModelView): for line in lines: values2 = {} values2.update(values) + values2.update(cls.clear_by_bookingtype(values, line)) values2.update(cls.get_debit_credit({ x:values.get(x, getattr(line, x)) for x in ['amount', 'bookingtype'] })) @@ -710,21 +832,7 @@ class Line(Workflow, ModelSQL, ModelView): def delete(cls, lines): """ deny delete if book is not 'open' or wf is not 'edit' """ - for line in lines: - if line.cashbook.state == 'closed': - raise UserError(gettext( - 'cashbook.msg_line_deny_delete1', - linetxt = line.rec_name, - bookname = line.cashbook.rec_name, - bookstate = line.cashbook.state_string, - )) - if line.state != 'edit': - raise UserError(gettext( - 'cashbook.msg_line_deny_delete2', - linetxt = line.rec_name, - linestate = line.state_string, - )) - + cls.check_permission_delete(lines) return super(Line, cls).delete(lines) # end Line diff --git a/locale/de.po b/locale/de.po index e2ca1a9..073d916 100644 --- a/locale/de.po +++ b/locale/de.po @@ -142,6 +142,14 @@ msgctxt "model:ir.message,text:msg_line_date_before_book" msgid "The date of the cashbook line '%(recname)s' cannot be earlier than the start date '%(datebook)s' of the cashbook." msgstr "Das Datum der Kassenbuchzeile '%(recname)s' kann nicht vor dem Anfangsdatum '%(datebook)s' des Kassenbuchs liegen." +msgctxt "model:ir.message,text:msg_line_split_invalid_category" +msgid "The category of the split booking line '%(splitrecname)s' does not match the posting type '%(booktype)s' of the line '%(recname)s'." +msgstr "Die Kategorie der Splitbuchungszeile '%(splitrecname)s' paßt nicht zum Buchungstyp '%(booktype)s' der Zeile '%(recname)s'." + +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'." + ############# # res.group # @@ -482,6 +490,10 @@ msgctxt "model:cashbook.split,name:" msgid "Split booking line" msgstr "Splitbuchungszeile" +msgctxt "view:cashbook.split:" +msgid "Description" +msgstr "Beschreibung" + msgctxt "field:cashbook.split,line:" msgid "Line" msgstr "Zeile" @@ -494,6 +506,10 @@ msgctxt "field:cashbook.split,category:" msgid "Category" msgstr "Kategorie" +msgctxt "field:cashbook.split,category_view:" +msgid "Category" +msgstr "Kategorie" + msgctxt "field:cashbook.split,amount:" msgid "Amount" msgstr "Betrag" @@ -582,6 +598,14 @@ msgctxt "view:cashbook.line:" msgid "State" msgstr "Status" +msgctxt "view:cashbook.line:" +msgid "Split booking lines" +msgstr "Splitbuchungszeilen" + +msgctxt "view:cashbook.line:" +msgid "References" +msgstr "Referenzen" + msgctxt "field:cashbook.line,cashbook:" msgid "Cashbook" msgstr "Kassenbuch" @@ -734,6 +758,14 @@ msgctxt "field:cashbook.line,number:" msgid "Number" msgstr "Nummer" +msgctxt "field:cashbook.line,splitlines:" +msgid "Split booking lines" +msgstr "Splitbuchungszeilen" + +msgctxt "help:cashbook.line,splitlines:" +msgid "Rows with different categories form the total sum of the booking" +msgstr "Zeilen mit unterschiedlichen Kategorien bilden die Gesamtsumme der Buchung" + ################# # cashbook.type # diff --git a/locale/en.po b/locale/en.po index f2ff648..c6f7965 100644 --- a/locale/en.po +++ b/locale/en.po @@ -106,10 +106,18 @@ msgctxt "model:ir.message,text:msg_line_bookingtype_in" msgid "Rev" msgstr "Rev" +msgctxt "model:ir.message,text:msg_line_bookingtype_spin" +msgid "Rev/Sp" +msgstr "Rev/Sp" + msgctxt "model:ir.message,text:msg_line_bookingtype_out" msgid "Exp" msgstr "Exp" +msgctxt "model:ir.message,text:msg_line_bookingtype_spout" +msgid "Exp/Sp" +msgstr "Exp/Sp" + msgctxt "model:ir.message,text:msg_line_bookingtype_mvin" msgid "from" msgstr "from" @@ -130,6 +138,14 @@ msgctxt "model:ir.message,text:msg_line_date_before_book" msgid "The date of the cashbook line '%(recname)s' cannot be earlier than the start date '%(datebook)s' of the cashbook." msgstr "The date of the cashbook line '%(recname)s' cannot be earlier than the start date '%(datebook)s' of the cashbook." +msgctxt "model:ir.message,text:msg_line_split_invalid_category" +msgid "The category of the split booking line '%(splitrecname)s' does not match the posting type '%(booktype)s' of the line '%(recname)s'." +msgstr "The category of the split booking line '%(splitrecname)s' does not match the posting type '%(booktype)s' of the line '%(recname)s'." + +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:res.group,name:group_cashbook" msgid "Cashbook" msgstr "Cashbook" @@ -154,6 +170,10 @@ msgctxt "model:ir.rule.group,name:rg_book_read_nonowner" msgid "Observers and Reviewers: Cashbook read" msgstr "Observers and Reviewers: Cashbook read" +msgctxt "model:ir.rule.group,name:rg_split_write_adm" +msgid "Administrators: Splitbooking line read/write" +msgstr "Administrators: Splitbooking line read/write" + msgctxt "model:ir.rule.group,name:rg_book_write_adm" msgid "Administrators: Cashbook read/write" msgstr "Administrators: Cashbook read/write" @@ -162,10 +182,18 @@ msgctxt "model:ir.rule.group,name:rg_line_write_adm" msgid "Administrators: Cashbook line read/write" msgstr "Administrators: Cashbook line read/write" +msgctxt "model:ir.rule.group,name:rg_split_write" +msgid "Owners and reviewers: Splitbooking line write" +msgstr "Owners and reviewers: Splitbooking line write" + msgctxt "model:ir.rule.group,name:rg_line_write" msgid "Owners and reviewers: Cashbook line write" msgstr "Owners and reviewers: Cashbook line write" +msgctxt "model:ir.rule.group,name:rg_split_read" +msgid "Observer: Splitbooking line read" +msgstr "Observer: Splitbooking line read" + msgctxt "model:ir.rule.group,name:rg_line_read" msgid "Observer: Cashbook line read" msgstr "Observer: Cashbook line read" @@ -174,6 +202,10 @@ msgctxt "model:ir.rule.group,name:rg_line_read" msgid "User in companies" msgstr "User in companies" +msgctxt "model:ir.rule.group,name:rg_split_companies" +msgid "User in companies" +msgstr "User in companies" + msgctxt "model:ir.rule.group,name:rg_type_companies" msgid "User in companies" msgstr "User in companies" @@ -414,6 +446,90 @@ 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 "model:cashbook.split,name:" +msgid "Split booking line" +msgstr "Split booking line" + +msgctxt "view:cashbook.split:" +msgid "Description" +msgstr "Description" + +msgctxt "field:cashbook.split,line:" +msgid "Line" +msgstr "Line" + +msgctxt "field:cashbook.split,description:" +msgid "Description" +msgstr "Description" + +msgctxt "field:cashbook.split,category:" +msgid "Category" +msgstr "Category" + +msgctxt "field:cashbook.split,category_view:" +msgid "Category" +msgstr "Category" + +msgctxt "field:cashbook.split,amount:" +msgid "Amount" +msgstr "Amount" + +msgctxt "field:cashbook.split,currency:" +msgid "Currency" +msgstr "Currency" + +msgctxt "field:cashbook.split,currency_digits:" +msgid "Currency Digits" +msgstr "Currency Digits" + +msgctxt "field:cashbook.split,bookingtype:" +msgid "Type" +msgstr "Type" + +msgctxt "selection:cashbook.split,bookingtype:" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "selection:cashbook.split,bookingtype:" +msgid "Revenue Splitbooking" +msgstr "Revenue Splitbooking" + +msgctxt "selection:cashbook.split,bookingtype:" +msgid "Expense" +msgstr "Expense" + +msgctxt "selection:cashbook.split,bookingtype:" +msgid "Expense Splitbooking" +msgstr "Expense Splitbooking" + +msgctxt "selection:cashbook.split,bookingtype:" +msgid "Transfer from" +msgstr "Transfer from" + +msgctxt "selection:cashbook.split,bookingtype:" +msgid "Transfer to" +msgstr "Transfer to" + +msgctxt "field:cashbook.split,state:" +msgid "State" +msgstr "State" + +msgctxt "selection:cashbook.split,state:" +msgid "Edit" +msgstr "Edit" + +msgctxt "selection:cashbook.split,state:" +msgid "Checked" +msgstr "Checked" + +msgctxt "selection:cashbook.split,state:" +msgid "Done" +msgstr "Done" + +msgctxt "field:cashbook.split,state_cashbook:" +msgid "State of Cashbook" +msgstr "State of Cashbook" + msgctxt "model:cashbook.line,name:" msgid "Cashbook Line" msgstr "Cashbook Line" @@ -438,6 +554,14 @@ msgctxt "view:cashbook.line:" msgid "State" msgstr "State" +msgctxt "view:cashbook.line:" +msgid "Split booking lines" +msgstr "Split booking lines" + +msgctxt "view:cashbook.line:" +msgid "References" +msgstr "References" + msgctxt "field:cashbook.line,cashbook:" msgid "Cashbook" msgstr "Cashbook" @@ -490,10 +614,18 @@ msgctxt "selection:cashbook.line,bookingtype:" msgid "Revenue" msgstr "Revenue" +msgctxt "selection:cashbook.line,bookingtype:" +msgid "Revenue Splitbooking" +msgstr "Revenue Splitbooking" + msgctxt "selection:cashbook.line,bookingtype:" msgid "Expense" msgstr "Expense" +msgctxt "selection:cashbook.line,bookingtype:" +msgid "Expense Splitbooking" +msgstr "Expense Splitbooking" + msgctxt "selection:cashbook.line,bookingtype:" msgid "Transfer from" msgstr "Transfer from" @@ -582,6 +714,14 @@ msgctxt "field:cashbook.line,number:" msgid "Number" msgstr "Number" +msgctxt "field:cashbook.line,splitlines:" +msgid "Split booking lines" +msgstr "Split booking lines" + +msgctxt "help:cashbook.line,splitlines:" +msgid "Rows with different categories form the total sum of the booking" +msgstr "Rows with different categories form the total sum of the booking" + msgctxt "model:cashbook.type,name:" msgid "Cashbook Type" msgstr "Cashbook Type" diff --git a/message.xml b/message.xml index 9cea207..8fea230 100644 --- a/message.xml +++ b/message.xml @@ -107,6 +107,12 @@ full copyright notices and license terms. --> The date of the cashbook line '%(recname)s' cannot be earlier than the start date '%(datebook)s' of the cashbook. + + The category of the split booking line '%(splitrecname)s' does not match the posting type '%(booktype)s' of the line '%(recname)s'. + + + The category of the booking line '%(recname)s' does not match the posting type '%(booktype)s'. + diff --git a/splitline.py b/splitline.py index a6571dc..0c691ad 100644 --- a/splitline.py +++ b/splitline.py @@ -7,6 +7,9 @@ from trytond.model import ModelView, ModelSQL, Workflow, fields, Check from trytond.pool import Pool from trytond.pyson import Eval, If +from trytond.report import Report +from trytond.i18n import gettext +from trytond.transaction import Transaction from .line import sel_linetype, sel_bookingtype, STATES, DEPENDS from .book import sel_state_book @@ -22,28 +25,74 @@ class SplitLine(ModelSQL, ModelView): states=STATES, depends=DEPENDS) category = fields.Many2One(string='Category', model_name='cashbook.category', ondelete='RESTRICT', - states=STATES, depends=DEPENDS+['bookingtype'], - required=True, + states=STATES, depends=DEPENDS+['bookingtype'], required=True, domain=[ If( - Eval('bookingtype', '').in_(['in', 'mvin']), + Eval('bookingtype', '') == 'spin', ('cattype', '=', 'in'), ('cattype', '=', 'out'), )]) + category_view = fields.Function(fields.Char(string='Category', readonly=True), + 'on_change_with_category_view') amount = fields.Numeric(string='Amount', digits=(16, Eval('currency_digits', 2)), required=True, states=STATES, depends=DEPENDS+['currency_digits']) currency = fields.Function(fields.Many2One(model_name='currency.currency', - string="Currency"), 'on_change_with_currency') - currency_digits = fields.Function(fields.Integer(string='Currency Digits'), - 'on_change_with_currency_digits') + string="Currency", readonly=True), 'on_change_with_currency') + currency_digits = fields.Function(fields.Integer(string='Currency Digits', + readonly=True), 'on_change_with_currency_digits') bookingtype = fields.Function(fields.Selection(string='Type', readonly=True, selection=sel_bookingtype), 'on_change_with_bookingtype') state = fields.Function(fields.Selection(string='State', readonly=True, selection=sel_linetype), 'on_change_with_state') state_cashbook = fields.Function(fields.Selection(string='State of Cashbook', readonly=True, states={'invisible': True}, selection=sel_state_book), - 'on_change_with_state_cashbook', searcher='search_state_cashbook') + 'on_change_with_state_cashbook') + + def get_rec_name(self, name): + """ short + name + """ + return '%(type)s|%(amount)s %(symbol)s|%(desc)s [%(category)s]' % { + 'desc': (self.description or '-')[:40], + 'amount': Report.format_number(self.amount, None), + 'symbol': getattr(self.currency, 'symbol', '-'), + 'category': self.category_view, + 'type': gettext('cashbook.msg_line_bookingtype_%s' % self.line.bookingtype), + } + + def get_amount_by_second_currency(self, to_currency): + """ get amount, calculate credit/debit from currency of current + cashbook to 'to_currency' + """ + Currency = Pool().get('currency.currency') + + values = { + 'amount': self.amount, + } + + if to_currency.id != self.line.cashbook.currency.id: + with Transaction().set_context({ + 'date': self.line.date, + }): + values['amount'] = Currency.compute( + self.line.cashbook.currency, + self.amount, + to_currency) + return values + + @fields.depends('category') + def on_change_with_category_view(self, name=None): + """ show optimizef form of category for list-view + """ + Configuration = Pool().get('cashbook.configuration') + + if self.category: + cfg1 = Configuration.get_singleton() + + if getattr(cfg1, 'catnamelong', True) == True: + return self.category.rec_name + else : + return self.category.name @fields.depends('line', '_parent_line.state') def on_change_with_state(self, name=None): @@ -82,4 +131,51 @@ class SplitLine(ModelSQL, ModelView): else: return 2 + @classmethod + def create(cls, vlist): + """ add debit/credit + """ + Line2 = Pool().get('cashbook.line') + + vlist = [x.copy() for x in vlist] + records = super(SplitLine, cls).create(vlist) + + to_update_line = [] + for record in records: + if not record.line in to_update_line: + to_update_line.append(record.line) + + if len(to_update_line) > 0: + Line2.update_amount_by_splitlines(to_update_line) + + @classmethod + def write(cls, *args): + """ deny update if cashbook.line!='open', + add or update debit/credit + """ + Line2 = Pool().get('cashbook.line') + + actions = iter(args) + to_update_line = [] + for records, values in zip(actions, actions): + Line2.check_permission_write([x.line for x in records]) + + if 'amount' in values.keys(): + for record in records: + if not record.line in to_update_line: + to_update_line.append(record.line) + super(SplitLine, cls).write(*args) + + if len(to_update_line) > 0: + Line2.update_amount_by_splitlines(to_update_line) + + @classmethod + def delete(cls, splitlines): + """ deny delete if book is not 'open' or wf is not 'edit' + """ + Line2 = Pool().get('cashbook.line') + + Line2.check_permission_delete([x.line for x in splitlines]) + return super(SplitLine, cls).delete(splitlines) + # end SplitLine diff --git a/tests/__init__.py b/tests/__init__.py index 8ecdac3..f557851 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,7 @@ import unittest from trytond.modules.cashbook.tests.test_type import TypeTestCase from trytond.modules.cashbook.tests.test_book import BookTestCase from trytond.modules.cashbook.tests.test_line import LineTestCase +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 @@ -19,6 +20,7 @@ class CashbookTestCase(\ CategoryTestCase,\ ConfigTestCase,\ LineTestCase, + SplitLineTestCase, BookTestCase, TypeTestCase, ): diff --git a/tests/test_splitline.py b/tests/test_splitline.py new file mode 100644 index 0000000..f34eb21 --- /dev/null +++ b/tests/test_splitline.py @@ -0,0 +1,109 @@ +# -*- 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 unittest.mock import MagicMock +from decimal import Decimal + + +class SplitLineTestCase(ModuleTestCase): + 'Test split line module' + module = 'cashbook' + + @with_transaction() + def test_splitline_check_clear_by_bookingtype(self): + """ add book, line, category, set line to 'in', + then update to 'spin' + """ + pool = Pool() + Book = pool.get('cashbook.book') + Lines = pool.get('cashbook.line') + + types = self.prep_type() + category1 = self.prep_category(cattype='in') + category2 = self.prep_category(name='Cat2', 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': 'Text 1', + 'category': category1.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + 'party': party.id, + }])], + }]) + + self.assertEqual(len(book.lines), 1) + self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|1.00 usd|Text 1 [Cat1]') + self.assertEqual(book.lines[0].amount, Decimal('1.0')) + self.assertEqual(book.lines[0].category.rec_name, 'Cat1') + + Lines.write(*[ + [book.lines[0]], + { + 'bookingtype': 'spin', + 'splitlines': [('create', [{ + 'amount': Decimal('5.0'), + 'category': category1.id, + 'description': 'line 1' + }, { + 'amount': Decimal('2.0'), + 'category': category2.id, + 'description': 'line 2', + }])], + }]) + + self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev/Sp|7.00 usd|Text 1 [-]') + self.assertEqual(book.lines[0].amount, Decimal('7.0')) + self.assertEqual(book.lines[0].category, None) + + self.assertEqual(len(book.lines[0].splitlines), 2) + self.assertEqual(book.lines[0].splitlines[0].amount, Decimal('5.0')) + self.assertEqual(book.lines[0].splitlines[0].category.rec_name, 'Cat1') + self.assertEqual(book.lines[0].splitlines[0].description, 'line 1') + self.assertEqual(book.lines[0].splitlines[0].rec_name, 'Rev/Sp|5.00 usd|line 1 [Cat1]') + + self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('2.0')) + self.assertEqual(book.lines[0].splitlines[1].category.rec_name, 'Cat2') + self.assertEqual(book.lines[0].splitlines[1].description, 'line 2') + self.assertEqual(book.lines[0].splitlines[1].rec_name, 'Rev/Sp|2.00 usd|line 2 [Cat2]') + + Lines.write(*[ + [book.lines[0]], + { + 'splitlines': [('write', + [book.lines[0].splitlines[0]], + { + 'amount': Decimal('3.5'), + })], + }]) + self.assertEqual(book.lines[0].splitlines[0].amount, Decimal('3.5')) + self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('2.0')) + self.assertEqual(book.lines[0].amount, Decimal('5.5')) + + Lines.write(*[ + [book.lines[0]], + { + 'bookingtype': 'in', + 'amount': Decimal('7.5'), + 'category': category2.id, + }]) + self.assertEqual(book.lines[0].rec_name, '05/01/2022|Rev|7.50 usd|Text 1 [Cat2]') + self.assertEqual(book.lines[0].category.rec_name, 'Cat2') + self.assertEqual(len(book.lines[0].splitlines), 0) + +# end SplitLineTestCase diff --git a/view/line_form.xml b/view/line_form.xml index c0b553b..acae914 100644 --- a/view/line_form.xml +++ b/view/line_form.xml @@ -34,20 +34,21 @@ full copyright notices and license terms. -->