diff --git a/addons/hr_timesheet/models/__init__.py b/addons/hr_timesheet/models/__init__.py index 880b2c587ff..8f9a07a3223 100644 --- a/addons/hr_timesheet/models/__init__.py +++ b/addons/hr_timesheet/models/__init__.py @@ -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 diff --git a/addons/hr_timesheet/models/project_project.py b/addons/hr_timesheet/models/project_project.py index 3dd7703a71b..648d48e5384 100644 --- a/addons/hr_timesheet/models/project_project.py +++ b/addons/hr_timesheet/models/project_project.py @@ -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 diff --git a/addons/hr_timesheet/models/project_update.py b/addons/hr_timesheet/models/project_update.py new file mode 100644 index 00000000000..a4732fdf9c6 --- /dev/null +++ b/addons/hr_timesheet/models/project_update.py @@ -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 diff --git a/addons/hr_timesheet/views/project_update_views.xml b/addons/hr_timesheet/views/project_update_views.xml index b90db9ddd3a..8300765427e 100644 --- a/addons/hr_timesheet/views/project_update_views.xml +++ b/addons/hr_timesheet/views/project_update_views.xml @@ -12,5 +12,18 @@ + + project.update.view.kanban.inherit + project.update + + + + +
+ / (%) +
+
+
+
diff --git a/addons/project/models/project_project.py b/addons/project/models/project_project.py index fb99ddd59cc..5e4f1fab96f 100644 --- a/addons/project/models/project_project.py +++ b/addons/project/models/project_project.py @@ -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, diff --git a/addons/project/models/project_update.py b/addons/project/models/project_update.py index 7351cf306f1..7bc2ff393bf 100644 --- a/addons/project/models/project_update.py +++ b/addons/project/models/project_update.py @@ -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): diff --git a/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.xml b/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.xml index 33eba4a6737..47829013622 100644 --- a/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.xml +++ b/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.xml @@ -10,7 +10,7 @@ >
- +
Progress
+
+ + / Tasks (%) + +
Date