From ed2a9c3a789d7a337e0b7433adf6792949feaffd Mon Sep 17 00:00:00 2001 From: MerlinGuillaume Date: Fri, 25 Nov 2022 15:51:39 +0000 Subject: [PATCH] [FIX] project: recurring dates of monthly repeated task until date Computing the recurrences of a task repeating every X months until a certain date generates too much dates Steps to reproduce: 1. Install Project 2. Go to Settings > Project > Tasks Management and enable Recurring Tasks 3. Open any project in the Project app and create a new task then edit it 4. Enable the Recurrent field of the task 5. In the Recurrence tab, edit: - Repeat Every: 6 Months - Until: End Date: one year from now 6. The recurrence message says there are 11 tasks but there should only be 2 Solution: Generate the recurrences until the `repeat_until` date is reached if the `repeat_type` is 'until', otherwise generate as much recurrences as the count Also relax the constraint on the `repeat_day` and `repeat_until` to not raise an error if `repeat_until` is the last day of the month Problem: The recurrence of a task with `repeat_unit` month and `repeat_interval` different than 1 with a `repeat_type` until creates too much tasks, exceeding the `repeat_until` opw-3076593 closes odoo/odoo#107910 X-original-commit: 236b383fd9d1ad6a5d39d8c4232b49864ec19b37 Signed-off-by: Guillaume Merlin (megu) Signed-off-by: Laurent Stukkens (ltu) --- addons/project/i18n/project.pot | 2 +- .../project/models/project_task_recurrence.py | 21 +++---- .../project/tests/test_project_recurrence.py | 63 +++++++++++++++++++ 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/addons/project/i18n/project.pot b/addons/project/i18n/project.pot index 7029e2d932d..71387654deb 100644 --- a/addons/project/i18n/project.pot +++ b/addons/project/i18n/project.pot @@ -5137,7 +5137,7 @@ msgstr "" #. module: project #: code:addons/project/models/project_task_recurrence.py:0 #, python-format -msgid "The end date should be after the day of the month" +msgid "The end date should be after the day of the month or the last day of the month" msgstr "" #. module: project diff --git a/addons/project/models/project_task_recurrence.py b/addons/project/models/project_task_recurrence.py index fe5b3737a12..4bc8ab5c509 100644 --- a/addons/project/models/project_task_recurrence.py +++ b/addons/project/models/project_task_recurrence.py @@ -8,8 +8,6 @@ from calendar import monthrange from dateutil.relativedelta import relativedelta from dateutil.rrule import rrule, rruleset, DAILY, WEEKLY, MONTHLY, YEARLY, MO, TU, WE, TH, FR, SA, SU -import math - MONTHS = { 'january': 31, 'february': 28, @@ -140,8 +138,9 @@ class ProjectTaskRecurrence(models.Model): @api.constrains('repeat_unit', 'repeat_on_month', 'repeat_day', 'repeat_type', 'repeat_until') def _check_repeat_until_month(self): - if self.filtered(lambda r: r.repeat_type == 'until' and r.repeat_unit == 'month' and r.repeat_until and r.repeat_on_month == 'date' and int(r.repeat_day) > r.repeat_until.day): - raise ValidationError(_('The end date should be after the day of the month')) + if self.filtered(lambda r: r.repeat_type == 'until' and r.repeat_unit == 'month' and r.repeat_until and r.repeat_on_month == 'date' + and int(r.repeat_day) > r.repeat_until.day and monthrange(r.repeat_until.year, r.repeat_until.month)[1] != r.repeat_until.day): + raise ValidationError(_('The end date should be after the day of the month or the last day of the month')) @api.model def _get_recurring_fields(self): @@ -179,16 +178,16 @@ class ProjectTaskRecurrence(models.Model): rrule_kwargs['freq'] = MONTHLY if repeat_on_month == 'date': start = date_start - relativedelta(days=1) - if repeat_type == 'until' and repeat_until > date_start: - delta = relativedelta(repeat_until, date_start) - count = math.floor((delta.years * 12 + delta.months) / repeat_interval) - for i in range(count): + start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) + if start < date_start: + # Ensure the next recurrence is in the future + start += relativedelta(months=repeat_interval) start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) - if i == 0 and start < date_start: - # Ensure the next recurrence is in the future - start += relativedelta(months=repeat_interval) + can_generate_date = (lambda: start <= repeat_until) if repeat_type == 'until' else (lambda: len(dates) < count) + while can_generate_date(): dates.append(start) start += relativedelta(months=repeat_interval) + start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) return dates elif repeat_unit == 'year': rrule_kwargs['freq'] = YEARLY diff --git a/addons/project/tests/test_project_recurrence.py b/addons/project/tests/test_project_recurrence.py index 216673e7360..cde7d0eb537 100644 --- a/addons/project/tests/test_project_recurrence.py +++ b/addons/project/tests/test_project_recurrence.py @@ -455,6 +455,69 @@ class TestProjectrecurrence(TransactionCase): self.assertEqual(dates[0], datetime(2020, 7, 3)) self.assertEqual(dates[1], datetime(2021, 1, 3)) + # Should generate a date at the last day of the current month + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2022, 2, 26), + repeat_interval=1, + repeat_unit='month', + repeat_type='until', + repeat_until=date(2022, 2, 28), + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(len(dates), 1) + self.assertEqual(dates[0], date(2022, 2, 28)) + + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2022, 11, 26), + repeat_interval=3, + repeat_unit='month', + repeat_type='until', + repeat_until=date(2024, 2, 29), + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=25, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(len(dates), 5) + self.assertEqual(dates[0], date(2023, 2, 25)) + self.assertEqual(dates[1], date(2023, 5, 25)) + self.assertEqual(dates[2], date(2023, 8, 25)) + self.assertEqual(dates[3], date(2023, 11, 25)) + self.assertEqual(dates[4], date(2024, 2, 25)) + + # Use the exact same parameters than the previous test but with a repeat_day that is not passed yet + # So we generate an additional date in the current month + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2022, 11, 26), + repeat_interval=3, + repeat_unit='month', + repeat_type='until', + repeat_until=date(2024, 2, 29), + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(len(dates), 6) + self.assertEqual(dates[0], date(2022, 11, 30)) + self.assertEqual(dates[1], date(2023, 2, 28)) + self.assertEqual(dates[2], date(2023, 5, 31)) + self.assertEqual(dates[3], date(2023, 8, 31)) + self.assertEqual(dates[4], date(2023, 11, 30)) + self.assertEqual(dates[5], date(2024, 2, 29)) + def test_recurrence_next_dates_year(self): dates = self.env['project.task.recurrence']._get_next_recurring_dates( date_start=date(2020, 12, 1),