[FW][MERGE] project: re-allow recurring sub 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 PR 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 -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#77198 Forward-port-of: odoo/odoo#77126 Signed-off-by: LTU-Odoo <IT-Ideas@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,11 @@
|
||||
<field name="groups_id" eval="[(4, ref('group_project_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Groups : Add subtasks and recurring tasks by default -->
|
||||
<record id="base.group_user" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('group_subtask_project')), (4, ref('group_project_recurring_tasks'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Categories -->
|
||||
<record id="project_tags_00" model="project.tags">
|
||||
<field name="name">Bug</field>
|
||||
@@ -875,5 +880,94 @@ Send it ASAP, its urgent.</field>
|
||||
<field name="progress" eval="35"/>
|
||||
<field name="status">at_risk</field>
|
||||
</record>
|
||||
|
||||
<!-- Recurring tasks and subtasks -->
|
||||
<record id="project_task_recurrence_1" model="project.task.recurrence">
|
||||
<field name="recurrence_left">3</field>
|
||||
<field name="repeat_unit">month</field>
|
||||
<field name="repeat_on_month">date</field>
|
||||
<field name="repeat_type">after</field>
|
||||
<field name="repeat_number">4</field>
|
||||
<field name="repeat_day">1</field>
|
||||
<field name="repeat_weekday">mon</field>
|
||||
<field name="create_date" eval="DateTime.now() + relativedelta(weeks=-2)"/>
|
||||
</record>
|
||||
<record id="project_task_recurrence_1" model="project.task.recurrence">
|
||||
<field name="next_recurrence_date" eval="DateTime.now() + relativedelta(weeks=-2)"/>
|
||||
</record>
|
||||
<record id="project_task_recurrence_2" model="project.task.recurrence">
|
||||
<field name="recurrence_left">20</field>
|
||||
<field name="mon" eval="True"/>
|
||||
<field name="tue" eval="True"/>
|
||||
<field name="wed" eval="True"/>
|
||||
<field name="thu" eval="True"/>
|
||||
<field name="fri" eval="True"/>
|
||||
<field name="repeat_type">after</field>
|
||||
<field name="repeat_number">20</field>
|
||||
</record>
|
||||
<record id="project_task_recurrence_2" model="project.task.recurrence">
|
||||
<field name="next_recurrence_date" eval="DateTime.now() + relativedelta(weeks=-2)"/>
|
||||
</record>
|
||||
<record id="project_task_27" model="project.task">
|
||||
<field name="sequence">20</field>
|
||||
<field name="planned_hours">20.0</field>
|
||||
<field name="user_ids" eval="False"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
<field name="name">Customer review</field>
|
||||
<field name="stage_id" ref="project_stage_1"/>
|
||||
<field name="recurring_task" eval="True"/>
|
||||
<field name="recurrence_id" ref="project_task_recurrence_1"/>
|
||||
<field name="create_date" eval="DateTime.now() + relativedelta(weeks=-2)"/>
|
||||
</record>
|
||||
<record id="project_task_28" model="project.task">
|
||||
<field name="sequence">20</field>
|
||||
<field name="planned_hours">0.25</field>
|
||||
<field name="user_ids" eval="False"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
<field name="parent_id" ref="project.project_task_27"/>
|
||||
<field name="name">Daily stand-up meeting - Send minutes</field>
|
||||
<field name="stage_id" ref="project_stage_1"/>
|
||||
<field name="recurring_task" eval="True"/>
|
||||
<field name="recurrence_id" ref="project_task_recurrence_2"/>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(weeks=-1)"/>
|
||||
</record>
|
||||
<record id="project_task_29" model="project.task">
|
||||
<field name="sequence">20</field>
|
||||
<field name="planned_hours">8.0</field>
|
||||
<field name="user_ids" eval="False"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
<field name="parent_id" ref="project.project_task_27"/>
|
||||
<field name="name">Customer Meeting</field>
|
||||
<field name="stage_id" ref="project_stage_1"/>
|
||||
</record>
|
||||
<record id="project_task_30" model="project.task">
|
||||
<field name="sequence">10</field>
|
||||
<field name="planned_hours">2.0</field>
|
||||
<field name="user_ids" eval="False"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
<field name="parent_id" ref="project.project_task_29"/>
|
||||
<field name="name">Daily Meetings summary</field>
|
||||
<field name="stage_id" ref="project_stage_1"/>
|
||||
</record>
|
||||
<record id="project_task_31" model="project.task">
|
||||
<field name="sequence">20</field>
|
||||
<field name="planned_hours">2.0</field>
|
||||
<field name="user_ids" eval="False"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
<field name="parent_id" ref="project.project_task_29"/>
|
||||
<field name="name">Preparation</field>
|
||||
<field name="stage_id" ref="project_stage_1"/>
|
||||
</record>
|
||||
<record id="project_task_32" model="project.task">
|
||||
<field name="sequence">30</field>
|
||||
<field name="planned_hours">2.0</field>
|
||||
<field name="user_ids" eval="False"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
<field name="parent_id" ref="project.project_task_29"/>
|
||||
<field name="name">Minutes</field>
|
||||
<field name="stage_id" ref="project_stage_1"/>
|
||||
</record>
|
||||
|
||||
<function model="project.task.recurrence" name="_cron_create_recurring_tasks"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -852,7 +852,7 @@
|
||||
<field name="partner_phone" widget="phone" attrs="{'invisible': True}"/>
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
|
||||
<field name="recurring_task" attrs="{'invisible': ['|', ('allow_recurring_tasks', '=', False), ('parent_id', '!=', False)]}" />
|
||||
<field name="recurring_task" attrs="{'invisible': [('allow_recurring_tasks', '=', False)]}" />
|
||||
<field name="legend_blocked" invisible="1"/>
|
||||
<field name="legend_normal" invisible="1"/>
|
||||
<field name="legend_done" invisible="1"/>
|
||||
|
||||
Reference in New Issue
Block a user