[IMP] project,hr_timesheet: improve project update

Add a new column, after tasks that shows the following information :

Tasks: Total done tasks /  Total tasks (ratio in percentage)
Timesheet: Hours spent/ allocated hours (ratio in percentage) 
\> visibility: only show if the timesheet setting is activated.

Right side panel  :

Add the Done icon before Tasks 
Add hours spent/allocated hours information under the Timesheet
Extra time: Add a new stat button that shows
the overall extra time used for the project. 
visible if hours spent > allocated hours.
By clicking on this it will redirect to the same view
as we have for the timesheet stat button.

task-3432102

closes odoo/odoo#129177

Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
Audric Onockx (auon)
2023-09-28 07:50:02 +00:00
parent ad95834434
commit 0baaa4b654
8 changed files with 182 additions and 22 deletions
+1
View File
@@ -9,5 +9,6 @@ from . import res_company
from . import res_config_settings
from . import project_project
from . import project_task
from . import project_update
from . import project_collaborator
from . import uom_uom
+73 -1
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import models, fields, api, _
from odoo import models, fields, api, _, _lt
from odoo.exceptions import ValidationError, RedirectWarning
class Project(models.Model):
@@ -211,3 +211,75 @@ class Project(models.Model):
action = self.env['ir.actions.act_window']._for_xml_id('hr_timesheet.act_hr_timesheet_line_by_project')
action['display_name'] = _("%(name)s's Timesheets", name=self.name)
return action
# ----------------------------
# Project Updates
# ----------------------------
def _get_stat_buttons(self):
buttons = super(Project, self)._get_stat_buttons()
if not self.allow_timesheets or not self.env.user.has_group("hr_timesheet.group_hr_timesheet_user"):
return buttons
encode_uom = self.env.company.timesheet_encode_uom_id
uom_ratio = self.env.ref('uom.product_uom_hour').factor / encode_uom.factor
allocated = self.allocated_hours / uom_ratio
effective = self.total_timesheet_time / uom_ratio
color = ""
if allocated:
number = f"{round(effective)} / {round(allocated)} {encode_uom.name}"
success_rate = round(100 * effective / allocated)
if success_rate > 100:
number = _lt(
"%(effective)s / %(allocated)s %(uom_name)s",
effective=round(effective),
allocated=round(allocated),
uom_name=encode_uom.name,
)
color = "text-danger"
else:
number = _lt(
"%(effective)s / %(allocated)s %(uom_name)s (%(success_rate)s%%)",
effective=round(effective),
allocated=round(allocated),
uom_name=encode_uom.name,
success_rate=success_rate,
)
if success_rate >= 80:
color = "text-warning"
else:
color = "text-success"
else:
number = _lt(
"%(effective)s %(uom_name)s",
effective=round(effective),
uom_name=encode_uom.name,
)
buttons.append({
"icon": f"clock-o {color}",
"text": _lt("Timesheets"),
"number": number,
"action_type": "object",
"action": "action_project_timesheets",
"show": True,
"sequence": 2,
})
if allocated and success_rate > 100:
buttons.append({
"icon": f"warning {color}",
"text": _lt("Extra Time"),
"number": _lt(
"%(exceeding_hours)s %(uom_name)s (+%(exceeding_rate)s%%)",
exceeding_hours=round(effective - allocated),
uom_name=encode_uom.name,
exceeding_rate=round(100 * (effective - allocated) / allocated),
),
"action_type": "object",
"action": "action_project_timesheets",
"show": True,
"sequence": 3,
})
return buttons
@@ -0,0 +1,39 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
class ProjectUpdate(models.Model):
_inherit = "project.update"
display_timesheet_stats = fields.Boolean(compute="_compute_display_timesheet_stats")
allocated_time = fields.Integer("Allocated Time", readonly=True)
timesheet_time = fields.Integer("Timesheet Time", readonly=True)
timesheet_percentage = fields.Integer(compute="_compute_timesheet_percentage")
uom_id = fields.Many2one("uom.uom", "Unit Of Measure", readonly=True)
def _compute_timesheet_percentage(self):
for update in self:
update.timesheet_percentage = update.allocated_time and round(update.timesheet_time * 100 / update.allocated_time)
def _compute_display_timesheet_stats(self):
for update in self:
update.display_timesheet_stats = update.project_id.allow_timesheets
# ---------------------------------
# ORM Override
# ---------------------------------
@api.model_create_multi
def create(self, vals_list):
updates = super().create(vals_list)
encode_uom = self.env.company.timesheet_encode_uom_id
ratio = self.env.ref("uom.product_uom_hour").ratio / encode_uom.ratio
for update in updates:
project = update.project_id
project.sudo().last_update_id = update
update.write({
"uom_id": encode_uom,
"allocated_time": round(project.allocated_hours / ratio),
"timesheet_time": round(project.total_timesheet_time / ratio),
})
return updates
@@ -12,5 +12,18 @@
</xpath>
</field>
</record>
<record id="project_update_view_kanban_inherit" model="ir.ui.view">
<field name="name">project.update.view.kanban.inherit</field>
<field name="model">project.update</field>
<field name="inherit_id" ref="project.project_update_view_kanban"/>
<field name="arch" type="xml">
<b id="tasks_stats" position='after'>
<field name="display_timesheet_stats" invisible="1"/>
<div invisible="not display_timesheet_stats">
<field name="timesheet_time"/><span invisible="not allocated_time"> / <field name="allocated_time"/></span> <field name="uom_id" no_open="1"/><span invisible="not allocated_time"> (<field name="timesheet_percentage"/>%)</span>
</div>
</b>
</field>
</record>
</data>
</odoo>
+32 -14
View File
@@ -49,19 +49,23 @@ class Project(models.Model):
project.doc_count = docs_count.get(project.id, 0)
def _compute_task_count(self):
task_count_per_project = {
project.id: count
for project, count in self.env['project.task'].with_context(
active_test=any(project.active for project in self)
)._read_group(
[('state', 'not in', list(CLOSED_STATES)), ('project_id', 'in', self.ids)],
['project_id'],
['__count'],
)
}
project_and_state_counts = self.env['project.task'].with_context(
active_test=any(project.active for project in self)
)._read_group(
[('project_id', 'in', self.ids)],
['project_id', 'state'],
['__count'],
)
task_counts_per_project_id = defaultdict(lambda: {
'open_task_count': 0,
'closed_task_count': 0,
})
for project, state, count in project_and_state_counts:
task_counts_per_project_id[project.id]['closed_task_count' if state in CLOSED_STATES else 'open_task_count'] += count
for project in self:
task_count = task_count_per_project.get(project.id, 0)
project.task_count = task_count
open_task_count, closed_task_count = task_counts_per_project_id[project.id].values()
project.closed_task_count = closed_task_count
project.task_count = open_task_count + closed_task_count
def _default_stage_id(self):
# Since project stages are order by sequence first, this should fetch the one with the lowest sequence number.
@@ -125,6 +129,7 @@ class Project(models.Model):
'resource.calendar', string='Working Time', compute='_compute_resource_calendar_id')
type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', string='Tasks Stages')
task_count = fields.Integer(compute='_compute_task_count', string="Task Count")
closed_task_count = fields.Integer(compute='_compute_task_count', string="Closed Task Count")
task_ids = fields.One2many('project.task', 'project_id', string='Tasks',
domain=[('state', 'not in', list(CLOSED_STATES))])
color = fields.Integer(string='Color Index')
@@ -820,10 +825,23 @@ class Project(models.Model):
def _get_stat_buttons(self):
self.ensure_one()
if self.task_count:
number = _lt(
"%(closed_task_count)s / %(task_count)s (%(closed_rate)s%%)",
closed_task_count=self.closed_task_count,
task_count=self.task_count,
closed_rate=round(100 * self.closed_task_count / self.task_count),
)
else:
number = _lt(
"%(closed_task_count)s / %(task_count)s",
closed_task_count=self.closed_task_count,
task_count=self.task_count,
)
buttons = [{
'icon': 'tasks',
'icon': 'check',
'text': _lt('Tasks'),
'number': self.task_count,
'number': number,
'action_type': 'object',
'action': 'action_view_tasks',
'show': True,
+18 -6
View File
@@ -23,7 +23,7 @@ STATUS_COLOR = {
class ProjectUpdate(models.Model):
_name = 'project.update'
_description = 'Project Update'
_order = 'date desc'
_order = 'id desc'
_inherit = ['mail.thread.cc', 'mail.activity.mixin']
def default_get(self, fields):
@@ -58,6 +58,9 @@ class ProjectUpdate(models.Model):
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")
task_count = fields.Integer("Task Count", readonly=True)
closed_task_count = fields.Integer("Closed Task Count", readonly=True)
closed_task_percentage = fields.Integer("Closed Task Percentage", compute="_compute_closed_task_percentage")
@api.depends('status')
def _compute_color(self):
@@ -66,13 +69,17 @@ class ProjectUpdate(models.Model):
@api.depends('progress')
def _compute_progress_percentage(self):
for u in self:
u.progress_percentage = u.progress / 100
for update in self:
update.progress_percentage = update.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
for update in self:
update.name_cropped = (update.name[:57] + '...') if len(update.name) > 60 else update.name
def _compute_closed_task_percentage(self):
for update in self:
update.closed_task_percentage = update.task_count and round(update.closed_task_count * 100 / update.task_count)
# ---------------------------------
# ORM Override
@@ -81,7 +88,12 @@ class ProjectUpdate(models.Model):
def create(self, vals_list):
updates = super().create(vals_list)
for update in updates:
update.project_id.sudo().last_update_id = update
project = update.project_id
project.sudo().last_update_id = update
update.write({
"task_count": project.task_count,
"closed_task_count": project.closed_task_count,
})
return updates
def unlink(self):
@@ -10,7 +10,7 @@
>
<div class="o_form_view">
<div class="oe_button_box o-form-buttonbox d-flex flex-wrap">
<t t-foreach="state.data.buttons" t-as="button" t-key="button.action">
<t t-foreach="state.data.buttons" t-as="button" t-key="button.sequence">
<ViewButton
t-if="button.show"
defaultRank="'oe_stat_button'"
@@ -88,6 +88,11 @@
<b><field name="progress_percentage" widget="percentage"/></b>
<div>Progress</div>
</div>
<div class="col-sm-2 col-6 pb-0">
<b id="tasks_stats">
<field name="closed_task_count"/> / <field name="task_count"/> Tasks<span invisible="not task_count"> (<field name="closed_task_percentage"/>%)</span>
</b>
</div>
<div class="col-sm-2 col-6 pb-0">
<b><field name="date"/></b>
<div>Date</div>