This commit aims to add a button in the control panel to easily display the current project update status. This is done by extending ControlPanel Component and template. We must add a context value `show_project_update` to enable the button in the breadcrumb and it's done only once; e.g. when we click to see all project tasks of an active project. The button redirects to a kanban list view of the project_updates. The activity rng is modified in order to accept js_class definition. Following commits will add behaviour to handle those updates and milestones in the various views. See PR for further information. PR : #68899 task-2393768
202 lines
8.5 KiB
Python
202 lines
8.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
from werkzeug.urls import url_encode
|
|
|
|
from odoo import api, fields, models
|
|
|
|
STATUS_COLOR = {
|
|
'on_track': 10, # green
|
|
'at_risk': 2, # orange
|
|
'off_track': 1, # red
|
|
'on_hold': 4, # light blue
|
|
}
|
|
|
|
class ProjectUpdate(models.Model):
|
|
_name = 'project.update'
|
|
_description = 'Project Update'
|
|
_order = 'create_date desc'
|
|
_inherit = ['mail.thread.cc', 'mail.activity.mixin']
|
|
|
|
def default_get(self, fields):
|
|
result = super().default_get(fields)
|
|
if 'project_id' in fields and not result.get('project_id'):
|
|
result['project_id'] = self.env.context.get('active_id')
|
|
if result.get('project_id'):
|
|
project = self.env['project.project'].browse(result['project_id'])
|
|
if 'progress' in fields and not result.get('progress'):
|
|
result['progress'] = project.last_update_id.progress
|
|
if 'description' in fields and not result.get('description'):
|
|
result['description'] = self._build_description(project)
|
|
if 'status' in fields and not result.get('status'):
|
|
result['status'] = project.last_update_status
|
|
return result
|
|
|
|
name = fields.Char("Title", required=True, tracking=True)
|
|
status = fields.Selection(selection=[
|
|
('on_track', 'On Track'),
|
|
('at_risk', 'At Risk'),
|
|
('off_track', 'Off Track'),
|
|
('on_hold', 'On Hold')
|
|
], required=True, tracking=True)
|
|
color = fields.Integer(compute='_compute_color')
|
|
progress = fields.Integer(tracking=True)
|
|
progress_percentage = fields.Float(compute='_compute_progress_percentage')
|
|
user_id = fields.Many2one('res.users', string='Author', required=True, default=lambda self: self.env.user)
|
|
description = fields.Html()
|
|
date = fields.Date(default=fields.Date.context_today, tracking=True)
|
|
project_id = fields.Many2one('project.project', required=True)
|
|
name_cropped = fields.Char(compute="_compute_name_cropped")
|
|
|
|
@api.depends('status')
|
|
def _compute_color(self):
|
|
for update in self:
|
|
update.color = STATUS_COLOR[update.status]
|
|
|
|
@api.depends('progress')
|
|
def _compute_progress_percentage(self):
|
|
for u in self:
|
|
u.progress_percentage = u.progress / 100
|
|
|
|
@api.depends('name')
|
|
def _compute_name_cropped(self):
|
|
for u in self:
|
|
u.name_cropped = (u.name[:57] + '...') if len(u.name) > 60 else u.name
|
|
|
|
# ---------------------------------
|
|
# ORM Override
|
|
# ---------------------------------
|
|
@api.model
|
|
def create(self, vals):
|
|
update = super().create(vals)
|
|
update.project_id.last_update_id = update
|
|
return update
|
|
|
|
# ---------------------------------
|
|
# Build default description
|
|
# ---------------------------------
|
|
@api.model
|
|
def _build_description(self, project):
|
|
template = self.env.ref('project.project_update_default_description')
|
|
return template._render(self._get_template_values(project), engine='ir.qweb')
|
|
|
|
@api.model
|
|
def _get_template_values(self, project):
|
|
return {
|
|
'user': self.env.user,
|
|
'project': project,
|
|
'tasks': self._get_tasks_values(project),
|
|
'milestones': self._get_milestone_values(project)
|
|
}
|
|
|
|
@api.model
|
|
def _get_tasks_values(self, project):
|
|
counts = project._get_tasks_analysis_counts(created=True)
|
|
return {
|
|
'open_tasks': counts['open_tasks_count'],
|
|
'total_tasks': counts['tasks_count'],
|
|
'created_tasks': counts['created_tasks_count'],
|
|
'closed_tasks': self._get_last_stage_changes(project=project),
|
|
'action': {
|
|
'url_default': '/web#' + url_encode({
|
|
'menu_id': self.env.ref('project.menu_projects').id,
|
|
'action': self.env.ref('project.action_project_task_burndown_chart_report').id,
|
|
'active_id': project.id
|
|
}),
|
|
'name': 'action_burndown_open_tasks',
|
|
}
|
|
}
|
|
|
|
@api.model
|
|
def _get_last_stage_changes(self, project):
|
|
query = """
|
|
SELECT DISTINCT pt.id as task_id
|
|
FROM mail_message mm
|
|
INNER JOIN mail_tracking_value mtv
|
|
ON mm.id = mtv.mail_message_id
|
|
INNER JOIN ir_model_fields imf
|
|
ON mtv.field = imf.id
|
|
AND imf.model = 'project.task'
|
|
AND imf.name = 'stage_id'
|
|
INNER JOIN project_task_type new_stage
|
|
ON mtv.new_value_integer = new_stage.id
|
|
INNER JOIN project_task pt
|
|
ON mm.res_id = pt.id
|
|
AND pt.stage_id = new_stage.id
|
|
WHERE mm.model = 'project.task'
|
|
AND mm.message_type = 'notification'
|
|
AND pt.display_project_id = %(project_id)s
|
|
AND (new_stage.fold OR new_stage.is_closed)
|
|
AND mm.date > (now() at time zone 'utc')::date - '1 month'::interval
|
|
AND pt.active
|
|
"""
|
|
self.env.cr.execute(query, {'project_id': project.id})
|
|
task_ids = [res['task_id'] for res in self.env.cr.dictfetchall()]
|
|
return self.env['project.task'].search_count([('id', 'in', task_ids)])
|
|
|
|
@api.model
|
|
def _get_milestone_values(self, project):
|
|
Milestone = self.env['project.milestone']
|
|
list_milestones = Milestone.search(
|
|
[('project_id', '=', project.id),
|
|
'|', ('deadline', '<', fields.Date.context_today(self) + relativedelta(years=1)), ('deadline', '=', False)])._get_data_list()
|
|
updated_milestones = self._get_last_updated_milestone(project)
|
|
created_milestones = Milestone.search(
|
|
[('project_id', '=', project.id),
|
|
('create_date', '>', fields.Datetime.now() + timedelta(days=-30))])._get_data_list()
|
|
return {
|
|
'show_section': (list_milestones or updated_milestones or created_milestones) and True or False,
|
|
'list': list_milestones,
|
|
'updated': updated_milestones,
|
|
'created': created_milestones,
|
|
}
|
|
|
|
@api.model
|
|
def _get_last_updated_milestone(self, project):
|
|
query = """
|
|
SELECT DISTINCT pm.id as milestone_id,
|
|
pm.deadline as deadline,
|
|
FIRST_VALUE(old_value_datetime::date) OVER w_partition as old_value,
|
|
pm.deadline as new_value
|
|
FROM mail_message mm
|
|
INNER JOIN mail_tracking_value mtv
|
|
ON mm.id = mtv.mail_message_id
|
|
INNER JOIN ir_model_fields imf
|
|
ON mtv.field = imf.id
|
|
AND imf.model = 'project.milestone'
|
|
AND imf.name = 'deadline'
|
|
INNER JOIN project_milestone pm
|
|
ON mm.res_id = pm.id
|
|
WHERE mm.model = 'project.milestone'
|
|
AND mm.message_type = 'notification'
|
|
AND pm.project_id = %(project_id)s
|
|
AND mm.date > (now() at time zone 'utc')::date - '1 month'::interval
|
|
WINDOW w_partition AS (
|
|
PARTITION BY pm.id
|
|
ORDER BY mm.date ASC
|
|
)
|
|
ORDER BY pm.deadline ASC;
|
|
"""
|
|
self.env.cr.execute(query, {'project_id': project.id})
|
|
results = self.env.cr.dictfetchall()
|
|
mapped_result = {res['milestone_id']: {'new_value': res['new_value'], 'old_value': res['old_value']} for res in results}
|
|
milestones = self.env['project.milestone'].search([('id', 'in', list(mapped_result.keys()))])
|
|
return [{
|
|
**milestone._get_data(),
|
|
'new_value': mapped_result[milestone.id]['new_value'],
|
|
'old_value': mapped_result[milestone.id]['old_value'],
|
|
} for milestone in milestones]
|
|
|
|
def action_burndown_open_tasks(self):
|
|
if not self.id or not self.project_id:
|
|
return False
|
|
action = self.env['ir.actions.act_window']._for_xml_id('project.action_project_task_burndown_chart_report')
|
|
context = {
|
|
'active_id': self.project_id.id,
|
|
'search_default_project_id': self.project_id.id,
|
|
'graph_mode': 'bar',
|
|
}
|
|
return dict(action, context=context)
|