[FIX] project: fix infinite recurring tasks creation

for repeat_type = 'forever' recurrences.

Consider a recurrence creating a task every 15th day of the month.
When the cron runs, it updates the next recurrence dates of the
recurrence.

But currently, the updated "next_recurrence_date" can be set to an
earlier date than today, if the fixed day of the recurring task is
earlier than the current day.

This means that the recurring task to create every 15th of the month
will be created the 15, the 16, the 17, the 18, ... until the next month
begins (no task will be created when the cron runs before the 15th of a
month).

This commit enforces the next recurrence_date to be set after the
current cron run, to ensure a task isn't wrongly created multiple times.

This commit also includes the tests that led to the discovery of this
bug.

closes odoo/odoo#63989

X-original-commit: e4943afef0f66703bafd78780764ff6f0b039d3e
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
Victor Feyens
2021-01-04 10:19:30 +00:00
parent 5fc28342a2
commit bc2a8dd04d
2 changed files with 107 additions and 1 deletions
@@ -182,6 +182,9 @@ class ProjectTaskRecurrence(models.Model):
count = delta.years * 12 + delta.months
for i in range(count):
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)
dates.append(start)
start += relativedelta(months=repeat_interval)
return dates
+104 -1
View File
@@ -56,7 +56,7 @@ class TestProjectrecurrence(TransactionCase):
task.recurring_task = False
self.assertFalse(bool(task.recurrence_id), 'the recurrence should be deleted')
def test_recurrence_cron(self):
def test_recurrence_cron_repeat_after(self):
domain = [('project_id', '=', self.project_recurring.id)]
with freeze_time("2020-01-01"):
form = Form(self.env['project.task'])
@@ -105,6 +105,109 @@ class TestProjectrecurrence(TransactionCase):
for f in self.env['project.task.recurrence']._get_recurring_fields():
self.assertTrue(tasks[0][f] == tasks[1][f] == tasks[2][f], "Field %s should have been copied" % f)
def test_recurrence_cron_repeat_until(self):
domain = [('project_id', '=', self.project_recurring.id)]
with freeze_time("2020-01-01"):
form = Form(self.env['project.task'])
form.name = 'test recurring task'
form.description = 'my super recurring task bla bla bla'
form.project_id = self.project_recurring
form.date_deadline = datetime(2020, 2, 1)
form.recurring_task = True
form.repeat_interval = 1
form.repeat_unit = 'month'
form.repeat_type = 'until'
form.repeat_until = date(2020, 2, 20)
form.repeat_on_month = 'date'
form.repeat_day = '15'
task = form.save()
task.planned_hours = 2
self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 1, 15))
self.assertEqual(self.env['project.task'].search_count(domain), 1)
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 1, 'no extra task should be created')
with freeze_time("2020-01-15"):
self.assertEqual(self.env['project.task'].search_count(domain), 1)
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 2)
with freeze_time("2020-02-15"):
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 3)
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 3)
tasks = self.env['project.task'].search(domain)
self.assertEqual(len(tasks), 3)
self.assertTrue(bool(tasks[2].date_deadline))
self.assertFalse(tasks[1].date_deadline, "Deadline should not be copied")
for f in self.env['project.task.recurrence']._get_recurring_fields():
self.assertTrue(tasks[0][f] == tasks[1][f] == tasks[2][f], "Field %s should have been copied" % f)
def test_recurrence_cron_repeat_forever(self):
domain = [('project_id', '=', self.project_recurring.id)]
with freeze_time("2020-01-01"):
form = Form(self.env['project.task'])
form.name = 'test recurring task'
form.description = 'my super recurring task bla bla bla'
form.project_id = self.project_recurring
form.date_deadline = datetime(2020, 2, 1)
form.recurring_task = True
form.repeat_interval = 1
form.repeat_unit = 'month'
form.repeat_type = 'forever'
form.repeat_on_month = 'date'
form.repeat_day = '15'
task = form.save()
task.planned_hours = 2
self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 1, 15))
self.assertEqual(self.env['project.task'].search_count(domain), 1)
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 1, 'no extra task should be created')
with freeze_time("2020-01-15"):
self.assertEqual(self.env['project.task'].search_count(domain), 1)
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 2)
with freeze_time("2020-02-15"):
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 3)
with freeze_time("2020-02-16"):
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 3)
with freeze_time("2020-02-17"):
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 3)
with freeze_time("2020-02-17"):
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 3)
with freeze_time("2020-03-15"):
self.env['project.task.recurrence']._cron_create_recurring_tasks()
self.assertEqual(self.env['project.task'].search_count(domain), 4)
tasks = self.env['project.task'].search(domain)
self.assertEqual(len(tasks), 4)
self.assertTrue(bool(tasks[3].date_deadline))
self.assertFalse(tasks[1].date_deadline, "Deadline should not be copied")
for f in self.env['project.task.recurrence']._get_recurring_fields():
self.assertTrue(
tasks[0][f] == tasks[1][f] == tasks[2][f] == tasks[3][f],
"Field %s should have been copied" % f)
def test_recurrence_update_task(self):
with freeze_time("2020-01-01"):
task = self.env['project.task'].create({