diff --git a/addons/project/models/project_milestone.py b/addons/project/models/project_milestone.py index 24fef86195b..8c6e4391e53 100644 --- a/addons/project/models/project_milestone.py +++ b/addons/project/models/project_milestone.py @@ -108,3 +108,13 @@ class ProjectMilestone(models.Model): def _get_data_list(self): return [ms._get_data() for ms in self] + + @api.returns('self', lambda value: value.id) + def copy(self, default=None): + if default is None: + default = {} + milestone_copy = super(ProjectMilestone, self).copy(default) + if self.project_id.allow_milestones: + milestone_mapping = self.env.context.get('milestone_mapping', {}) + milestone_mapping[self.id] = milestone_copy.id + return milestone_copy diff --git a/addons/project/models/project_project.py b/addons/project/models/project_project.py index b79f3510a5f..7e07f9599e7 100644 --- a/addons/project/models/project_project.py +++ b/addons/project/models/project_project.py @@ -204,7 +204,7 @@ class Project(models.Model): ('done', 'Done'), ], default='to_define', compute='_compute_last_update_status', store=True, readonly=False, required=True) last_update_color = fields.Integer(compute='_compute_last_update_color') - milestone_ids = fields.One2many('project.milestone', 'project_id', copy=True) + milestone_ids = fields.One2many('project.milestone', 'project_id') milestone_count = fields.Integer(compute='_compute_milestone_count', groups='project.group_project_milestone') milestone_count_reached = fields.Integer(compute='_compute_milestone_reached_count', groups='project.group_project_milestone') is_milestone_exceeded = fields.Boolean(compute="_compute_is_milestone_exceeded", search='_search_is_milestone_exceeded') @@ -385,6 +385,10 @@ class Project(models.Model): project = super(Project, self_with_mail_context).copy(default) for follower in self.message_follower_ids: project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids) + if self.allow_milestones: + if 'milestone_mapping' not in self.env.context: + self = self.with_context(milestone_mapping=dict()) + project.milestone_ids = [milestone.copy().id for milestone in self.milestone_ids] if 'tasks' not in default: self.map_tasks(project.id) diff --git a/addons/project/models/project_task.py b/addons/project/models/project_task.py index b0e547fdd93..50b93ffb7f1 100644 --- a/addons/project/models/project_task.py +++ b/addons/project/models/project_task.py @@ -677,6 +677,9 @@ class Task(models.Model): self.write({'dependent_ids': [Command.unlink(t.id) for t in self.dependent_ids if t.id in new_tasks]}) task_copy.write({'depend_on_ids': [Command.link(task_mapping.get(t.id, t.id)) for t in self.depend_on_ids]}) task_copy.write({'dependent_ids': [Command.link(task_mapping.get(t.id, t.id)) for t in self.dependent_ids]}) + if self.allow_milestones: + milestone_mapping = self.env.context.get('milestone_mapping', {}) + task_copy.milestone_id = milestone_mapping.get(task_copy.milestone_id.id, task_copy.milestone_id.id) return task_copy @api.model diff --git a/addons/project/tests/test_project_milestone.py b/addons/project/tests/test_project_milestone.py index 13070b461e5..271c0e40889 100644 --- a/addons/project/tests/test_project_milestone.py +++ b/addons/project/tests/test_project_milestone.py @@ -49,3 +49,48 @@ class TestProjectMilestone(TestProjectCommon): 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') + + def test_duplicate_project_duplicates_milestones_on_tasks(self): + """ + Test when we duplicate the project with tasks linked to its' milestones, + that the tasks in the new project are also linked to the duplicated milestones of the new project + We can't really robustly test that the mapping of task -> milestone is the same in the old and new project, + the workaround way of testing the mapping is basing ourselves on unique names and check that those are equals in the test. + """ + # original unique_names, used to map between the original -> copy + unique_name_1 = "unique_name_1" + unique_name_2 = "unique_name_2" + unique_names = [unique_name_1, unique_name_2] + project = self.env['project.project'].create({'name': 'Test project'}) + milestones = self.env['project.milestone'].create([{ + 'name': unique_name_1, + 'project_id': project.id, + }, { + 'name': unique_name_2, + 'project_id': project.id, + }]) + tasks = self.env['project.task'].create([{ + 'name': unique_name_1, + 'project_id': project.id, + 'milestone_id': milestones[0].id, + }, { + 'name': unique_name_2, + 'project_id': project.id, + 'milestone_id': milestones[1].id, + }]) + self.assertEqual(tasks[0].milestone_id, milestones[0]) + self.assertEqual(tasks[1].milestone_id, milestones[1]) + project_copy = project.copy() + self.assertNotEqual(project_copy.milestone_ids, False) + self.assertEqual(project.milestone_ids.mapped('name'), project_copy.milestone_ids.mapped('name')) + self.assertNotEqual(project_copy.task_ids, False) + for milestone in project_copy.task_ids.milestone_id: + self.assertTrue(milestone in project_copy.milestone_ids) + for unique_name in unique_names: + orig_task = project.task_ids.filtered(lambda t: t.name == unique_name) + copied_task = project_copy.task_ids.filtered(lambda t: t.name == unique_name) + self.assertEqual(orig_task.name, copied_task.name, "The copied_task should be a copy of the original task") + 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 ")