[IMP] project: set parent milestone on subtasks
As sub-tasks are a sub-set of a task that should logically be completed for the parent task to be completed, it would make sense for the sub-tasks to share the same milestone as their parent task by default. The milestone of a parent task is automatically set to its subtasks if: - The subtask has no milestone set - AND They belong to the same project or the subtask has no project set - OR they shared the same milestone before the change on the parent task (side effect for that case: if an invalide milestone is set on the parent task, both parent task's and subtasks' milestones which are equal are gonna be reset) task-3450281 closes odoo/odoo#130439 Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
@@ -384,6 +384,7 @@ class Project(models.Model):
|
||||
'name': task.name,
|
||||
'state': task.state,
|
||||
'company_id': project.company_id.id,
|
||||
'project_id': project.id,
|
||||
}
|
||||
|
||||
def map_tasks(self, new_project_id):
|
||||
@@ -399,11 +400,6 @@ class Project(models.Model):
|
||||
defaults = self._map_tasks_default_valeus(task, project)
|
||||
new_tasks |= task.copy(defaults)
|
||||
project.write({'tasks': [Command.set(new_tasks.ids)]})
|
||||
new_tasks._get_all_subtasks().filtered(
|
||||
lambda child: child.project_id == self
|
||||
).write({
|
||||
'project_id': project.id
|
||||
})
|
||||
return True
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
|
||||
@@ -998,6 +998,46 @@ class Task(models.Model):
|
||||
# unset the parent => "I want to display the task back in the project"
|
||||
# => set `display_in_project` to True
|
||||
vals['display_in_project'] = True
|
||||
|
||||
if 'milestone_id' in vals:
|
||||
# WARNING: has to be done after 'project_id' vals is written on subtasks
|
||||
milestone = self.env['project.milestone'].browse(vals['milestone_id'])
|
||||
|
||||
# 1. Task for which the milestone is unvalid -> milestone_id is reset
|
||||
if 'project_id' not in vals:
|
||||
unvalid_milestone_tasks = self.filtered(lambda task: task.project_id != milestone.project_id) if vals['milestone_id'] else self.env['project.task']
|
||||
else:
|
||||
unvalid_milestone_tasks = self if not vals['milestone_id'] or milestone.project_id.id != vals['project_id'] else self.env['project.task']
|
||||
valid_milestone_tasks = self - unvalid_milestone_tasks
|
||||
if unvalid_milestone_tasks:
|
||||
unvalid_milestone_tasks.write({'milestone_id': False})
|
||||
if valid_milestone_tasks:
|
||||
valid_milestone_tasks.write({'milestone_id': vals['milestone_id']})
|
||||
del vals['milestone_id']
|
||||
|
||||
# 2. Parent's milestone is set to subtask with no milestone recursively
|
||||
subtasks_to_update = valid_milestone_tasks.child_ids.filtered(
|
||||
lambda task: (task not in self and \
|
||||
not task.milestone_id and \
|
||||
task.project_id == milestone.project_id and \
|
||||
task.state not in CLOSED_STATES))
|
||||
|
||||
# 3. If parent and child task share the same milestone, child task's milestone is updated when the parent one is changed
|
||||
# No need to check if state is changed in vals as it won't affect the subtasks selected for update
|
||||
if 'project_id' not in vals:
|
||||
subtasks_to_update |= valid_milestone_tasks.child_ids.filtered(
|
||||
lambda task: (task not in self and \
|
||||
task.milestone_id == task.parent_id.milestone_id and \
|
||||
task.state not in CLOSED_STATES))
|
||||
else:
|
||||
subtasks_to_update |= valid_milestone_tasks.child_ids.filtered(
|
||||
lambda task: (task not in self and \
|
||||
(not task.display_in_project or task.project_id.id == vals['project_id']) and \
|
||||
task.milestone_id == task.parent_id.milestone_id and \
|
||||
task.state not in CLOSED_STATES))
|
||||
if subtasks_to_update:
|
||||
subtasks_to_update.write({'milestone_id': vals['milestone_id']})
|
||||
|
||||
# stage change: update date_last_stage_update
|
||||
now = fields.Datetime.now()
|
||||
if 'stage_id' in vals:
|
||||
@@ -1103,8 +1143,8 @@ class Task(models.Model):
|
||||
@api.depends('project_id')
|
||||
def _compute_milestone_id(self):
|
||||
for task in self:
|
||||
if (task.project_id or task.parent_id.project_id) != task.milestone_id.project_id:
|
||||
task.milestone_id = False
|
||||
if task.project_id != task.milestone_id.project_id:
|
||||
task.milestone_id = task.parent_id.project_id == task.project_id and task.parent_id.milestone_id
|
||||
|
||||
def _compute_has_late_and_unreached_milestone(self):
|
||||
if all(not task.allow_milestones for task in self):
|
||||
|
||||
@@ -11,10 +11,13 @@ class TestProjectMilestone(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.milestone = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
cls.milestone_pigs, cls.milestone_goats = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create([{
|
||||
'name': 'Test Milestone',
|
||||
'project_id': cls.project_pigs.id,
|
||||
})
|
||||
}, {
|
||||
'name': 'Test Milestone Goats',
|
||||
'project_id': cls.project_goats.id,
|
||||
}])
|
||||
|
||||
def test_milestones_settings_change(self):
|
||||
# To be sure the feature is disabled globally to begin the test.
|
||||
@@ -36,20 +39,62 @@ class TestProjectMilestone(TestProjectCommon):
|
||||
self.assertTrue(project_form.allow_milestones, 'New projects allow_milestones should be True by default.')
|
||||
|
||||
def test_change_project_in_task(self):
|
||||
""" Test when a task is linked to a milestone and when we change its project the milestone is removed
|
||||
""" Test when a task is linked to a milestone and when we change its project the milestone is removed (and
|
||||
we fallback on the parent milestone if it belongs to the same project)
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Set a milestone on the task
|
||||
2) Change the project of that task
|
||||
3) Check no milestone is linked to the task
|
||||
3) Check no milestone is linked to the task (or the one of its parent is used if relevant)
|
||||
"""
|
||||
self.task_1.milestone_id = self.milestone
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone)
|
||||
# A. No parent task
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
|
||||
|
||||
self.task_1.project_id = self.project_goats
|
||||
self.assertFalse(self.task_1.milestone_id, 'No milestone should be linked to the task since its project has changed')
|
||||
|
||||
# B. Parent task with no milestone set
|
||||
task_2 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Child MilestoneTask',
|
||||
'user_ids': self.user_projectmanager,
|
||||
'project_id': self.project_pigs.id,
|
||||
'parent_id': self.task_1.id,
|
||||
'milestone_id': self.milestone_pigs.id,
|
||||
})
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
|
||||
task_2.project_id = self.project_goats
|
||||
self.assertFalse(task_2.milestone_id, 'No milestone should be linked to the task since its project has changed and its parent task has no milestone')
|
||||
|
||||
# C. Parent task with a milestone set but on a different project
|
||||
self.task_1.project_id = self.project_pigs
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
task_2.project_id = self.project_pigs
|
||||
task_2.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
|
||||
task_2.project_id = self.project_goats
|
||||
self.assertFalse(task_2.milestone_id, 'No milestone should be linked to the task since its project has changed and its parent task belongs to another project')
|
||||
|
||||
# D. Parent task with a milestone set on the same project
|
||||
self.task_1.project_id = self.project_goats
|
||||
self.task_1.milestone_id = self.milestone_goats
|
||||
task_2.project_id = self.project_pigs
|
||||
task_2.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
task_2.project_id = self.project_goats
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_goats,
|
||||
'The milestone of the task should be replaced by the one of its parent task as they now belong to the same project')
|
||||
|
||||
# E. No milestone for private task
|
||||
task_2.parent_id = False
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_goats)
|
||||
task_2.project_id = False
|
||||
self.assertFalse(task_2.milestone_id, 'No milestone should be linked to a private task')
|
||||
|
||||
def test_duplicate_project_duplicates_milestones_on_tasks(self):
|
||||
"""
|
||||
Test when we duplicate the project with tasks linked to its' milestones,
|
||||
@@ -96,4 +141,188 @@ class TestProjectMilestone(TestProjectCommon):
|
||||
self.assertNotEqual(copied_task.milestone_id, False,
|
||||
"We should copy the milestone and it shouldn't be reset to false from _compute_milestone_id")
|
||||
self.assertEqual(orig_task.milestone_id.name, copied_task.milestone_id.name,
|
||||
"the copied milestone should be a copy if the original ")
|
||||
"the copied milestone should be a copy of the original ")
|
||||
|
||||
def test_basic_milestone_write(self):
|
||||
""" Testing basic milestone/project write operation on task, i.e:
|
||||
1. Set/change the milestone of a task
|
||||
2. Change the milestone/project of a task simultaneously
|
||||
3. Set/change to an invalid milestone
|
||||
"""
|
||||
extra_milestone_pigs = self.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Extra Milestone',
|
||||
'project_id': self.project_pigs.id,
|
||||
})
|
||||
|
||||
# 1. Set/change the milestone of a task
|
||||
self.task_1.project_id = self.project_pigs
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs)
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs,
|
||||
"Assignation of a valid milestone to a task with no milestone is not working properly.")
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs,
|
||||
"Change of the milestone of a task to a milestone from the same project is not working properly.")
|
||||
|
||||
# 2. Change the milestone/project of a task simultaneously
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'milestone_id': self.milestone_goats.id,
|
||||
'project_id': self.project_goats.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_goats,
|
||||
"Changing the project of a task and its milestone simultaneously is not working properly.")
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats,
|
||||
"Changing the project of a task and its milestone simultaneously is not working properly.")
|
||||
|
||||
# 3. Set/change to an invalid milestone
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats)
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_goats)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertFalse(self.task_1.milestone_id,
|
||||
"Setting the milestone of a task to an invalid value should reset the value of milestone_id.")
|
||||
|
||||
|
||||
def test_set_milestone_parent_task(self):
|
||||
""" When a milestone is set on a parent task, it is set as well on its child tasks if they have no milestone set yet and
|
||||
if they belong to the same project (or they have no project set).
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Set a milestone on the task (or not)
|
||||
2) Change the milestone of its parent
|
||||
3) Check the result
|
||||
"""
|
||||
# A. Child task with no milestone set and belonging to the same project
|
||||
task_2, task_3 = self.env['project.task'].with_context({'mail_create_nolog': True}).create([{
|
||||
'name': 'Child MilestoneTask',
|
||||
'user_ids': self.user_projectmanager,
|
||||
'project_id': self.project_pigs.id,
|
||||
'parent_id': self.task_1.id,
|
||||
}, {
|
||||
'name': 'Grand-child MilestoneTask',
|
||||
'user_ids': self.user_projectmanager,
|
||||
'project_id': self.project_pigs.id,
|
||||
}])
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
self.assertFalse(task_2.milestone_id)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs,
|
||||
"The milestone of the parent task should be set to its subtasks if they belong to the same project (or the subtask has not project set) and the subtask has no milestone already set.")
|
||||
|
||||
# B. Child task with a milestone already set
|
||||
extra_milestone_pigs = self.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Extra Milestone',
|
||||
'project_id': self.project_pigs.id,
|
||||
})
|
||||
self.task_1.milestone_id = False
|
||||
task_2.milestone_id = extra_milestone_pigs
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs, "The milestone of the child task should not be modified has it has already one set.")
|
||||
|
||||
# C. Child task with no milestone set but belonging to another project
|
||||
self.task_1.project_id = self.project_goats
|
||||
task_2.milestone_id = False
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
self.assertFalse(task_2.milestone_id)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_goats
|
||||
self.assertFalse(task_2.milestone_id, "The milestone of the parent task should not be set to its child task has they belong to different projects.")
|
||||
|
||||
# D. Recursion test (grand-parent task's milestone set to grand-child task)
|
||||
task_3.parent_id = task_2
|
||||
self.task_1.project_id = task_2.project_id = task_3.project_id = self.project_pigs
|
||||
self.task_1.milestone_id = task_2.milestone_id = task_3.milestone_id = False
|
||||
self.assertFalse(task_2.milestone_id)
|
||||
self.assertFalse(task_3.milestone_id)
|
||||
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_3.milestone_id, self.milestone_pigs, "The milestone of the parent task should be set to its (grand)child tasks recursively.")
|
||||
|
||||
# E. Recursion test 2 (grand-child task has no milestone but first level child does)
|
||||
self.task_1.milestone_id = False
|
||||
task_2.milestone_id = self.milestone_pigs
|
||||
task_3.milestone_id = False
|
||||
self.assertFalse(self.task_1.milestone_id)
|
||||
self.assertFalse(task_3.milestone_id)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
self.assertFalse(task_3.milestone_id,
|
||||
"The milestone of the parent task should be set to its (grand)child tasks recursively. If a child task milestone should not be updated, it stops the recursion.")
|
||||
|
||||
# F. Update of the parent's milestone, trigger the update of the subtask's milestone if they were the same before change
|
||||
self.task_1.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs,
|
||||
"If parent and child tasks share the same milestone, the update of the parent's milestone should trigger the update of its child's milestone.")
|
||||
|
||||
# G. Same as F but project and milestone of the parent task are changed at the same time -> The milestone of the child should not change
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'project_id': self.project_goats.id,
|
||||
'milestone_id': self.milestone_goats.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_goats)
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs,
|
||||
"The child milestone should not be updated if the parent task's project is changed.")
|
||||
|
||||
# H. Same as G but project the project writen value is the same as the previous one -> No actual change of project_id so update the subtask milestone
|
||||
self.task_1.project_id = self.project_pigs
|
||||
self.task_1.milestone_id = extra_milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'project_id': self.project_pigs.id,
|
||||
'milestone_id': self.milestone_pigs.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs,
|
||||
"The child milestone should be updated as the project of the parent task does not actually change.")
|
||||
|
||||
# I. Same case as G but the display_on_project is set to False on the child task -> Both project and milestone of the subtask should be updated
|
||||
self.task_1.write({
|
||||
'project_id': self.project_pigs.id,
|
||||
'milestone_id': extra_milestone_pigs.id,
|
||||
})
|
||||
task_2.display_in_project = False
|
||||
self.assertEqual(task_2.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'project_id': self.project_goats.id,
|
||||
'milestone_id': self.milestone_goats.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_goats)
|
||||
self.assertEqual(task_2.project_id, self.project_goats)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_goats,
|
||||
"The child milestone should be updated if the parent task's project is changed only if dislay_on_project is set to False for the subtask.")
|
||||
|
||||
# J. Same case as F but subtask is closed -> no update of its milestone
|
||||
task_2.state = '1_done'
|
||||
self.task_1.project_id = task_2.project_id = self.project_pigs
|
||||
self.task_1.milestone_id = task_2.milestone_id = self.milestone_pigs
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs)
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone_pigs)
|
||||
|
||||
self.task_1.write({
|
||||
'milestone_id': extra_milestone_pigs.id,
|
||||
})
|
||||
self.assertEqual(self.task_1.milestone_id, extra_milestone_pigs)
|
||||
self.assertEqual(task_2.milestone_id, self.milestone_pigs,
|
||||
"The child milestone should not be updated if it is closed.")
|
||||
|
||||
Reference in New Issue
Block a user