[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) <megu@odoo.com>
Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
MerlinGuillaume
2022-12-14 07:49:06 +01:00
parent ab3497b889
commit ed2a9c3a78
3 changed files with 74 additions and 12 deletions
+1 -1
View File
@@ -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
@@ -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
@@ -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),