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..08b71669dc6 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -1417,11 +1417,6 @@ class Task(models.Model): else: task.stage_id = False - @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.")) - @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() 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 @@ - +