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 @@
-
+