From 9685670672c7d408c8a1e2a09887687c6adef6da Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Mon, 3 Jun 2024 23:37:26 +0200 Subject: [PATCH] Add option to rule to move date of occurence to working day --- planner.py | 46 ++++++++++++++++++++++++++++++++++++++++--- tests/planner.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/planner.py b/planner.py index 1acfadd..547de8e 100644 --- a/planner.py +++ b/planner.py @@ -217,11 +217,42 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): used instead of the stored values, Defaults to {}, allowed: frequ, weekday, start_date, end_date (preferred over 'count'), - monthday, interval, setpos + monthday, interval, setpos, move_event Returns: list: date values, result of rrlue """ + def get_moved_date(xdate, m_mode): + """ re-arrange xdate to a working day + + Args: + xdate (date): date to move to a working day + move_mode (str): move mode: + nop - no operation + after/before - move date to after/before input date + + Returns: + date: re-arranged date + """ + Config = Pool().get('cashbook.configuration') + config = Config.get_singleton() + + assert m_mode in ['nop', 'after', 'before'], 'invalid move_mode' + + if (not config) or (m_mode == 'nop'): + return xdate + + holidays = config.holiday_dates([xdate.year, xdate.year + 1]) + day_cnt = ( + 1 if m_mode == 'after' + else -1 if m_mode == 'before' else 0) + + if day_cnt != 0: + while (xdate in holidays) or (xdate.weekday() in [5, 6]): + # re-arrange + xdate = xdate + timedelta(days=day_cnt) + return xdate + pfrequ = { 'year': YEARLY, 'month': MONTHLY, 'week': WEEKLY, 'day': DAILY} pweekday = { @@ -235,6 +266,10 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): end_date = params.get('end_date', self.end_date) frequ = pfrequ[params.get('frequ', self.frequ)] + move_event = params.get('move_event', self.move_event) + if move_event not in ['nop', 'before', 'after']: + move_event = 'nop' + setpos = params.get('setpos', self.setpos) if setpos is not None: setpos = 1 if setpos < 1 else 4 if setpos > 4 else setpos @@ -262,7 +297,12 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): for x in dtrule: if (query_date and (x.date() >= query_date)) or \ (query_date is None): - result.append(x.date()) + x_date = get_moved_date(x.date(), move_event) + + # if date was re-arranged backwards and we are before + # query_date - skip it + if x_date >= query_date: + result.append(x_date) if len(result) >= count: break return result @@ -328,7 +368,7 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): @fields.depends( 'start_date', 'end_date', 'frequ', 'weekday', 'monthday', - 'interval', 'setpos') + 'interval', 'setpos', 'move_event') def on_change_with_nextdates(self, name=None): """ Calculates the next 5 appointments based on the configured rule, returns a formatted date list diff --git a/tests/planner.py b/tests/planner.py index c15066d..2bd39e0 100644 --- a/tests/planner.py +++ b/tests/planner.py @@ -170,7 +170,9 @@ class PlannerTestCase(object): def test_planner_create_job(self): """ create job, check rule + constraints """ - Planner = Pool().get('cashbook.planner') + pool = Pool() + Planner = pool.get('cashbook.planner') + Config = pool.get('cashbook.configuration') job = self.prep_create_job() self.assertEqual( @@ -243,6 +245,53 @@ 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)]) + # set up holidays + cfg1 = Config( + holidays='01-01;05-01;easter:+1;easter:-2;ascension;whitsun:+1') + cfg1.save() + # 1st of may, should be moved to 2nd of may + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 25), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 5, 1), + 'move_event': 'after', 'weekday': None, + 'setpos': None, 'interval': 1, 'frequ': 'year', + 'monthday': None}), + [date(2022, 5, 2), date(2023, 5, 2), date(2024, 5, 2)]) + # easter of 2022, occurence-date moved to tuesday after easter'22 + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 10), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 4, 17), + 'move_event': 'after', 'weekday': None, + 'setpos': None, + 'interval': 1, 'frequ': 'month', 'monthday': None}), + [date(2022, 4, 19), date(2022, 5, 17), date(2022, 6, 17)]) + # easter of 2022, monthly, occurence-date moved to + # thursday before easter'22 + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 10), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 4, 17), + 'move_event': 'before', 'weekday': None, + 'setpos': None, + 'interval': 1, 'frequ': 'month', 'monthday': None}), + [date(2022, 4, 14), date(2022, 5, 17), date(2022, 6, 17)]) + # easter of 2022, monthly, check next occurence after easter + # recompute date at moved occurence-date+1 + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 15), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 4, 17), + 'move_event': 'before', 'weekday': None, + 'setpos': None, + 'interval': 1, 'frequ': 'month', 'monthday': None}), + [date(2022, 5, 17), date(2022, 6, 17), date(2022, 7, 15)]) + Planner.write(*[[job], { 'frequ': 'year', 'start_date': date(2022, 5, 1), 'setpos': None, 'monthday': None, 'interval': 1,