From 3b426db10a1d832a716d3daad4e8bd3a25aa588a Mon Sep 17 00:00:00 2001 From: Thibault Libioulle Date: Tue, 25 May 2021 11:21:52 +0200 Subject: [PATCH 1/2] [REV] project: re-allow recurring sub tasks With the modification introducted in the PR #62577, create recurring subtask was no more allowed. This commits reverts this limitation and adds tests to verify this behaviour. PR : #77126 task-2522076 X-original-commit: f6b12588757bd7f2d7802264fc7cd1aaae4c6c57 --- addons/project/data/project_demo.xml | 94 +++++++++++++++++++ addons/project/models/project.py | 5 +- .../project/tests/test_project_recurrence.py | 68 ++++++++++++++ addons/project/views/project_views.xml | 2 +- 4 files changed, 166 insertions(+), 3 deletions(-) diff --git a/addons/project/data/project_demo.xml b/addons/project/data/project_demo.xml index 2bba5e2e0a3..aaa602f29aa 100644 --- a/addons/project/data/project_demo.xml +++ b/addons/project/data/project_demo.xml @@ -7,6 +7,11 @@ + + + + + Bug @@ -875,5 +880,94 @@ Send it ASAP, its urgent. at_risk + + + + 3 + month + date + after + 4 + 1 + mon + + + + + + + 20 + + + + + + after + 20 + + + + + + 20 + 20.0 + + + Customer review + + + + + + + 20 + 0.25 + + + + Daily stand-up meeting - Send minutes + + + + + + + 20 + 8.0 + + + + Customer Meeting + + + + 10 + 2.0 + + + + Daily Meetings summary + + + + 20 + 2.0 + + + + Preparation + + + + 30 + 2.0 + + + + Minutes + + + + diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 6be16a3c627..c9828112dd2 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -1419,8 +1419,9 @@ class Task(models.Model): @api.constrains('recurring_task') def _check_recurring_task(self): - if self.filtered(lambda task: task.parent_id and task.recurring_task): - raise ValidationError(_("A sub-task cannot be a recurring task. Please consider making its parent task, a recurring task.")) + # Deprecated method + # TODO : Remove me in master + return @api.returns('self', lambda value: value.id) def copy(self, default=None): diff --git a/addons/project/tests/test_project_recurrence.py b/addons/project/tests/test_project_recurrence.py index 561bd17447b..6f7c2470fa4 100644 --- a/addons/project/tests/test_project_recurrence.py +++ b/addons/project/tests/test_project_recurrence.py @@ -458,3 +458,71 @@ class TestProjectrecurrence(TransactionCase): self.assertEqual(dates[2], datetime(2023, 11, 30)) self.assertEqual(dates[3], datetime(2024, 11, 30)) self.assertEqual(dates[4], datetime(2025, 11, 30)) + + def test_recurrence_cron_repeat_after_subtasks(self): + + def get_task_and_subtask_counts(domain): + tasks = self.env['project.task'].search(domain) + return len(tasks), len(tasks.filtered('parent_id')) + + parent_task = self.env['project.task'].create({ + 'name': 'Parent Task', + 'project_id': self.project_recurring.id + }) + domain = [('project_id', '=', self.project_recurring.id)] + with Form(parent_task.with_context({'tracking_disable': True})) as task_form: + with task_form.child_ids.new() as subtask_form: + subtask_form.name = 'Test Subtask 1' + with freeze_time("2020-01-01"): + with Form(parent_task.child_ids.with_context({'tracking_disable': True})) as form: + form.description = 'my super recurring task bla bla bla' + form.date_deadline = datetime(2020, 2, 1) + + form.recurring_task = True + form.repeat_interval = 1 + form.repeat_unit = 'month' + form.repeat_type = 'after' + form.repeat_number = 2 + form.repeat_on_month = 'date' + form.repeat_day = '15' + subtask = form.save() + subtask.planned_hours = 2 + + self.assertEqual(subtask.recurrence_id.next_recurrence_date, date(2020, 1, 15)) + project_task_count, project_subtask_count = get_task_and_subtask_counts(domain) + self.assertEqual(project_task_count, 2) + self.assertEqual(project_subtask_count, 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + self.assertEqual(self.env['project.task'].search_count(domain), 2, 'no extra task should be created') + self.assertEqual(subtask.recurrence_id.recurrence_left, 2) + + with freeze_time("2020-01-15"): + project_task_count, project_subtask_count = get_task_and_subtask_counts(domain) + self.assertEqual(project_task_count, 2) + self.assertEqual(project_subtask_count, 1) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + project_task_count, project_subtask_count = get_task_and_subtask_counts(domain) + self.assertEqual(project_task_count, 3) + self.assertEqual(project_subtask_count, 2) + self.assertEqual(subtask.recurrence_id.recurrence_left, 1) + + with freeze_time("2020-02-15"): + self.env['project.task.recurrence']._cron_create_recurring_tasks() + project_task_count, project_subtask_count = get_task_and_subtask_counts(domain) + self.assertEqual(project_task_count, 4) + self.assertEqual(project_subtask_count, 3) + self.assertEqual(subtask.recurrence_id.recurrence_left, 0) + self.env['project.task.recurrence']._cron_create_recurring_tasks() + project_task_count, project_subtask_count = get_task_and_subtask_counts(domain) + self.assertEqual(project_task_count, 4) + self.assertEqual(project_subtask_count, 3) + self.assertEqual(subtask.recurrence_id.recurrence_left, 0) + + tasks = self.env['project.task'].search(domain) + self.assertEqual(len(tasks), 4) + + 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) diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 64403a217a7..4e2fce8e87a 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -852,7 +852,7 @@ - + From 987023f3b53f33ebe40851d45970a2e88dc38011 Mon Sep 17 00:00:00 2001 From: Thibault Libioulle Date: Mon, 7 Jun 2021 16:44:33 +0200 Subject: [PATCH 2/2] [FIX] project: fix recurrence for child tasks Previously, only the direct children of a recurrent tasks were copied during a recurrence. Also, the child tasks which had a recurrence were copied with the recurrence_id of the initial subtask, which lead the new subtask to be linked with the recurrence of the initial subtask. With this commit we copy the children until the 3rd layer. Also, we hard copy the recurrence of the initial subtask and link this new recurrence with the new subtask, to make it decoupled from the initial subtask recurrence. Furthermore, if several initial subtasks has been created with a same recurrence, only the first subtask is copied. PR: #77126 task-2522076 X-original-commit: 808a965b4171a386f3987071c19cc27783ba0119 --- addons/project/models/project.py | 6 --- .../project/models/project_task_recurrence.py | 44 ++++++++++++++----- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index c9828112dd2..08b71669dc6 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -1417,12 +1417,6 @@ class Task(models.Model): else: task.stage_id = False - @api.constrains('recurring_task') - def _check_recurring_task(self): - # Deprecated method - # TODO : Remove me in master - return - @api.returns('self', lambda value: value.id) def copy(self, default=None): if default is None: diff --git a/addons/project/models/project_task_recurrence.py b/addons/project/models/project_task_recurrence.py index f9964deb313..9dcbcc089af 100644 --- a/addons/project/models/project_task_recurrence.py +++ b/addons/project/models/project_task_recurrence.py @@ -44,9 +44,9 @@ class ProjectTaskRecurrence(models.Model): _name = 'project.task.recurrence' _description = 'Task Recurrence' - task_ids = fields.One2many('project.task', 'recurrence_id') + task_ids = fields.One2many('project.task', 'recurrence_id', copy=False) next_recurrence_date = fields.Date() - recurrence_left = fields.Integer(string="Number of Tasks Left to Create") + recurrence_left = fields.Integer(string="Number of Tasks Left to Create", copy=False) repeat_interval = fields.Integer(string='Repeat Every', default=1) repeat_unit = fields.Selection([ @@ -212,19 +212,43 @@ class ProjectTaskRecurrence(models.Model): create_values['user_ids'] = False return create_values + def _create_subtasks(self, task, new_task, depth=3): + if depth == 0 or not task.child_ids: + return + children = [] + child_recurrence = [] + # copy the subtasks of the original task + for child in task.child_ids: + if child.recurrence_id and child.recurrence_id.id in child_recurrence: + # The subtask has been generated by another subtask in the childs + # This subtasks is skipped as it will be meant to be a copy of the first + # task of the recurrence we just created. + continue + child_values = self._new_task_values(child) + child_values['parent_id'] = new_task.id + if child.recurrence_id: + # The subtask has a recurrence, the recurrence is thus copied rather than used + # with raw reference in order to decouple the recurrence of the initial subtask + # from the recurrence of the copied subtask which will live its own life and generate + # subsequent tasks. + child_recurrence += [child.recurrence_id.id] + child_values['recurrence_id'] = child.recurrence_id.copy().id + if child.child_ids and depth > 1: + # If child has childs in the following layer and we will have to copy layer, we have to + # first create the new_child record in order to have a new parent_id reference for the + # "grandchildren" tasks + new_child = self.env['project.task'].sudo().create(child_values) + self._create_subtasks(child, new_child, depth=depth - 1) + else: + children.append(child_values) + self.env['project.task'].sudo().create(children) + def _create_next_task(self): for recurrence in self: task = recurrence.sudo().task_ids[-1] create_values = recurrence._new_task_values(task) new_task = self.env['project.task'].sudo().create(create_values) - if not new_task.parent_id and task.child_ids: - children = [] - # copy the subtasks of the original task - for child in task.child_ids: - child_values = recurrence._new_task_values(child) - child_values['parent_id'] = new_task.id - children.append(child_values) - self.env['project.task'].create(children) + recurrence._create_subtasks(task, new_task, depth=3) def _set_next_recurrence_date(self): today = fields.Date.today()