From 3ed2216cbf59166ad6d6c4a3d4cd176607f4bfc3 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. -->
+
+