diff --git a/locale/de.po b/locale/de.po index 87af133..9d2a64b 100644 --- a/locale/de.po +++ b/locale/de.po @@ -3,6 +3,18 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" +############## +# ir.message # +############## +msgctxt "model:ir.message,text:msg_title_notify" +msgid "Cashbook Scheduled Booking" +msgstr "Kassenbuch geplante Buchung" + +msgctxt "model:ir.message,text:msg_text_notify" +msgid "The following transaction was generated: %(bname)s" +msgstr "Die folgende Buchung wurde erzeugt: %(bname)s" + + ########### # ir.cron # ########### @@ -35,6 +47,18 @@ msgid "Scheduled Bookings" msgstr "geplante Buchungen" +################### +# ir.model.button # +################### +msgctxt "model:ir.model.button,string:book_now_button" +msgid "Execute Booking Now" +msgstr "Buchung jetzt ausführen" + +msgctxt "model:ir.model.button,help:book_now_button" +msgid "The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution." +msgstr "Die geplante Buchung wird vorgezogen und jetzt ausgeführt. Die nächste Buchung wird dann regulär für die nachfolgende Ausführung geplant." + + ################# # ir.rule.group # ################# @@ -366,6 +390,22 @@ msgctxt "help:cashbook.planner,move_event:" msgid "If the date of execution falls on a weekend or holiday, it can be moved to a business day." msgstr "Wenn das Datum der Ausführung auf ein Wochenende oder Feiertag fällt, kann es auf einen Geschäftstag verschoben werden." +msgctxt "field:cashbook.planner,last_day_of_month:" +msgid "Last day of the month" +msgstr "letzer Tag des Monats" + +msgctxt "help:cashbook.planner,last_day_of_month:" +msgid "The booking is made on the last day of the month." +msgstr "Die Buchung wird am letzten Tag des Monats ausgeführt." + +msgctxt "field:cashbook.planner,notify_bycron:" +msgid "Notify" +msgstr "Benachrichtigen" + +msgctxt "help:cashbook.planner,notify_bycron:" +msgid "A notification will appear in the web browser when the booking has been created." +msgstr "Es wird eine Benachrichtigung im Webbrowser angezeigt wenn die Buchung erstellt wurde." + ############################ # cashbook.planner.nextrun # diff --git a/locale/en.po b/locale/en.po index cb63fac..2a86165 100644 --- a/locale/en.po +++ b/locale/en.po @@ -2,6 +2,14 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" +msgctxt "model:ir.message,text:msg_title_notify" +msgid "Cashbook Scheduled Booking" +msgstr "Cashbook Scheduled Booking" + +msgctxt "model:ir.message,text:msg_text_notify" +msgid "The following transaction was generated: %(bname)s" +msgstr "The following transaction was generated: %(bname)s" + msgctxt "selection:ir.cron,method:" msgid "Execute scheduled bookings" msgstr "Execute scheduled bookings" @@ -18,6 +26,14 @@ msgctxt "model:ir.action,name:act_planner_view" msgid "Scheduled Bookings" msgstr "Scheduled Bookings" +msgctxt "model:ir.model.button,string:book_now_button" +msgid "Execute Booking Now" +msgstr "Execute Booking Now" + +msgctxt "model:ir.model.button,help:book_now_button" +msgid "The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution." +msgstr "The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution." + msgctxt "model:ir.rule.group,name:rg_planner_admin" msgid "Administrators: scheduled bookings read/write" msgstr "Administrators: scheduled bookings read/write" @@ -374,3 +390,18 @@ msgctxt "view:cashbook.line:" msgid "Scheduled Bookings" msgstr "Scheduled Bookings" +msgctxt "field:cashbook.planner,last_day_of_month:" +msgid "Last day of the month" +msgstr "Last day of the month" + +msgctxt "help:cashbook.planner,last_day_of_month:" +msgid "The booking is made on the last day of the month." +msgstr "The booking is made on the last day of the month." + +msgctxt "field:cashbook.planner,notify_bycron:" +msgid "Notify" +msgstr "Notify" + +msgctxt "help:cashbook.planner,notify_bycron:" +msgid "A notification will appear in the web browser when the booking has been created." +msgstr "A notification will appear in the web browser when the booking has been created." diff --git a/message.xml b/message.xml new file mode 100644 index 0000000..7686921 --- /dev/null +++ b/message.xml @@ -0,0 +1,16 @@ + + + + + + + Cashbook Scheduled Booking + + + The following transaction was generated: %(bname)s + + + + diff --git a/planner.py b/planner.py index 547de8e..2435007 100644 --- a/planner.py +++ b/planner.py @@ -14,6 +14,7 @@ from trytond.pool import Pool from trytond.report import Report from trytond.i18n import gettext from trytond.pyson import Eval, Bool, If, And +from trytond.bus import notify 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 @@ -91,7 +92,14 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): [('monthday', '>=', 1), ('monthday', '<=', 31)], ('monthday', '=', None))], depends=['weekday', 'frequ'], - states={'required': COND_MONTHDAY, 'invisible': ~COND_MONTHDAY}) + states={ + 'required': And(COND_MONTHDAY, ~Eval('last_day_of_month', False)), + 'invisible': ~And( + COND_MONTHDAY, ~Eval('last_day_of_month', False))}) + last_day_of_month = fields.Boolean( + string='Last day of the month', depends=['weekday', 'frequ'], + help='The booking is made on the last day of the month.', + states={'invisible': ~COND_MONTHDAY}) interval = fields.Integer( string='Interval', required=True, help='Select an interval to run the rule on every n-th date.', @@ -110,6 +118,9 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): string='If no business day', required=True, selection=SEL_MOVE_EVENT, help='If the date of execution falls on a weekend or holiday, ' + 'it can be moved to a business day.') + notify_bycron = fields.Boolean( + string='Notify', help='A notification will appear in the web ' + + 'browser when the booking has been created.') bookingtype = fields.Selection( string='Type', selection=sel_bookingtype, required=True, @@ -180,8 +191,10 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): Index( t, (t.end_date, Index.Range(order='ASC')), - where=t.end_date != DEF_NONE), - }) + where=t.end_date != DEF_NONE)}) + cls._buttons.update({ + 'booknow': {'readonly': ~Eval('active', False)}, + }) def get_rec_name(self, name=None): """ get formatted name of record @@ -263,6 +276,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): count = 5 count = 1 if count < 1 else 100 if count > 100 else count + last_day_of_month = params.get( + 'last_day_of_month', self.last_day_of_month) end_date = params.get('end_date', self.end_date) frequ = pfrequ[params.get('frequ', self.frequ)] @@ -283,6 +298,18 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): interval = 1 interval = 1 if interval < 1 else 10 if interval > 10 else interval + # last-day-of-month: set date short before end of month, + # then compute move result to end of month + updt_lastday = False + if last_day_of_month and (frequ == MONTHLY) and not pweekday: + monthday = 28 + updt_lastday = True + + lastday_valid = last_day_of_month and ( + frequ == MONTHLY) and (pweekday is None) + assert (lastday_valid or not last_day_of_month), \ + ('last-day-of-month can only be used with frequ=month ' + + 'and weekday=99.') assert (monthday is None) or (pweekday is None), \ "weekday and monthday cannot be used together" @@ -297,7 +324,12 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): for x in dtrule: if (query_date and (x.date() >= query_date)) or \ (query_date is None): - x_date = get_moved_date(x.date(), move_event) + x_date = x.date() + if updt_lastday: + x_date = ( + (x_date + timedelta(days=5)).replace(day=1) - + timedelta(days=1)) + x_date = get_moved_date(x_date, move_event) # if date was re-arranged backwards and we are before # query_date - skip it @@ -368,7 +400,7 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): @fields.depends( 'start_date', 'end_date', 'frequ', 'weekday', 'monthday', - 'interval', 'setpos', 'move_event') + 'interval', 'setpos', 'move_event', 'last_day_of_month') def on_change_with_nextdates(self, name=None): """ Calculates the next 5 appointments based on the configured rule, returns a formatted date list @@ -401,7 +433,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): 'weekday': self.weekday, 'monthday': self.monthday, 'interval': self.interval, - 'setpos': self.setpos} + 'setpos': self.setpos, + 'last_day_of_month': self.last_day_of_month} )]) @fields.depends('cashbook', '_parent_cashbook.owner') @@ -428,26 +461,33 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): elif self.bookingtype in ['mvin', 'mvout']: self.category = None - @fields.depends('frequ', 'setpos', 'weekday', 'monthday') + @fields.depends( + 'frequ', 'setpos', 'weekday', 'monthday', 'last_day_of_month') def on_change_frequ(self): """ update fields """ if self.frequ and self.frequ == 'month': if self.weekday: if self.weekday == '99': - if self.monthday is None: - self.monthday = 1 + if self.last_day_of_month: + self.monthday = None + else: + if self.monthday is None: + self.monthday = 1 self.setpos = None else: if self.setpos is None: self.setpos = 1 self.monthday = None + self.last_day_of_month = False else: self.setpos = None self.monthday = None self.weekday = '99' + self.last_day_of_month = False - @fields.depends('frequ', 'setpos', 'weekday', 'monthday') + @fields.depends( + 'frequ', 'setpos', 'weekday', 'monthday', 'last_day_of_month') def on_change_weekday(self): """ clear day-of-month if weekday is used """ @@ -546,6 +586,15 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): """ return 1 + @classmethod + def default_last_day_of_month(cls): + """ get default for last-day-of-month + + Returns: + boolean: False + """ + return False + @classmethod def default_frequ(cls): """ get default for frequency @@ -569,6 +618,15 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): IrDate = Pool().get('ir.date') return IrDate.today() + @classmethod + def default_notify_bycron(cls): + """ get False as default + + Returns: + boolean: False + """ + return False + @classmethod def fill_placeholder(cls, linedata): """ replace placeholder in description @@ -687,6 +745,20 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): if to_write: NextRun.write(*to_write) + @classmethod + @ModelView.button + def booknow(cls, records): + """ run planned booking now + """ + to_work = [x for x in records if x.active and x.nextrun_date] + cls.run_booking(to_work) + + for record in to_work: + if record.active: + cls.update_next_occurence( + [record], + query_date=record.nextrun_date + timedelta(days=1)) + @classmethod def create(cls, vlist): """ update nextrun-records on create of planner-records @@ -784,11 +856,27 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): else: to_create.append(line) + to_notify = [] if to_create_check: lines = Line.create(to_create_check) Line.wfcheck(lines) + to_notify.extend([ + x for x in lines + if x.planners[0].notify_bycron]) + if to_create: - Line.create(to_create) + lines = Line.create(to_create) + to_notify.extend([ + x for x in lines + if x.planners[0].notify_bycron]) + + for line in to_notify: + notify( + title=gettext('cashbook_planner.msg_title_notify'), + body=gettext( + 'cashbook_planner.msg_text_notify', + bname=line.rec_name), + user=line.cashbook.owner.id) @classmethod def cronjob(cls): diff --git a/planner.xml b/planner.xml index 5146fe1..df428fc 100644 --- a/planner.xml +++ b/planner.xml @@ -157,5 +157,17 @@ full copyright notices and license terms. --> + + + booknow + Execute Booking Now + The planned booking is brought forward and executed now. The next posting is then scheduled regularly for the following execution. + + + + + + + diff --git a/tests/planner.py b/tests/planner.py index 2bd39e0..29271f5 100644 --- a/tests/planner.py +++ b/tests/planner.py @@ -245,6 +245,17 @@ class PlannerTestCase(object): [date(2022, 5, 11), date(2022, 6, 8), date(2022, 7, 13), date(2022, 8, 10), date(2022, 9, 14), date(2022, 10, 12)]) + # last day of month + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 5, 1), count=6, + params={ + 'weekday': '99', 'end_date': None, 'frequ': 'month', + 'interval': 1, 'setpos': None, 'monthday': None, + 'last_day_of_month': True}), + [date(2022, 5, 31), date(2022, 6, 30), date(2022, 7, 31), + date(2022, 8, 31), date(2022, 9, 30), date(2022, 10, 31)]) + # set up holidays cfg1 = Config( holidays='01-01;05-01;easter:+1;easter:-2;ascension;whitsun:+1') @@ -377,6 +388,23 @@ class PlannerTestCase(object): 'setpos': 5, 'monthday': None, 'weekday': '2', 'end_date': None}]) + @with_transaction() + def test_planner_run_booking_now(self): + """ create job, press button 'booknow' + """ + Planner = Pool().get('cashbook.planner') + + job = self.prep_create_job() + self.assertEqual( + job.rec_name, "Job 1|Book 1|Exp|Cat1|05/01/2022|usd0.00") + self.assertEqual( + job._compute_dates_by_rrule( + count=1, query_date=date(2022, 5, 1)), [ + date(2022, 5, 1)]) + Planner.booknow([job]) + self.assertEqual( + job.rec_name, "Job 1|Book 1|Exp|Cat1|06/01/2022|usd0.00") + @with_transaction() def test_planner_create_update_nextrun(self): """ create job, check nextrun-record diff --git a/tryton.cfg b/tryton.cfg index 24da6fd..c51caaf 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -5,6 +5,7 @@ depends: extras_depend: cashbook_investment xml: + message.xml group.xml planner.xml nextrun.xml diff --git a/view/planner_form.xml b/view/planner_form.xml index 86e826f..d7b8fbb 100644 --- a/view/planner_form.xml +++ b/view/planner_form.xml @@ -12,7 +12,8 @@ full copyright notices and license terms. -->