From 181f7405d6095cda29227cd39f97a54001ad4fad Mon Sep 17 00:00:00 2001 From: =Frederik Jaeckel <=frederik.jaeckel@m-ds.de> Date: Fri, 27 Sep 2024 23:24:48 +0200 Subject: [PATCH] recurrence: add last-day-of-month --- locale/de.po | 8 ++++++ locale/en.po | 7 +++++ planner.py | 59 +++++++++++++++++++++++++++++++++++++------ tests/planner.py | 11 ++++++++ view/planner_form.xml | 2 ++ 5 files changed, 79 insertions(+), 8 deletions(-) diff --git a/locale/de.po b/locale/de.po index 87af133..4720fa4 100644 --- a/locale/de.po +++ b/locale/de.po @@ -366,6 +366,14 @@ 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." + ############################ # cashbook.planner.nextrun # diff --git a/locale/en.po b/locale/en.po index cb63fac..ea5519a 100644 --- a/locale/en.po +++ b/locale/en.po @@ -374,3 +374,10 @@ 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." diff --git a/planner.py b/planner.py index 547de8e..3bfc436 100644 --- a/planner.py +++ b/planner.py @@ -91,7 +91,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.', @@ -263,6 +270,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 +292,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 +318,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 +394,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 +427,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 +455,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 +580,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 diff --git a/tests/planner.py b/tests/planner.py index 2bd39e0..23e20f4 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') diff --git a/view/planner_form.xml b/view/planner_form.xml index 86e826f..f9d6547 100644 --- a/view/planner_form.xml +++ b/view/planner_form.xml @@ -34,6 +34,8 @@ full copyright notices and license terms. -->