diff --git a/line.py b/line.py index 607b0a0..6b9515a 100644 --- a/line.py +++ b/line.py @@ -279,19 +279,41 @@ 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, + 'bookingtype': 'mvin' if line.bookingtype == 'mvout' else 'mvout', + 'date': line.date, + 'description': sp_line.description, + 'booktransf': line.cashbook.id, + 'reference': line.id, + } + 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: @@ -393,7 +415,7 @@ class Line(Workflow, ModelSQL, ModelView): }): values['amount'] = Currency.compute( self.cashbook.currency, - self.amount, + values['amount'], to_currency) return values @@ -748,6 +770,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', diff --git a/locale/de.po b/locale/de.po index e169b0f..d1be018 100644 --- a/locale/de.po +++ b/locale/de.po @@ -606,6 +606,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 # diff --git a/locale/en.po b/locale/en.po index 2dd8149..fdab81a 100644 --- a/locale/en.po +++ b/locale/en.po @@ -566,6 +566,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" 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/test_line.py b/tests/test_line.py index 8e04bcf..cc30e49 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -911,7 +911,7 @@ class LineTestCase(ModuleTestCase): 'booktransf': book2.id, }, { 'date': date(2022, 6, 1), # in-category, return - 'description': 'in-return', # amount negative + 'description': 'in-return', # amount negative 'category': category_in.id, 'bookingtype': 'in', 'amount': Decimal('-1.0'), 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/view/split_form.xml b/view/split_form.xml index 3773e8f..dd0924f 100644 --- a/view/split_form.xml +++ b/view/split_form.xml @@ -9,8 +9,13 @@ full copyright notices and license terms. -->