[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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user