From bc2a8dd04db716c8a0b5808cde266b47536a3e58 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 17 Dec 2020 11:16:36 +0000 Subject: [PATCH] [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) Signed-off-by: Victor Feyens (vfe) --- .../project/models/project_task_recurrence.py | 3 + .../project/tests/test_project_recurrence.py | 105 +++++++++++++++++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/addons/project/models/project_task_recurrence.py b/addons/project/models/project_task_recurrence.py index 7d23669ad64..94095cdadde 100644 --- a/addons/project/models/project_task_recurrence.py +++ b/addons/project/models/project_task_recurrence.py @@ -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 diff --git a/addons/project/tests/test_project_recurrence.py b/addons/project/tests/test_project_recurrence.py index 1ba1d961f0d..b37ebd2ebf1 100644 --- a/addons/project/tests/test_project_recurrence.py +++ b/addons/project/tests/test_project_recurrence.py @@ -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({