diff --git a/locale/de.po b/locale/de.po index 7bb3e9b..e62fd0f 100644 --- a/locale/de.po +++ b/locale/de.po @@ -102,6 +102,10 @@ msgctxt "view:cashbook.planner:" msgid "Booking" msgstr "Buchung" +msgctxt "view:cashbook.planner:" +msgid "Booking text" +msgstr "Buchungstext" + msgctxt "field:cashbook.planner,company:" msgid "Company" msgstr "Unternehmen" @@ -238,18 +242,10 @@ msgctxt "selection:cashbook.planner,bookingtype:" msgid "Revenue" msgstr "Einnahme" -msgctxt "selection:cashbook.planner,bookingtype:" -msgid "Revenue Splitbooking" -msgstr "Einnahme Splitbuchung" - msgctxt "selection:cashbook.planner,bookingtype:" msgid "Expense" msgstr "Ausgabe" -msgctxt "selection:cashbook.planner,bookingtype:" -msgid "Expense Splitbooking" -msgstr "Ausgabe Splitbuchung" - msgctxt "selection:cashbook.planner,bookingtype:" msgid "Transfer from" msgstr "Umbuchung von" @@ -258,6 +254,50 @@ msgctxt "selection:cashbook.planner,bookingtype:" msgid "Transfer to" msgstr "Umbuchung nach" +msgctxt "field:cashbook.planner,currency_cashbook:" +msgid "Currency" +msgstr "Währung" + +msgctxt "help:cashbook.planner,currency_cashbook:" +msgid "Currency of Cashbook" +msgstr "Währung des Kassenbuchs" + +msgctxt "field:cashbook.planner,amount:" +msgid "Amount" +msgstr "Betrag" + +msgctxt "field:cashbook.planner,category:" +msgid "Category" +msgstr "Kategorie" + +msgctxt "help:cashbook.planner,category:" +msgid "Category for the planned booking" +msgstr "Kategorie für die geplante Buchung" + +msgctxt "field:cashbook.planner,booktransf:" +msgid "Source/Dest" +msgstr "Quelle/Ziel" + +msgctxt "field:cashbook.planner,owner_cashbook:" +msgid "Owner" +msgstr "Eigentümer" + +msgctxt "field:cashbook.planner,state_cashbook:" +msgid "State of Cashbook" +msgstr "Status des Kassenbuchs" + +msgctxt "field:cashbook.planner,subject:" +msgid "Booking text" +msgstr "Buchungstext" + +msgctxt "field:cashbook.planner,wfcheck:" +msgid "Set to 'Checked'" +msgstr "auf 'Geprüft' setzen" + +msgctxt "help:cashbook.planner,wfcheck:" +msgid "Switches the booking to the 'Verified' state." +msgstr "Schaltet die Buchung in den Zustand 'Geprüft'" + ############################ # cashbook.planner.nextrun # diff --git a/locale/en.po b/locale/en.po index 6ff0974..77d1b8b 100644 --- a/locale/en.po +++ b/locale/en.po @@ -78,6 +78,10 @@ msgctxt "view:cashbook.planner:" msgid "Booking" msgstr "Booking" +msgctxt "view:cashbook.planner:" +msgid "Booking text" +msgstr "Booking text" + msgctxt "field:cashbook.planner,company:" msgid "Company" msgstr "Company" @@ -214,18 +218,10 @@ msgctxt "selection:cashbook.planner,bookingtype:" msgid "Revenue" msgstr "Revenue" -msgctxt "selection:cashbook.planner,bookingtype:" -msgid "Revenue Splitbooking" -msgstr "Revenue Splitbooking" - msgctxt "selection:cashbook.planner,bookingtype:" msgid "Expense" msgstr "Expense" -msgctxt "selection:cashbook.planner,bookingtype:" -msgid "Expense Splitbooking" -msgstr "Expense Splitbooking" - msgctxt "selection:cashbook.planner,bookingtype:" msgid "Transfer from" msgstr "Transfer from" @@ -234,6 +230,50 @@ msgctxt "selection:cashbook.planner,bookingtype:" msgid "Transfer to" msgstr "Transfer to" +msgctxt "field:cashbook.planner,currency_cashbook:" +msgid "Currency" +msgstr "Currency" + +msgctxt "help:cashbook.planner,currency_cashbook:" +msgid "Currency of Cashbook" +msgstr "Currency of Cashbook" + +msgctxt "field:cashbook.planner,amount:" +msgid "Amount" +msgstr "Amount" + +msgctxt "field:cashbook.planner,category:" +msgid "Category" +msgstr "Category" + +msgctxt "help:cashbook.planner,category:" +msgid "Category for the planned booking" +msgstr "Category for the planned booking" + +msgctxt "field:cashbook.planner,booktransf:" +msgid "Source/Dest" +msgstr "Source/Dest" + +msgctxt "field:cashbook.planner,owner_cashbook:" +msgid "Owner" +msgstr "Owner" + +msgctxt "field:cashbook.planner,state_cashbook:" +msgid "State of Cashbook" +msgstr "State of Cashbook" + +msgctxt "field:cashbook.planner,subject:" +msgid "Booking text" +msgstr "Booking text" + +msgctxt "field:cashbook.planner,wfcheck:" +msgid "Set to 'Checked'" +msgstr "Set to 'Checked'" + +msgctxt "help:cashbook.planner,wfcheck:" +msgid "Switches the booking to the 'Verified' state." +msgstr "Switches the booking to the 'Verified' state." + msgctxt "model:cashbook.planner.nextrun,name:" msgid "Next Execution Date" msgstr "Next Execution Date" diff --git a/planner.py b/planner.py index b7a34b2..2c038f1 100644 --- a/planner.py +++ b/planner.py @@ -3,6 +3,7 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. +from decimal import Decimal from datetime import date, timedelta from dateutil.rrule import ( rrule, YEARLY, MONTHLY, WEEKLY, DAILY, MO, TU, WE, TH, FR, SA, SU) @@ -11,9 +12,15 @@ from trytond.transaction import Transaction from trytond.pool import Pool from trytond.report import Report from trytond.pyson import Eval, Bool, If, And -from trytond.modules.cashbook.line import sel_bookingtype +from trytond.modules.currency.fields import Monetary +from trytond.modules.cashbook.book import sel_state_book +from trytond.modules.cashbook.line import sel_bookingtype as sel_bookingtype_cb +sel_bookingtype = [ + x for x in sel_bookingtype_cb if x[0] + in ['in', 'out', 'mvin', 'mvout']] + DEF_NONE = None SEL_FREQU = [ ('year', 'Yearly'), @@ -97,6 +104,48 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): bookingtype = fields.Selection( string='Type', selection=sel_bookingtype, required=True, help='Type of Booking') + currency_cashbook = fields.Function(fields.Many2One( + string='Currency', help='Currency of Cashbook', + model_name='currency.currency'), 'on_change_with_currency_cashbook') + amount = Monetary( + string='Amount', currency='currency_cashbook', + digits='currency_cashbook', required=True) + category = fields.Many2One( + string='Category', model_name='cashbook.category', + help='Category for the planned booking', depends=['bookingtype'], + states={ + 'required': Eval('bookingtype', '').in_(['in', 'out']), + 'invisible': ~Eval('bookingtype', '').in_(['in', 'out'])}) + party = fields.Many2One( + string='Party', model_name='party.party', depends=['bookingtype'], + states={ + 'required': Eval('bookingtype', '').in_(['in', 'out']), + 'invisible': ~Eval('bookingtype', '').in_(['in', 'out'])}) + booktransf = fields.Many2One( + string='Source/Dest', + ondelete='RESTRICT', model_name='cashbook.book', + domain=[ + ('owner.id', '=', Eval('owner_cashbook', -1)), + ('id', '!=', Eval('cashbook', -1)), + ('btype', '!=', None)], + states={ + 'readonly': Eval('state_cashbook', '') != 'open', + 'invisible': ~Eval('bookingtype', '').in_(['mvin', 'mvout']), + 'required': Eval('bookingtype', '').in_(['mvin', 'mvout'])}, + depends=[ + 'state_cashbook', 'bookingtype', 'owner_cashbook', 'cashbook']) + owner_cashbook = fields.Function(fields.Many2One( + string='Owner', readonly=True, + states={'invisible': True}, model_name='res.user'), + 'on_change_with_owner_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') + subject = fields.Text(string='Booking text', required=True) + wfcheck = fields.Boolean( + string="Set to 'Checked'", + help="Switches the booking to the 'Verified' state.") @classmethod def __setup__(cls): @@ -177,6 +226,19 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): break return result + @fields.depends('cashbook', '_parent_cashbook.currency') + def on_change_with_currency_cashbook(self, name=None): + """ get currency of selected cashbook + + Args: + name (str, optional): name of field. Defaults to None. + + Returns: + int: id of cashbook currency + """ + if self.cashbook: + return self.cashbook.currency.id + @fields.depends('nextrun') def on_change_with_nextrun_link(self, name=None): """ get nextrun-record if exist @@ -229,6 +291,30 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): 'setpos': self.setpos} )]) + @fields.depends('cashbook', '_parent_cashbook.owner') + def on_change_with_owner_cashbook(self, name=None): + """ get current owner + """ + if self.cashbook: + return self.cashbook.owner.id + + @fields.depends('cashbook', '_parent_cashbook.state') + def on_change_with_state_cashbook(self, name=None): + """ get state of cashbook + """ + if self.cashbook: + return self.cashbook.state + + @fields.depends('bookingtype', 'category', 'booktransf') + def on_change_bookingtype(self): + """ reset category/booktransf on change of bookingtype + """ + if self.bookingtype: + if self.bookingtype in ['in', 'out']: + self.booktransf = None + elif self.bookingtype in ['mvin', 'mvout']: + self.category = None + @fields.depends('frequ', 'setpos', 'weekday', 'monthday') def on_change_frequ(self): """ update fields @@ -254,6 +340,24 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): """ self.on_change_frequ() + @classmethod + def default_wfcheck(cls): + """ False as default for wf-state 'checked' + + Returns: + bool: False + """ + return False + + @classmethod + def default_amount(cls): + """ default for amount + + Returns: + Decimal: 0.00 + """ + return Decimal('0.0') + @classmethod def default_interval(cls): """ get default for interval @@ -387,8 +491,40 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): def run_booking(cls, records): """ do prepared booking """ + pool = Pool() + IrDate = pool.get('ir.date') + Line = pool.get('cashbook.line') + + to_create = [] + to_create_check = [] for record in records: - print('-- booking:', record.rec_name) + line = { + 'cashbook': record.cashbook.id, + 'bookingtype': record.bookingtype, + 'date': IrDate.today(), + 'amount': record.amount, + 'description': record.subject} + + if record.bookingtype in ['in', 'out']: + if record.category: + line['category'] = record.category.id + if record.party: + line['party'] = record.party.id + elif record.bookingtype in ['mvin', 'mvout']: + if record.booktransf: + line['booktransf'] = record.booktransf.id + + if record.wfcheck: + to_create_check.append(line) + else: + to_create.append(line) + + if to_create_check: + lines = Line.create(to_create_check) + Line.wfcheck(lines) + + if to_create: + Line.create(to_create) @classmethod def cronjob(cls): diff --git a/tests/planner.py b/tests/planner.py index 842c398..0d34dc2 100644 --- a/tests/planner.py +++ b/tests/planner.py @@ -3,6 +3,7 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. +from decimal import Decimal from unittest.mock import MagicMock from trytond.tests.test_tryton import with_transaction from trytond.pool import Pool @@ -19,6 +20,7 @@ class PlannerTestCase(object): pool = Pool() Book = pool.get('cashbook.book') Planner = pool.get('cashbook.planner') + Party = pool.get('party.party') types = self.prep_type() company = self.prep_company() @@ -36,11 +38,18 @@ class PlannerTestCase(object): }]) self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open') + category = self.prep_category() + party, = Party.create([{ + 'name': 'Party', + 'addresses': [('create', [{}])]}]) job, = Planner.create([{ 'cashbook': book.id, 'name': name, 'start_date': date(2022, 5, 1), - 'bookingtype': 'out'}]) + 'bookingtype': 'out', + 'category': category.id, + 'party': party.id, + 'subject': 'Booking text'}]) # check applied defaults self.assertEqual(job.rec_name, 'Job 1') self.assertEqual(job.start_date, date(2022, 5, 1)) @@ -286,4 +295,111 @@ class PlannerTestCase(object): IrDate.today = MagicMock(return_value=date.today()) + @with_transaction() + def test_planner_cronjobs_booking_with_category(self): + """ create job, configure booking with category, run job + """ + pool = Pool() + Planner = pool.get('cashbook.planner') + IrDate = pool.get('ir.date') + Category = pool.get('cashbook.category') + Cashbook = pool.get('cashbook.book') + + job = self.prep_create_job() + self.assertEqual( + job._compute_dates_by_rrule( + count=1, query_date=date(2022, 5, 1)), [ + date(2022, 5, 1)]) + + IrDate.today = MagicMock(return_value=date(2022, 5, 24)) + + category, = Category.search([('name', '=', 'Cat1')]) + Planner.write(*[ + [job], + { + 'name': 'Booking to category', + 'amount': Decimal('10.0'), + 'bookingtype': 'out', + 'category': category.id, + 'subject': 'booking ${month}/${year}, ${date}', + 'wfcheck': True, + }]) + self.assertEqual(job.rec_name, 'Booking to category') + self.assertEqual(job.cashbook.rec_name, 'Book 1 | 0.00 usd | Open') + self.assertEqual(len(job.cashbook.lines), 0) + + job, = Planner.search([]) + self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) + IrDate.today = MagicMock(return_value=date(2022, 6, 1)) + Planner.cronjob() + self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) + + # check cashbook + self.assertEqual(len(job.cashbook.lines), 1) + self.assertEqual( + job.cashbook.lines[0].rec_name, + "06/01/2022|Exp|-10.00 usd|booking " + + "${month}/${year}, ${date} [Cat1]") + self.assertEqual(job.cashbook.lines[0].state, 'check') + + with Transaction().set_context({'date': date(2022, 6, 1)}): + cashbook, = Cashbook.browse([job.cashbook]) + self.assertEqual(cashbook.rec_name, 'Book 1 | -10.00 usd | Open') + + IrDate.today = MagicMock(return_value=date.today()) + + @with_transaction() + def test_planner_cronjobs_booking_transfer_nonasset_eur(self): + """ create job, configure transfer-booking to non-asset-cashbook, + run job + """ + pool = Pool() + Planner = pool.get('cashbook.planner') + IrDate = pool.get('ir.date') + Category = pool.get('cashbook.category') + Cashbook = pool.get('cashbook.book') + + job = self.prep_create_job() + self.assertEqual( + job._compute_dates_by_rrule( + count=1, query_date=date(2022, 5, 1)), [ + date(2022, 5, 1)]) + + IrDate.today = MagicMock(return_value=date(2022, 5, 24)) + + category, = Category.search([('name', '=', 'Cat1')]) + Planner.write(*[ + [job], + { + 'name': 'Booking to category', + 'amount': Decimal('10.0'), + 'bookingtype': 'out', + 'category': category.id, + 'subject': 'booking ${month}/${year}, ${date}', + 'wfcheck': True, + }]) + self.assertEqual(job.rec_name, 'Booking to category') + self.assertEqual(job.cashbook.rec_name, 'Book 1 | 0.00 usd | Open') + self.assertEqual(len(job.cashbook.lines), 0) + + job, = Planner.search([]) + self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) + IrDate.today = MagicMock(return_value=date(2022, 6, 1)) + Planner.cronjob() + self.assertEqual(job.nextrun[0].date, date(2022, 7, 1)) + + # check cashbook + self.assertEqual(len(job.cashbook.lines), 1) + self.assertEqual( + job.cashbook.lines[0].rec_name, + "06/01/2022|Exp|-10.00 usd|booking " + + "${month}/${year}, ${date} [Cat1]") + self.assertEqual(job.cashbook.lines[0].state, 'check') + + with Transaction().set_context({'date': date(2022, 6, 1)}): + cashbook, = Cashbook.browse([job.cashbook]) + self.assertEqual(cashbook.rec_name, 'Book 1 | -10.00 usd | Open') + + IrDate.today = MagicMock(return_value=date.today()) + # end PlannerTestCase diff --git a/view/planner_form.xml b/view/planner_form.xml index ca4f209..cc1ebbc 100644 --- a/view/planner_form.xml +++ b/view/planner_form.xml @@ -41,6 +41,23 @@ full copyright notices and license terms. --> diff --git a/view/planner_list.xml b/view/planner_list.xml index 3da7230..e351a37 100644 --- a/view/planner_list.xml +++ b/view/planner_list.xml @@ -8,4 +8,8 @@ full copyright notices and license terms. --> + + + +