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) <xbo@odoo.com> Signed-off-by: Piryns Victor (pivi) <pivi@odoo.com>
121 lines
5.6 KiB
Python
121 lines
5.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
|
|
from odoo import api, fields, models
|
|
|
|
class ProjectMilestone(models.Model):
|
|
_name = 'project.milestone'
|
|
_description = "Project Milestone"
|
|
_inherit = ['mail.thread']
|
|
_order = 'deadline, is_reached desc, name'
|
|
|
|
def _get_default_project_id(self):
|
|
return self.env.context.get('default_project_id') or self.env.context.get('active_id')
|
|
|
|
name = fields.Char(required=True)
|
|
project_id = fields.Many2one('project.project', required=True, default=_get_default_project_id, ondelete='cascade')
|
|
deadline = fields.Date(tracking=True, copy=False)
|
|
is_reached = fields.Boolean(string="Reached", default=False, copy=False)
|
|
reached_date = fields.Date(compute='_compute_reached_date', store=True)
|
|
task_ids = fields.One2many('project.task', 'milestone_id', 'Tasks')
|
|
|
|
# computed non-stored fields
|
|
is_deadline_exceeded = fields.Boolean(compute="_compute_is_deadline_exceeded")
|
|
is_deadline_future = fields.Boolean(compute="_compute_is_deadline_future")
|
|
task_count = fields.Integer('# of Tasks', compute='_compute_task_count', groups='project.group_project_milestone')
|
|
done_task_count = fields.Integer('# of Done Tasks', compute='_compute_task_count', groups='project.group_project_milestone')
|
|
can_be_marked_as_done = fields.Boolean(compute='_compute_can_be_marked_as_done', groups='project.group_project_milestone')
|
|
|
|
@api.depends('is_reached')
|
|
def _compute_reached_date(self):
|
|
for ms in self:
|
|
ms.reached_date = ms.is_reached and fields.Date.context_today(self)
|
|
|
|
@api.depends('is_reached', 'deadline')
|
|
def _compute_is_deadline_exceeded(self):
|
|
today = fields.Date.context_today(self)
|
|
for ms in self:
|
|
ms.is_deadline_exceeded = not ms.is_reached and ms.deadline and ms.deadline < today
|
|
|
|
@api.depends('deadline')
|
|
def _compute_is_deadline_future(self):
|
|
for ms in self:
|
|
ms.is_deadline_future = ms.deadline and ms.deadline > fields.Date.context_today(self)
|
|
|
|
@api.depends('task_ids.milestone_id')
|
|
def _compute_task_count(self):
|
|
all_and_done_task_count_per_milestone = {
|
|
group['milestone_id'][0]: (group['milestone_id_count'], group['is_closed'].count(True))
|
|
for group in self.env['project.task']._read_group(
|
|
[('milestone_id', 'in', self.ids), ('allow_milestones', '=', True)],
|
|
['milestone_id', 'is_closed:array_agg'], ['milestone_id']
|
|
)
|
|
}
|
|
for milestone in self:
|
|
milestone.task_count, milestone.done_task_count = all_and_done_task_count_per_milestone.get(milestone.id, (0, 0))
|
|
|
|
def _compute_can_be_marked_as_done(self):
|
|
if not any(self._ids):
|
|
for milestone in self:
|
|
milestone.can_be_marked_as_done = not milestone.is_reached and all(milestone.task_ids.is_closed)
|
|
return
|
|
unreached_milestones = self.filtered(lambda milestone: not milestone.is_reached)
|
|
(self - unreached_milestones).can_be_marked_as_done = False
|
|
if unreached_milestones:
|
|
task_read_group = self.env['project.task']._read_group(
|
|
[('milestone_id', 'in', unreached_milestones.ids)],
|
|
['milestone_id', 'is_closed', 'task_count:count(id)'],
|
|
['milestone_id', 'is_closed'],
|
|
lazy=False,
|
|
)
|
|
task_count_per_milestones = defaultdict(lambda: (0, 0))
|
|
for res in task_read_group:
|
|
opened_task_count, closed_task_count = task_count_per_milestones[res['milestone_id'][0]]
|
|
if res['is_closed']:
|
|
closed_task_count += res['task_count']
|
|
else:
|
|
opened_task_count += res['task_count']
|
|
task_count_per_milestones[res['milestone_id'][0]] = opened_task_count, closed_task_count
|
|
for milestone in unreached_milestones:
|
|
opened_task_count, closed_task_count = task_count_per_milestones[milestone.id]
|
|
milestone.can_be_marked_as_done = closed_task_count > 0 and not opened_task_count
|
|
|
|
def toggle_is_reached(self, is_reached):
|
|
self.ensure_one()
|
|
self.update({'is_reached': is_reached})
|
|
return self._get_data()
|
|
|
|
def action_view_tasks(self):
|
|
self.ensure_one()
|
|
action = self.env['ir.actions.act_window']._for_xml_id('project.action_view_task_from_milestone')
|
|
action['context'] = {'default_project_id': self.project_id.id, 'default_milestone_id': self.id}
|
|
if self.task_count == 1:
|
|
action['view_mode'] = 'form'
|
|
action['res_id'] = self.task_ids.id
|
|
if 'views' in action:
|
|
action['views'] = [(view_id, view_type) for view_id, view_type in action['views'] if view_type == 'form']
|
|
return action
|
|
|
|
@api.model
|
|
def _get_fields_to_export(self):
|
|
return ['id', 'name', 'deadline', 'is_reached', 'reached_date', 'is_deadline_exceeded', 'is_deadline_future', 'can_be_marked_as_done']
|
|
|
|
def _get_data(self):
|
|
self.ensure_one()
|
|
return {field: self[field] for field in self._get_fields_to_export()}
|
|
|
|
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
|