From 149baef174e244253a1cd351092eec7dc36b277b Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Fri, 12 Aug 2022 16:43:49 +0200 Subject: [PATCH] =?UTF-8?q?line:=20=C3=A4nderungesperre=20bei=20diversen?= =?UTF-8?q?=20wf-zust=C3=A4nden=20+=20test,=20abstimmung:=20datum/betrag?= =?UTF-8?q?=20anfang/ende=20korrekt=20+=20test=20f=C3=BCr=20betr=C3=A4ge?= =?UTF-8?q?=20mu=C3=9F=20noch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- line.py | 65 ++++++- locale/de.po | 40 ++++ locale/en.po | 362 +++++++++++++++++++++++++++++++--- message.xml | 24 +++ reconciliation.py | 243 +++++++++++++++++++++-- tests/__init__.py | 2 + tests/test_line.py | 54 ++++-- tests/test_reconciliation.py | 365 +++++++++++++++++++++++++++++++++++ view/recon_form.xml | 6 +- view/recon_list.xml | 2 + 10 files changed, 1099 insertions(+), 64 deletions(-) create mode 100644 tests/test_reconciliation.py diff --git a/line.py b/line.py index b95e4ae..31e399f 100644 --- a/line.py +++ b/line.py @@ -78,7 +78,9 @@ class Line(Workflow, ModelSQL, ModelView): credit = fields.Numeric(string='Credit', digits=(16, Eval('currency_digits', 2)), required=True, readonly=True, depends=['currency_digits']) reconciliation = fields.Many2One(string='Reconciliation', readonly=True, - model_name='cashbook.recon', ondelete='SET NULL') + model_name='cashbook.recon', ondelete='SET NULL', + domain=[('cashbook.id', '=', Eval('cashbook'))], + depends=['cashbook']) balance = fields.Function(fields.Numeric(string='Balance', digits=(16, Eval('currency_digits', 2)), @@ -184,9 +186,12 @@ class Line(Workflow, ModelSQL, ModelView): def get_rec_name(self, name): """ short + name """ - return '%(date)s %(desc)s' % { + return '%(date)s|%(amount)s %(symbol)s|%(desc)s [%(category)s]' % { 'date': Report.format_date(self.date), 'desc': (self.description or '-')[:40], + 'amount': Report.format_number(self.amount or 0.0, None), + 'symbol': getattr(self.currency, 'symbol', '-'), + 'category': self.category_view, } @staticmethod @@ -360,8 +365,23 @@ class Line(Workflow, ModelSQL, ModelView): """ add debit/credit """ vlist = [x.copy() for x in vlist] - for vals in vlist: - vals.update(cls.get_debit_credit(vals)) + for values in vlist: + values.update(cls.get_debit_credit(values)) + + # deny add to reconciliation if state is not 'check' or 'done' + if values.get('reconciliation', None): + if not values.get('state', '-') in ['check', 'done']: + date_txt = '-' + if values.get('date', None): + date_txt = Report.format_date(values.get('date', None)) + raise UserError(gettext( + 'cashbook.msg_line_deny_recon_by_state', + recname = '%(date)s|%(descr)s' % { + 'date': date_txt, + 'descr': values.get('description', '-'), + }, + )) + return super(Line, cls).create(vlist) @classmethod @@ -373,14 +393,49 @@ class Line(Workflow, ModelSQL, ModelView): to_write = [] for lines, values in zip(actions, actions): for line in lines: + # deny write if chashbook 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': + raise UserError(gettext( + 'cashbook.msg_line_deny_stateedit_with_recon', + recname = line.rec_name, + )) - # debit / credit + # 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: + if not line.state in ['check', 'done']: + raise UserError(gettext( + 'cashbook.msg_line_deny_recon_by_state', + recname = line.rec_name + )) + + # update debit / credit if len(set(values.keys()).intersection(set({'amount', 'bookingtype'}))) > 0: for line in lines: values2 = {} diff --git a/locale/de.po b/locale/de.po index 621161a..d38bd9c 100644 --- a/locale/de.po +++ b/locale/de.po @@ -38,6 +38,22 @@ msgctxt "model:ir.message,text:msg_line_deny_delete2" msgid "The cashbook line '%(linetxt)s' cannot be deleted, its in state '%(linestate)s'." msgstr "Die Kassenbuchzeile '%(linetxt)s' kann nicht gelöscht werden, da sie im Status '%(linestate)s' ist." +msgctxt "model:ir.message,text:msg_line_deny_stateedit_with_recon" +msgid "The status cannot be changed to 'Edit' as long as the line '%(recname)s' is associated with a reconciliation." +msgstr "Der Status kann nicht in 'Bearbeiten' geändert werden, solange die Zeile '%(recname)s' mit einer Abstimmung verbunden ist." + +msgctxt "model:ir.message,text:msg_line_deny_write" +msgid "The cashbook line '%(recname)s' is '%(state_txt)s' and cannot be changed." +msgstr "Die Kassenbuchzeile '%(recname)s' ist '%(state_txt)s' und kann nicht geändert werden." + +msgctxt "model:ir.message,text:msg_recon_deny_delete1" +msgid "The reconciliation '%(recontxt)s' cannot be deleted because the Cashbook '%(bookname)s' is in state '%(bookstate)s'." +msgstr "Die Abstimmung '%(recontxt)s' kann nicht gelöscht werden, weil das Kassenbuch '%(bookname)s' im Status '%(bookstate)s' ist." + +msgctxt "model:ir.message,text:msg_recon_deny_delete2" +msgid "The reconciliation '%(recontxt)s' cannot be deleted, its in state '%(reconstate)s'." +msgstr "Die Abstimmung '%(recontxt)s' kann nicht gelöscht werden, da sie im Status '%(reconstate)s' ist." + msgctxt "model:ir.message,text:msg_setting_already_exists" msgid "Settings for this user alredy exists." msgstr "Einstellungen für diesen Benutzer sind bereits vorhanden." @@ -58,6 +74,22 @@ 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." + +msgctxt "model:ir.message,text:mds_recon_deny_line_not_check" +msgid "For the reconciliation '%(reconame)s' of the cashbook '%(bookname)s', all lines in the date range from '%(datefrom)s' to '%(dateto)s' must be in the 'Check' state." +msgstr "Für die Abstimmung '%(reconame)s' des Kassenbuchs '%(bookname)s' müssen alle Zeilen im Datumsbereich von '%(datefrom)s' bis '%(dateto)s' im Zustand 'Prüfen' sein." + +msgctxt "model:ir.message,text:msg_line_deny_write_by_reconciliation" +msgid "The line '%(recname)s' cannot be changed because the reconciliation '%(reconame)s'is 'Done'." +msgstr "Die Zeile '%(recname)s' kann nicht geändert werden, da die Abstimmung '%(reconame)s' ist." + +msgctxt "model:ir.message,text:msg_recon_err_overlap" +msgid "The date range overlaps with another reconciliation." +msgstr "Der Datumsbereich überschneidet sich mit einer anderen Abstimmung." + ############# # res.group # @@ -801,3 +833,11 @@ msgstr "Geschlossen" msgctxt "selection:cashbook.recon,state_cashbook:" msgid "Archive" msgstr "Archiv" + +msgctxt "field:cashbook.recon,start_amount:" +msgid "Start Amount" +msgstr "Anfangsbetrag" + +msgctxt "field:cashbook.recon,end_amount:" +msgid "End Amount" +msgstr "Endbetrag" diff --git a/locale/en.po b/locale/en.po index 610765c..c772074 100644 --- a/locale/en.po +++ b/locale/en.po @@ -23,8 +23,8 @@ msgid "The cashbook '%(bookname)s' cannot be deleted because it contains %(bookl msgstr "The cashbook '%(bookname)s' cannot be deleted because it contains %(booklines)s lines and is not in the status 'Archive'." msgctxt "model:ir.message,text:msg_book_deny_write" -msgid "The cash book '%(bookname)s' is '%(state_txt)s' and cannot be changed." -msgstr "The cash book '%(bookname)s' is '%(state_txt)s' and cannot be changed." +msgid "The cashbook '%(bookname)s' is '%(state_txt)s' and cannot be changed." +msgstr "The cashbook '%(bookname)s' is '%(state_txt)s' and cannot be changed." msgctxt "model:ir.message,text:msg_line_deny_delete1" msgid "The cashbook line '%(linetxt)s' cannot be deleted because the Cashbook '%(bookname)s' is in state '%(bookstate)s'." @@ -34,6 +34,18 @@ msgctxt "model:ir.message,text:msg_line_deny_delete2" msgid "The cashbook line '%(linetxt)s' cannot be deleted, its in state '%(linestate)s'." msgstr "The cashbook line '%(linetxt)s' cannot be deleted, its in state '%(linestate)s'." +msgctxt "model:ir.message,text:msg_line_deny_write" +msgid "The cashbook line '%(recname)s' is '%(state_txt)s' and cannot be changed." +msgstr "The cashbook line '%(recname)s' is '%(state_txt)s' and cannot be changed." + +msgctxt "model:ir.message,text:msg_recon_deny_delete1" +msgid "The reconciliation '%(recontxt)s' cannot be deleted because the Cashbook '%(bookname)s' is in state '%(bookstate)s'." +msgstr "The reconciliation '%(recontxt)s' cannot be deleted because the Cashbook '%(bookname)s' is in state '%(bookstate)s'." + +msgctxt "model:ir.message,text:msg_recon_deny_delete2" +msgid "The reconciliation '%(recontxt)s' cannot be deleted, its in state '%(reconstate)s'." +msgstr "The reconciliation '%(recontxt)s' cannot be deleted, its in state '%(reconstate)s'." + msgctxt "model:ir.message,text:msg_setting_already_exists" msgid "Settings for this user alredy exists." msgstr "Settings for this user alredy exists." @@ -42,6 +54,30 @@ msgctxt "model:ir.message,text:msg_category_name_unique" msgid "The category name already exists at this level." msgstr "The category name already exists at this level." +msgctxt "model:ir.message,text:msg_category_account_unique" +msgid "The account is already in use for a category." +msgstr "The account is already in use for a category." + +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'." + +msgctxt "model:ir.message,text:mds_recon_deny_line_not_check" +msgid "For the reconciliation '%(reconame)s' of the cashbook '%(bookname)s', all lines in the date range from '%(datefrom)s' to '%(dateto)s' must be in the 'Check' state." +msgstr "For the reconciliation '%(reconame)s' of the cashbook '%(bookname)s', all lines in the date range from '%(datefrom)s' to '%(dateto)s' must be in the 'Check' state." + +msgctxt "model:ir.message,text:msg_line_deny_write_by_reconciliation" +msgid "The line '%(recname)s' cannot be changed because the reconciliation '%(reconame)s'is 'Done'." +msgstr "The line '%(recname)s' cannot be changed because the reconciliation '%(reconame)s'is 'Done'." + msgctxt "model:res.group,name:group_cashbook" msgid "Cashbook" msgstr "Cashbook" @@ -74,6 +110,30 @@ msgctxt "model:ir.rule.group,name:rg_line_read" msgid "Observer: Cashbook line read" msgstr "Observer: Cashbook line read" +msgctxt "model:ir.rule.group,name:rg_line_read" +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" + +msgctxt "model:ir.rule.group,name:rg_book_companies" +msgid "User in companies" +msgstr "User in companies" + +msgctxt "model:ir.rule.group,name:rg_recon_companies" +msgid "User in companies" +msgstr "User in companies" + +msgctxt "model:ir.rule.group,name:rg_recon_write_adm" +msgid "Administrators: Reconciliation read/write" +msgstr "Administrators: Reconciliation read/write" + +msgctxt "model:ir.rule.group,name:rg_recon_write" +msgid "Owners and reviewers: Reconciliation write" +msgstr "Owners and reviewers: Reconciliation write" + msgctxt "model:ir.ui.menu,name:menu_cashbook" msgid "Cashbook" msgstr "Cashbook" @@ -86,7 +146,7 @@ msgctxt "model:ir.ui.menu,name:menu_typeconfig" msgid "Cashbook Type" msgstr "Cashbook Type" -msgctxt "model:ir.ui.menu,name:menu_bookconfig" +msgctxt "model:ir.ui.menu,name:menu_booklist" msgid "Cashbook" msgstr "Cashbook" @@ -103,8 +163,8 @@ msgid "Category" msgstr "Category" msgctxt "model:ir.action,name:act_book_view" -msgid "Account" -msgstr "Account" +msgid "Cashbook" +msgstr "Cashbook" msgctxt "model:ir.action,name:act_type_view" msgid "Cashbook Type" @@ -122,6 +182,18 @@ msgctxt "model:ir.ui.menu,name:act_category_view" msgid "Category" msgstr "Category" +msgctxt "model:ir.action.act_window.domain,name:act_line_domain_current" +msgid "Current Month" +msgstr "Current Month" + +msgctxt "model:ir.action.act_window.domain,name:act_line_domain_last" +msgid "Last Month" +msgstr "Last Month" + +msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all" +msgid "All" +msgstr "All" + msgctxt "model:ir.model.button,string:line_wfedit_button" msgid "Edit" msgstr "Edit" @@ -146,13 +218,29 @@ msgctxt "model:ir.model.button,string:book_wfarchive_button" msgid "Archive" msgstr "Archive" +msgctxt "model:ir.model.button,string:recon_wfedit_button" +msgid "Edit" +msgstr "Edit" + +msgctxt "model:ir.model.button,string:recon_wfcheck_button" +msgid "Check" +msgstr "Check" + +msgctxt "model:ir.model.button,string:recon_wfdone_button" +msgid "Done" +msgstr "Done" + msgctxt "model:cashbook.book,name:" msgid "Cashbook" msgstr "Cashbook" msgctxt "view:cashbook.book:" -msgid "Owner & Authorizeds" -msgstr "Owner & Authorizeds" +msgid "Owner and Authorizeds" +msgstr "Owner and Authorizeds" + +msgctxt "view:cashbook.book:" +msgid "Reconciliations" +msgstr "Reconciliations" msgctxt "field:cashbook.book,name:" msgid "Name" @@ -202,14 +290,50 @@ msgctxt "field:cashbook.book,account:" msgid "Account" msgstr "Account" +msgctxt "field:cashbook.book,company:" +msgid "Company" +msgstr "Company" + +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,balance:" +msgid "Balance" +msgstr "Balance" + +msgctxt "field:cashbook.book,reconciliations:" +msgid "Reconciliations" +msgstr "Reconciliations" + +msgctxt "field:cashbook.book,lines:" +msgid "Lines" +msgstr "Lines" + msgctxt "model:cashbook.line,name:" msgid "Cashbook Line" msgstr "Cashbook Line" +msgctxt "view:cashbook.line:" +msgid "Credit" +msgstr "Credit" + +msgctxt "view:cashbook.line:" +msgid "Debit" +msgstr "Debit" + msgctxt "view:cashbook.line:" msgid "Cashbook Line" msgstr "Cashbook Line" +msgctxt "view:cashbook.line:" +msgid "Description" +msgstr "Description" + msgctxt "view:cashbook.line:" msgid "State" msgstr "State" @@ -246,17 +370,73 @@ msgctxt "field:cashbook.line,month:" msgid "Month" msgstr "Month" -msgctxt "model:ir.action.act_window.domain,name:act_line_domain_current" -msgid "Current Month" -msgstr "Current Month" +msgctxt "field:cashbook.line,category:" +msgid "Category" +msgstr "Category" -msgctxt "model:ir.action.act_window.domain,name:act_line_domain_last" -msgid "Last Month" -msgstr "Last Month" +msgctxt "field:cashbook.line,category_view:" +msgid "Category" +msgstr "Category" -msgctxt "model:ir.action.act_window.domain,name:act_line_domain_all" -msgid "All" -msgstr "All" +msgctxt "field:cashbook.line,bookingtype:" +msgid "Type" +msgstr "Type" + +msgctxt "help:cashbook.line,bookingtype:" +msgid "Type of Booking" +msgstr "Type of Booking" + +msgctxt "selection:cashbook.line,bookingtype:" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "selection:cashbook.line,bookingtype:" +msgid "Expense" +msgstr "Expense" + +msgctxt "selection:cashbook.line,bookingtype:" +msgid "Transfer from" +msgstr "Transfer from" + +msgctxt "selection:cashbook.line,bookingtype:" +msgid "Transfer to" +msgstr "Transfer to" + +msgctxt "field:cashbook.line,company:" +msgid "Company" +msgstr "Company" + +msgctxt "field:cashbook.line,amount:" +msgid "Amount" +msgstr "Amount" + +msgctxt "field:cashbook.line,debit:" +msgid "Debit" +msgstr "Debit" + +msgctxt "field:cashbook.line,credit:" +msgid "Credit" +msgstr "Credit" + +msgctxt "field:cashbook.line,currency:" +msgid "Currency" +msgstr "Currency" + +msgctxt "field:cashbook.line,currency_digits:" +msgid "Currency Digits" +msgstr "Currency Digits" + +msgctxt "field:cashbook.line,balance:" +msgid "Balance" +msgstr "Balance" + +msgctxt "help:cashbook.line,balance:" +msgid "Balance of the cash book up to the current line, if the default sorting applies." +msgstr "Balance of the cash book up to the current line, if the default sorting applies." + +msgctxt "field:cashbook.line,reconciliation:" +msgid "Reconciliation" +msgstr "Reconciliation" msgctxt "model:cashbook.type,name:" msgid "Cashbook Type" @@ -270,17 +450,9 @@ msgctxt "field:cashbook.type,short:" msgid "Abbreviation" msgstr "Abbreviation" -msgctxt "model:cashbook.type,name:atype_cash" -msgid "Cash" -msgstr "Cash" - -msgctxt "model:cashbook.type,name:atype_giro" -msgid "Giro" -msgstr "Giro" - -msgctxt "model:cashbook.type,name:atype_fixtermdep" -msgid "Fixed-term deposit" -msgstr "Fixed-term deposit" +msgctxt "field:cashbook.type,company:" +msgid "Company" +msgstr "Company" msgctxt "model:cashbook.category,name:" msgid "Category" @@ -330,6 +502,22 @@ msgctxt "field:cashbook.category,right:" msgid "Right" msgstr "Right" +msgctxt "field:cashbook.category,cattype:" +msgid "Type" +msgstr "Type" + +msgctxt "help:cashbook.category,cattype:" +msgid "Type of Category" +msgstr "Type of Category" + +msgctxt "selection:cashbook.category,cattype:" +msgid "Revenue" +msgstr "Revenue" + +msgctxt "selection:cashbook.category,cattype:" +msgid "Expense" +msgstr "Expense" + msgctxt "model:cashbook.open_lines.start,name:" msgid "Open Cashbook" msgstr "Open Cashbook" @@ -370,11 +558,11 @@ msgctxt "model:cashbook.open_lines,name:" msgid "Open Cashbook" msgstr "Open Cashbook" -msgctxt "wizard_button:cashbook.open_lines,start,end:" +msgctxt "wizard_button:cashbook.open_lines,askuser,end:" msgid "Cancel" msgstr "Cancel" -msgctxt "wizard_button:cashbook.open_lines,start,open_:" +msgctxt "wizard_button:cashbook.open_lines,askuser,open_:" msgid "Open" msgstr "Open" @@ -418,6 +606,10 @@ msgctxt "view:cashbook.configuration:" msgid "Open Cashbook Wizard" msgstr "Open Cashbook Wizard" +msgctxt "view:cashbook.configuration:" +msgid "Cashbook" +msgstr "Cashbook" + msgctxt "field:cashbook.configuration,date_from:" msgid "Start Date" msgstr "Start Date" @@ -430,10 +622,34 @@ msgctxt "field:cashbook.configuration,checked:" msgid "Checked" msgstr "Checked" +msgctxt "help:cashbook.configuration,checked:" +msgid "Show cashbook lines in Checked-state." +msgstr "Show cashbook lines in Checked-state." + msgctxt "field:cashbook.configuration,done:" msgid "Done" msgstr "Done" +msgctxt "help:cashbook.configuration,done:" +msgid "Show cashbook lines in Done-state." +msgstr "Show cashbook lines in Done-state." + +msgctxt "field:cashbook.configuration,catnamelong:" +msgid "Category: Show long name" +msgstr "Category: Show long name" + +msgctxt "help:cashbook.configuration,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,cataccno:" +msgid "Category: Show account number" +msgstr "Category: Show account number" + +msgctxt "help:cashbook.configuration,cataccno:" +msgid "Shows the number of the linked account in the name of a category." +msgstr "Shows the number of the linked account in the name of a category." + msgctxt "model:cashbook.configuration_user,name:" msgid "User Configuration" msgstr "User Configuration" @@ -450,3 +666,91 @@ msgctxt "field:cashbook.configuration_user,checked:" msgid "Checked" msgstr "Checked" +msgctxt "help:cashbook.configuration_user,checked:" +msgid "Show cashbook lines in Checked-state." +msgstr "Show cashbook lines in Checked-state." + +msgctxt "field:cashbook.configuration_user,done:" +msgid "Done" +msgstr "Done" + +msgctxt "help:cashbook.configuration_user,done:" +msgid "Show cashbook lines in Done-state." +msgstr "Show cashbook lines in Done-state." + +msgctxt "field:cashbook.configuration_user,catnamelong:" +msgid "Category: Show long name" +msgstr "Category: Show long name" + +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,cataccno:" +msgid "Category: Show account number" +msgstr "Category: Show account number" + +msgctxt "help:cashbook.configuration_user,cataccno:" +msgid "Shows the number of the linked account in the name of a category." +msgstr "Shows the number of the linked account in the name of a category." + +msgctxt "model:cashbook.recon,name:" +msgid "Cashbook Reconciliation" +msgstr "Cashbook Reconciliation" + +msgctxt "view:cashbook.recon:" +msgid "Reconciliation period" +msgstr "Reconciliation period" + +msgctxt "view:cashbook.recon:" +msgid "State" +msgstr "State" + +msgctxt "field:cashbook.recon,cashbook:" +msgid "Cashbook" +msgstr "Cashbook" + +msgctxt "field:cashbook.recon,date:" +msgid "Date" +msgstr "Date" + +msgctxt "field:cashbook.recon,date_from:" +msgid "Start Date" +msgstr "Start Date" + +msgctxt "field:cashbook.recon,date_to:" +msgid "End Date" +msgstr "End Date" + +msgctxt "field:cashbook.recon,state:" +msgid "State" +msgstr "State" + +msgctxt "selection:cashbook.recon,state:" +msgid "Edit" +msgstr "Edit" + +msgctxt "selection:cashbook.recon,state:" +msgid "Check" +msgstr "Check" + +msgctxt "selection:cashbook.recon,state:" +msgid "Done" +msgstr "Done" + +msgctxt "field:cashbook.recon,lines:" +msgid "Lines" +msgstr "Lines" + +msgctxt "field:cashbook.recon,state_cashbook:" +msgid "State of Cashbook" +msgstr "State of Cashbook" + +msgctxt "selection:cashbook.recon,state_cashbook:" +msgid "Open" +msgstr "Open" + +msgctxt "selection:cashbook.recon,state_cashbook:" +msgid "Closed" +msgstr "Closed" + diff --git a/message.xml b/message.xml index 439317f..f4f8ba1 100644 --- a/message.xml +++ b/message.xml @@ -29,6 +29,18 @@ full copyright notices and license terms. --> The cashbook line '%(linetxt)s' cannot be deleted, its in state '%(linestate)s'. + + The cashbook line '%(recname)s' is '%(state_txt)s' and cannot be changed. + + + The status cannot be changed to 'Edit' as long as the line '%(recname)s' is associated with a reconciliation. + + + The reconciliation '%(recontxt)s' cannot be deleted because the Cashbook '%(bookname)s' is in state '%(bookstate)s'. + + + The reconciliation '%(recontxt)s' cannot be deleted, its in state '%(reconstate)s'. + Settings for this user alredy exists. @@ -44,6 +56,18 @@ full copyright notices and license terms. --> 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'. + + + For the reconciliation '%(reconame)s' of the cashbook '%(bookname)s', all lines in the date range from '%(datefrom)s' to '%(dateto)s' must be in the 'Check' state. + + + The line '%(recname)s' cannot be changed because the reconciliation '%(reconame)s'is 'Done'. + + + The date range overlaps with another reconciliation. + diff --git a/reconciliation.py b/reconciliation.py index 6105259..55fe50c 100644 --- a/reconciliation.py +++ b/reconciliation.py @@ -3,12 +3,16 @@ # 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, Unique +from trytond.model import Workflow, ModelView, ModelSQL, fields from trytond.transaction import Transaction from trytond.pyson import Eval, If, Or from trytond.pool import Pool from trytond.report import Report +from trytond.exceptions import UserError +from trytond.i18n import gettext from decimal import Decimal +from sql.operators import Equal, Between +from sql import Literal, Null from .book import sel_state_book @@ -52,11 +56,19 @@ class Reconciliation(Workflow, ModelSQL, ModelView): ()), ], states=STATES, depends=DEPENDS+['date_from']) + start_amount = fields.Numeric(string='Start Amount', required=True, + readonly=True, digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + end_amount = fields.Numeric(string='End Amount', required=True, + readonly=True, digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + lines = fields.One2Many(string='Lines', field='reconciliation', model_name='cashbook.line', states=STATES, depends=DEPENDS+['date_from', 'date_to', 'cashbook'], add_remove=[ ('cashbook', '=', Eval('cashbook')), + ('state', 'in', ['check', 'done']), ('date', '>=', Eval('date_from')), ('date', '<=', Eval('date_to')), ], @@ -65,6 +77,11 @@ class Reconciliation(Workflow, ModelSQL, ModelView): ('date', '<=', Eval('date_to')), ]) + 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') + state = fields.Selection(string='State', required=True, readonly=True, select=True, selection=sel_reconstate) state_string = state.translated('state') @@ -96,40 +113,169 @@ class Reconciliation(Workflow, ModelSQL, ModelView): }, }) + @classmethod + def check_overlap_dates(cls, date_from, date_to, id_cashbook, record=None): + """ deny overlap of date_from/date_to between records of same cashbook + allow: date_to=date_from + """ + Recon = Pool().get('cashbook.recon') + + query = [ + ('cashbook.id', '=', id_cashbook), + ['OR', + [ # 'start' is inside of other record + ('date_from', '<=', date_from), + ('date_to', '>', date_from), + ], + [ # 'end' is inside of other record + ('date_from', '<', date_to), + ('date_to', '>=', date_to), + ], + [ # enclose other record + ('date_from', '>=', date_from), + ('date_to', '<=', date_to), + ], + ], + ] + + # avoid finding ourselves + if record: + query.append(('id', '!=', record.id)) + + if Recon.search_count(query) > 0: + raise UserError(gettext('cashbook.msg_recon_err_overlap')) + + @classmethod + def check_lines_not_checked(cls, reconciliations): + """ deny lines in date-range not 'checked', w/o records at date-limit + """ + Line = Pool().get('cashbook.line') + + for reconciliation in reconciliations: + if Line.search_count([ + ('date', '>', reconciliation.date_from), + ('date', '<', reconciliation.date_to), + ('cashbook.id', '=', reconciliation.cashbook.id), + ('state', '!=', 'check'), + ]) > 0: + raise UserError(gettext( + 'cashbook.mds_recon_deny_line_not_check', + bookname = reconciliation.cashbook.rec_name, + reconame = reconciliation.rec_name, + datefrom = Report.format_date(reconciliation.date_from), + dateto = Report.format_date(reconciliation.date_to), + )) + @classmethod @ModelView.button @Workflow.transition('edit') - def wfedit(cls, lines): + def wfedit(cls, reconciliations): """ edit """ - pass + Recon = Pool().get('cashbook.recon') + + to_write = [] + for reconciliation in reconciliations: + values = { + 'start_amount': Decimal('0.0'), + 'end_amount': Decimal('0.0'), + } + + # unlink lines from reconciliation + if len(reconciliation.lines) > 0: + values['lines'] = [('remove', [x.id for x in reconciliation.lines])] + + to_write.extend([[reconciliation], values]) + + if len(to_write) > 0: + Recon.write(*to_write) @classmethod @ModelView.button @Workflow.transition('check') - def wfcheck(cls, lines): - """ is checked + def wfcheck(cls, reconciliations): + """ checked: add lines of book in date-range to reconciliation, + state of lines must be 'checked' """ - pass + pool = Pool() + Line = pool.get('cashbook.line') + Recon = pool.get('cashbook.recon') + + cls.check_lines_not_checked(reconciliations) + + to_write = [] + for reconciliation in reconciliations: + values = {} + + # get start_amount: end_amount of predecessor + pre_recon = Recon.search([ + ('cashbook.id', '=', reconciliation.cashbook.id), + ('date_to', '<=', reconciliation.date_from), + ('state', 'in', ['check', 'done']), + ], order=[('date_to', 'DESC')], limit=1) + if len(pre_recon) > 0: + values['start_amount'] = pre_recon[0].end_amount + else : + # not found, use 'start_balance' of cashbook + values['start_amount'] = reconciliation.cashbook.start_balance + values['end_amount'] = values['start_amount'] + + # add 'checked'-lines to reconciliation + lines = Line.search([ + ('date', '>=', reconciliation.date_from), + ('date', '<=', reconciliation.date_to), + ('cashbook.id', '=', reconciliation.cashbook.id), + ('reconciliation', '=', None), + ('state', '=', 'check'), + ]) + if len(lines) > 0: + values['lines'] = [('add', [x.id for x in lines])] + + # add amounts of new lines + values['end_amount'] += sum([x.credit - x.debit for x in lines]) + # add amounts of already linked lines + values['end_amount'] += sum([x.credit - x.debit for x in reconciliation.lines]) + + to_write.extend([[reconciliation], values]) + + if len(to_write) > 0: + Recon.write(*to_write) @classmethod @ModelView.button @Workflow.transition('done') - def wfdone(cls, lines): + def wfdone(cls, reconciliations): """ is done """ - pass + Line = Pool().get('cashbook.line') + + to_wfdone_line = [] + for reconciliation in reconciliations: + to_wfdone_line.extend(list(reconciliation.lines)) + + if len(to_wfdone_line) > 0: + Line.wfdone(to_wfdone_line) def get_rec_name(self, name): """ short + name """ - return '%(from)s - %(to)s: %(amount)s %(symbol)s' % { + return '%(from)s - %(to)s | %(start_amount)s %(symbol)s - %(start_amount)s %(symbol)s [%(num)s]' % { 'from': Report.format_date(self.date_from, None) if self.date_from is not None else '-', 'to': Report.format_date(self.date_to, None) if self.date_to is not None else '-', - 'amount': Decimal('0.0'), - 'symbol': getattr(getattr(self.cashbook, 'currency', None), 'symbol', '-'), + 'start_amount': Report.format_number(self.start_amount or 0.0, None), + 'end_amount': Report.format_number(self.end_amount or 0.0, None), + 'symbol': getattr(self.currency, 'symbol', '-'), + 'num': len(self.lines), } + @classmethod + def default_start_amount(cls): + return Decimal('0.0') + + @classmethod + def default_end_amount(cls): + return Decimal('0.0') + @classmethod def default_state(cls): return 'edit' @@ -141,6 +287,22 @@ class Reconciliation(Workflow, ModelSQL, ModelView): IrDate = Pool().get('ir.date') return IrDate.today() + @fields.depends('cashbook', '_parent_cashbook.currency') + def on_change_with_currency(self, name=None): + """ currency of cashbook + """ + if self.cashbook: + return self.cashbook.currency.id + + @fields.depends('cashbook', '_parent_cashbook.currency') + def on_change_with_currency_digits(self, name=None): + """ currency of cashbook + """ + if self.cashbook: + return self.cashbook.currency.digits + else: + return 2 + @fields.depends('cashbook', '_parent_cashbook.state') def on_change_with_state_cashbook(self, name=None): """ get state of cashbook @@ -148,4 +310,63 @@ class Reconciliation(Workflow, ModelSQL, ModelView): if self.cashbook: return self.cashbook.state + @classmethod + def create(cls, vlist): + """ add debit/credit + """ + for values in vlist: + cls.check_overlap_dates( + values.get('date_from', None), + values.get('date_to', None), + values.get('cashbook', None)) + return super(Reconciliation, cls).create(vlist) + + @classmethod + def write(cls, *args): + """ deny update if cashbook.line!='open', + add or update debit/credit + """ + actions = iter(args) + for reconciliations, values in zip(actions, actions): + # deny write if chashbook is not open + for reconciliation in reconciliations: + + # deny overlap + if len(set({'date_from', 'date_to'}).intersection(set(values.keys()))) > 0: + cls.check_overlap_dates( + values.get('date_from', reconciliation.date_from), + values.get('date_to', reconciliation.date_to), + reconciliation.cashbook.id, + reconciliation) + + if reconciliation.cashbook.state != 'open': + raise UserError(gettext( + 'cashbook.msg_book_deny_write', + bookname = reconciliation.cashbook.rec_name, + state_txt = reconciliation.cashbook.state_string, + )) + + super(Reconciliation, cls).write(*args) + + @classmethod + def delete(cls, reconciliations): + """ deny delete if book is not 'open' or wf is not 'edit' + """ + for reconciliation in reconciliations: + if reconciliation.cashbook.state == 'closed': + raise UserError(gettext( + 'cashbook.msg_line_deny_delete1', + linetxt = reconciliation.rec_name, + bookname = reconciliation.cashbook.rec_name, + bookstate = reconciliation.cashbook.state_string, + )) + if reconciliation.state != 'edit': + raise UserError(gettext( + 'cashbook.msg_recon_deny_delete2', + recontxt = reconciliation.rec_name, + reconstate = reconciliation.state_string, + )) + + return super(Reconciliation, cls).delete(reconciliations) + # end Type diff --git a/tests/__init__.py b/tests/__init__.py index 25789bd..8ecdac3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -9,11 +9,13 @@ from trytond.modules.cashbook.tests.test_book import BookTestCase from trytond.modules.cashbook.tests.test_line import LineTestCase 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 __all__ = ['suite'] class CashbookTestCase(\ + ReconTestCase,\ CategoryTestCase,\ ConfigTestCase,\ LineTestCase, diff --git a/tests/test_line.py b/tests/test_line.py index 3bcd986..0466dff 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -51,10 +51,10 @@ class LineTestCase(ModuleTestCase): self.assertEqual(book.state, 'open') 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 Text 1') + self.assertEqual(book.lines[0].rec_name, '05/01/2022|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 Text 2') + self.assertEqual(book.lines[1].rec_name, '05/02/2022|1.00 usd|Text 2 [Cat1]') self.assertEqual(Lines.search_count([('rec_name', '=', 'Text 1')]), 1) self.assertEqual(Lines.search_count([('rec_name', '=', 'Text 1a')]), 0) @@ -67,9 +67,9 @@ class LineTestCase(ModuleTestCase): # sorting: date -> state -> id self.assertEqual(len(book.lines), 2) - self.assertEqual(book.lines[0].rec_name, '05/01/2022 Text 1') + self.assertEqual(book.lines[0].rec_name, '05/01/2022|1.00 usd|Text 1 [Cat1]') self.assertEqual(book.lines[0].state, 'edit') - self.assertEqual(book.lines[1].rec_name, '05/02/2022 Text 2') + self.assertEqual(book.lines[1].rec_name, '05/02/2022|1.00 usd|Text 2 [Cat1]') self.assertEqual(book.lines[1].state, 'edit') # set to same date @@ -80,17 +80,17 @@ class LineTestCase(ModuleTestCase): }]) # check again book, = Book.search([]) - self.assertEqual(book.lines[0].rec_name, '05/01/2022 Text 1') + self.assertEqual(book.lines[0].rec_name, '05/01/2022|1.00 usd|Text 1 [Cat1]') self.assertEqual(book.lines[0].state, 'edit') - self.assertEqual(book.lines[1].rec_name, '05/01/2022 Text 2') + self.assertEqual(book.lines[1].rec_name, '05/01/2022|1.00 usd|Text 2 [Cat1]') self.assertEqual(book.lines[1].state, 'edit') # set to 'check', will sort first Lines.wfcheck([book.lines[1]]) book, = Book.search([]) - self.assertEqual(book.lines[0].rec_name, '05/01/2022 Text 2') + self.assertEqual(book.lines[0].rec_name, '05/01/2022|1.00 usd|Text 2 [Cat1]') self.assertEqual(book.lines[0].state, 'check') - self.assertEqual(book.lines[1].rec_name, '05/01/2022 Text 1') + self.assertEqual(book.lines[1].rec_name, '05/01/2022|1.00 usd|Text 1 [Cat1]') self.assertEqual(book.lines[1].state, 'edit') @with_transaction() @@ -127,6 +127,24 @@ class LineTestCase(ModuleTestCase): self.assertEqual(book.state, 'open') self.assertEqual(len(book.lines), 2) + Line.write(*[ + [book.lines[0]], + { + 'description': 'works', + }]) + Line.wfcheck([book.lines[0]]) + self.assertEqual(book.lines[0].state, 'check') + + self.assertRaisesRegex(UserError, + "The cashbook line '05/01/2022|1.00 usd|works [Cat1]' is 'Checked' and cannot be changed.", + Line.write, + *[ + [book.lines[0]], + { + 'description': 'denied by line.state', + }, + ]) + Book.wfclosed([book]) self.assertEqual(book.state, 'closed') @@ -521,7 +539,7 @@ class LineTestCase(ModuleTestCase): self.assertEqual(book.lines[0].state, 'check') self.assertRaisesRegex(UserError, - "The cashbook line '05/01/2022 Text 1' cannot be deleted, its in state 'Checked'.", + "The cashbook line '05/01/2022|1.00 usd|Test 1 [Cat1]' cannot be deleted, its in state 'Checked'.", Lines.delete, [book.lines[0]]) @@ -586,14 +604,14 @@ class LineTestCase(ModuleTestCase): lines = Line.search([]) self.assertEqual(len(lines), 1) self.assertEqual(lines[0].cashbook.rec_name, 'Fridas book | 1.00 usd | Open') - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 1') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 1 [Cat1]') Line.write(*[ lines, { 'description': 'Test 2', }]) - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 2') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 2 [Cat1]') @with_transaction() def test_line_permission_reviewer(self): @@ -659,25 +677,25 @@ class LineTestCase(ModuleTestCase): self.assertEqual(len(lines), 1) self.assertEqual(len(lines[0].cashbook.reviewer.users), 1) self.assertEqual(lines[0].cashbook.reviewer.users[0].rec_name, 'Diego') - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 1') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 1 [Cat1]') Line.write(*[ lines, { 'description': 'Test 2', }]) - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 2') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 2 [Cat1]') # change to user 'frida' read/write line with Transaction().set_user(usr_lst[0].id): lines = Line.search([]) self.assertEqual(len(lines), 1) - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 2') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 2 [Cat1]') Line.write(*[ lines, { 'description': 'Test 3', }]) - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 3') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 3 [Cat1]') @with_transaction() def test_line_permission_observer(self): @@ -743,7 +761,7 @@ class LineTestCase(ModuleTestCase): self.assertEqual(len(lines), 1) self.assertEqual(len(lines[0].cashbook.observer.users), 1) self.assertEqual(lines[0].cashbook.observer.users[0].rec_name, 'Diego') - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 1') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 1 [Cat1]') self.assertRaisesRegex(UserError, 'You are not allowed to write to records "[0-9]{1,}" of "Cashbook Line" because of at least one of these rules:\nOwners and reviewers: Cashbook line write - ', @@ -759,12 +777,12 @@ class LineTestCase(ModuleTestCase): with Transaction().set_user(usr_lst[0].id): lines = Line.search([]) self.assertEqual(len(lines), 1) - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 1') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 1 [Cat1]') Line.write(*[ lines, { 'description': 'Test 2', }]) - self.assertEqual(lines[0].rec_name, '05/01/2022 Test 2') + self.assertEqual(lines[0].rec_name, '05/01/2022|1.00 usd|Test 2 [Cat1]') # end LineTestCase diff --git a/tests/test_reconciliation.py b/tests/test_reconciliation.py new file mode 100644 index 0000000..6e12b81 --- /dev/null +++ b/tests/test_reconciliation.py @@ -0,0 +1,365 @@ +# -*- 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 + + +class ReconTestCase(ModuleTestCase): + 'Test cashbook reconciliation module' + module = 'cashbook' + + @with_transaction() + def test_recon_check_overlap(self): + """ create, check deny of overlap date + """ + pool = Pool() + Book = pool.get('cashbook.book') + Reconciliation = pool.get('cashbook.recon') + + types = self.prep_type() + category = self.prep_category(cattype='in') + company = self.prep_company() + book, = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + }]) + + recon1, = Reconciliation.create([{ + 'date': date(2022, 5, 1), + 'date_from': date(2022, 5, 1), + 'date_to': date(2022, 5, 31), + 'cashbook': book.id, + }]) + self.assertEqual(recon1.rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') + + recon2, = Reconciliation.create([{ + 'date': date(2022, 6, 1), + 'date_from': date(2022, 6, 1), + 'date_to': date(2022, 6, 30), + 'cashbook': book.id, + }]) + self.assertEqual(recon2.rec_name, '06/01/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + + # same day for end of '0' and start of '1' + Reconciliation.write(*[ + [recon2], + { + 'date_from': date(2022, 5, 31), + }, + ]) + self.assertEqual(recon1.rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(recon2.rec_name, '05/31/2022 - 06/30/2022 | 0.00 usd - 0.00 usd [0]') + + # 'date_from' inside of other record + self.assertRaisesRegex(UserError, + 'The date range overlaps with another reconciliation.', + Reconciliation.write, + *[ + [recon2], + { + 'date_from': date(2022, 5, 30), + }, + ]) + + # 'date_from' inside of other record + self.assertRaisesRegex(UserError, + 'The date range overlaps with another reconciliation.', + Reconciliation.write, + *[ + [recon2], + { + 'date_from': date(2022, 5, 2), + }, + ]) + + # 'date_from' same date_from like other record + self.assertRaisesRegex(UserError, + 'The date range overlaps with another reconciliation.', + Reconciliation.write, + *[ + [recon2], + { + 'date_from': date(2022, 5, 1), + }, + ]) + + # enclose other record + self.assertRaisesRegex(UserError, + 'The date range overlaps with another reconciliation.', + Reconciliation.write, + *[ + [recon2], + { + 'date_from': date(2022, 4, 30), + }, + ]) + + # within other record + self.assertRaisesRegex(UserError, + 'The date range overlaps with another reconciliation.', + Reconciliation.write, + *[ + [recon2], + { + 'date_from': date(2022, 5, 2), + 'date_to': date(2022, 5, 12), + }, + ]) + + # from after to + self.assertRaisesRegex(UserError, + 'The value for field "Start Date" in "Cashbook Reconciliation" is not valid according to its domain.', + Reconciliation.write, + *[ + [recon2], + { + 'date_from': date(2022, 4, 15), + 'date_to': date(2022, 4, 10), + }, + ]) + + # 'date_to' at 'date_from' of other record - allowed + Reconciliation.write(*[ + [recon2], + { + 'date_from': date(2022, 4, 15), + 'date_to': date(2022, 5, 1), + }]) + + # 'date_to' after other date_from + self.assertRaisesRegex(UserError, + 'The date range overlaps with another reconciliation.', + Reconciliation.write, + *[ + [recon2], + { + 'date_to': date(2022, 5, 2), + }, + ]) + + # overlap at create + self.assertRaisesRegex(UserError, + 'The date range overlaps with another reconciliation.', + Reconciliation.create, + [{ + 'date': date(2022, 5, 1), + 'date_from': date(2022, 4, 1), + 'date_to': date(2022, 4, 16), + 'cashbook': book.id, + }]) + + recon3, = Reconciliation.create([{ + 'date': date(2022, 4, 17), + 'date_from': date(2022, 4, 1), + 'date_to': date(2022, 4, 15), + 'cashbook': book.id, + }]) + self.assertEqual(recon1.rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(recon2.rec_name, '04/15/2022 - 05/01/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(recon3.rec_name, '04/01/2022 - 04/15/2022 | 0.00 usd - 0.00 usd [0]') + + @with_transaction() + def test_recon_create_check_line_add_to_recon(self): + """ create, booklines, add reconciliation + """ + 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() + book, = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Text 1', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + }, { + 'date': date(2022, 5, 5), + 'description': 'Text 2', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + }])], + 'reconciliations': [('create', [{ + 'date': date(2022, 5, 28), + 'date_from': date(2022, 5, 1), + 'date_to': date(2022, 5, 31), + }])], + }]) + self.assertEqual(book.name, 'Book 1') + self.assertEqual(book.state, 'open') + self.assertEqual(len(book.lines), 2) + self.assertEqual(book.lines[0].rec_name, '05/01/2022|1.00 usd|Text 1 [Cat1]') + self.assertEqual(book.lines[1].rec_name, '05/05/2022|1.00 usd|Text 2 [Cat1]') + self.assertEqual(len(book.reconciliations), 1) + self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(len(book.reconciliations[0].lines), 0) + + self.assertRaisesRegex(UserError, + "For reconciliation, the line '05/01/2022|1.00 usd|Text 1 [Cat1]' must be in the status 'Check' or 'Done'.", + Lines.write, + *[ + [book.lines[0]], + { + 'reconciliation': book.reconciliations[0].id, + } + ]) + Lines.wfcheck(book.lines) + + Lines.write(*[ + list(book.lines), + { + 'reconciliation': book.reconciliations[0].id, + }]) + self.assertEqual(len(book.reconciliations[0].lines), 2) + + self.assertRaisesRegex(UserError, + "The status cannot be changed to 'Edit' as long as the line '05/01/2022|1.00 usd|Text 1 [Cat1]' is associated with a reconciliation.", + Lines.wfedit, + [book.lines[0]]) + + @with_transaction() + def test_recon_check_deny_delete(self): + """ create, booklines, reconciliation, try delete + """ + 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() + book, = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Text 1', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + }])], + 'reconciliations': [('create', [{ + 'date': date(2022, 5, 28), + 'date_from': date(2022, 5, 1), + 'date_to': date(2022, 5, 31), + }])], + }]) + self.assertEqual(book.name, 'Book 1') + self.assertEqual(book.state, 'open') + + Lines.wfcheck(list(book.lines)) + Reconciliation.wfcheck(list(book.reconciliations)) + self.assertEqual(len(book.reconciliations), 1) + self.assertEqual(len(book.reconciliations[0].lines), 1) + + self.assertRaisesRegex(UserError, + "The reconciliation '05/01/2022 - 05/31/2022 | 0.00 - 0.00 usd [0]' cannot be deleted, its in state 'Check'.", + Reconciliation.delete, + list(book.reconciliations)) + + Book.wfclosed([book]) + + self.assertRaisesRegex(UserError, + "The cashbook line '05/01/2022 - 05/31/2022: 0.00 usd' cannot be deleted because the Cashbook 'Book 1 | 1.00 usd | Closed' is in state 'Closed'.", + Reconciliation.delete, + list(book.reconciliations)) + + self.assertRaisesRegex(UserError, + "The cash book 'Book 1 | 1.00 usd | Closed' is 'Closed' and cannot be changed.", + Reconciliation.write, + *[ + list(book.reconciliations), + { + 'date': date(2022, 5, 29), + }, + ]) + + @with_transaction() + def test_recon_check_wf_edit_to_check(self): + """ create, booklines, add reconciliation + """ + 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() + book, = Book.create([{ + 'name': 'Book 1', + 'btype': types.id, + 'company': company.id, + 'currency': company.currency.id, + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Text 1', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + }, { + 'date': date(2022, 5, 5), + 'description': 'Text 2', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1.0'), + }])], + 'reconciliations': [('create', [{ + 'date': date(2022, 5, 28), + 'date_from': date(2022, 5, 1), + 'date_to': date(2022, 5, 31), + }])], + }]) + self.assertEqual(book.name, 'Book 1') + self.assertEqual(book.state, 'open') + self.assertEqual(len(book.lines), 2) + self.assertEqual(book.lines[0].rec_name, '05/01/2022|1.00 usd|Text 1 [Cat1]') + self.assertEqual(book.lines[1].rec_name, '05/05/2022|1.00 usd|Text 2 [Cat1]') + self.assertEqual(book.lines[0].reconciliation, None) + self.assertEqual(book.lines[1].reconciliation, None) + self.assertEqual(len(book.reconciliations), 1) + self.assertEqual(book.reconciliations[0].rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]') + self.assertEqual(len(book.reconciliations[0].lines), 0) + + # run wf, fail with lines not 'checked' + self.assertRaisesRegex(UserError, + "For the reconciliation '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [0]' of the cashbook 'Book 1 | 2.00 usd | Open', all lines in the date range from '05/01/2022' to '05/31/2022' must be in the 'Check' state.", + Reconciliation.wfcheck, + list(book.reconciliations), + ) + + # edit --> check + Lines.wfcheck(book.lines) + Reconciliation.wfcheck(list(book.reconciliations)) + self.assertEqual(len(book.reconciliations[0].lines), 2) + self.assertEqual(book.lines[0].reconciliation.rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [2]') + self.assertEqual(book.lines[1].reconciliation.rec_name, '05/01/2022 - 05/31/2022 | 0.00 usd - 0.00 usd [2]') + + # check --> edit + Reconciliation.wfedit(list(book.reconciliations)) + self.assertEqual(len(book.reconciliations[0].lines), 0) + self.assertEqual(book.lines[0].reconciliation, None) + self.assertEqual(book.lines[1].reconciliation, None) + +# end ReconTestCase diff --git a/view/recon_form.xml b/view/recon_form.xml index 94ce19b..536fe66 100644 --- a/view/recon_form.xml +++ b/view/recon_form.xml @@ -22,7 +22,11 @@ full copyright notices and license terms. -->