From cb73921bef267f1523e577466705f71ecd2098d0 Mon Sep 17 00:00:00 2001 From: "Victor Piryns (pivi)" Date: Wed, 12 Apr 2023 11:23:44 +0000 Subject: [PATCH] [FIX] project: dup milestones on tasks when dup project Current behaviour: When duplicating a project, new milestones are copied for the new project, but none of them are assigned to the copied tasks like in the original project. Expected behaviour: The new tasks in the new project should have the corresponding copy of the milestone that were assigned in the original project. Steps to reproduce: - Install Project - Duplicate "Office Design" (it has milestones) - Observe that the tasks in the new project don't have milestones assigned to them, like in the original project. Reason for the problem: When we copy the tasks, they have the milestones of the original project correctly assigned to them, but since the project of the milestone is different from the project of the task (former references the original project, while the latter references the copied project), so in `_compute_milestone_id`, the milestone of the task is set to False. Fix: Remove `copy=True` from `milestone_ids` on the project, and copy the milestone by hand. This allows us to use an overwrite of `copy()` for `project.milestone`, and we create a mapping between the old milestones and the new ones in the context, similar to how we did with `task_mapping`. With this we can assign the newly created milestones on the copied tasks correctly (while preserving the mapping like in the original project). Affected versions: - 16.0 - saas-16.1 - saas-16.2 - master opw-3254868 closes odoo/odoo#118487 X-original-commit: aad60a2c8a887f3ea226af99be9b162673c33ec8 Signed-off-by: Xavier Bol (xbo) Signed-off-by: Piryns Victor (pivi) --- addons/project/models/project_milestone.py | 10 +++++ addons/project/models/project_project.py | 6 ++- addons/project/models/project_task.py | 3 ++ .../project/tests/test_project_milestone.py | 45 +++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) 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 ")