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),