Skip to content
Snippets Groups Projects
Commit 232d5c61 authored by MerlinGuillaume's avatar MerlinGuillaume
Browse files

[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#107817

X-original-commit: f94dbfe0
Signed-off-by: default avatarLaurent Stukkens (ltu) <ltu@odoo.com>
Signed-off-by: default avatarGuillaume Merlin (megu) <megu@odoo.com>
parent 1f496609
Branches
Tags
No related merge requests found
......@@ -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),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment