From 5985dd502f93252340e10cd840fc0a128f9913fb Mon Sep 17 00:00:00 2001 From: maud-buchwalter Date: Fri, 25 Sep 2026 18:48:45 +0200 Subject: [PATCH] [ADD] project_dashboard_billable_hours display billable hours on project dashboard view --- project_dashboard_billable_hours/__init__.py | 1 + .../__manifest__.py | 32 ++++++ project_dashboard_billable_hours/i18n/fr.po | 57 ++++++++++ .../models/__init__.py | 1 + .../models/project_project.py | 106 ++++++++++++++++++ 5 files changed, 197 insertions(+) create mode 100644 project_dashboard_billable_hours/__init__.py create mode 100644 project_dashboard_billable_hours/__manifest__.py create mode 100644 project_dashboard_billable_hours/i18n/fr.po create mode 100644 project_dashboard_billable_hours/models/__init__.py create mode 100644 project_dashboard_billable_hours/models/project_project.py diff --git a/project_dashboard_billable_hours/__init__.py b/project_dashboard_billable_hours/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/project_dashboard_billable_hours/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/project_dashboard_billable_hours/__manifest__.py b/project_dashboard_billable_hours/__manifest__.py new file mode 100644 index 0000000..41f2ae0 --- /dev/null +++ b/project_dashboard_billable_hours/__manifest__.py @@ -0,0 +1,32 @@ +# Copyright 2026 Maud Buchwalter (Elabore) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "project_dashboard_billable_hours", + "version": "18.0.1.0.0", + "author": "Elabore", + "website": "https://git.elabore.coop/elabore/project-tools", + "maintainer": "Maud Buchwalter", + "license": "AGPL-3", + "category": "Tools", + "summary": "Display billable hours (timesheets linked to a SO line) in the project dashboard panel", + # any module necessary for this one to work correctly + "depends": ["sale_timesheet"], + "qweb": [ + # "static/src/xml/*.xml", + ], + "external_dependencies": { + "python": [], + }, + # always loaded + "data": [], + # only loaded in demonstration mode + "demo": [], + "js": [], + "css": [], + "installable": True, + # Install this module automatically if all dependency have been previously + # and independently installed. Used for synergetic or glue modules. + "auto_install": False, + "application": False, +} diff --git a/project_dashboard_billable_hours/i18n/fr.po b/project_dashboard_billable_hours/i18n/fr.po new file mode 100644 index 0000000..558c2dd --- /dev/null +++ b/project_dashboard_billable_hours/i18n/fr.po @@ -0,0 +1,57 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_dashboard_billable_hours +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-25 16:40+0000\n" +"PO-Revision-Date: 2026-09-25 16:40+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: project_dashboard_billable_hours +#. odoo-python +#: code:addons/project_dashboard_billable_hours/models/project_project.py:0 +msgid "%(billable)s %(uom_name)s" +msgstr "%(billable)s %(uom_name)s" + +#. module: project_dashboard_billable_hours +#. odoo-python +#: code:addons/project_dashboard_billable_hours/models/project_project.py:0 +msgid "%(billable)s / %(effective)s %(uom_name)s" +msgstr "%(billable)s / %(effective)s %(uom_name)s" + +#. module: project_dashboard_billable_hours +#. odoo-python +#: code:addons/project_dashboard_billable_hours/models/project_project.py:0 +msgid "%(billable)s / %(effective)s %(uom_name)s (%(billable_rate)s%%)" +msgstr "%(billable)s / %(effective)s %(uom_name)s (%(billable_rate)s%%)" + +#. module: project_dashboard_billable_hours +#. odoo-python +#: code:addons/project_dashboard_billable_hours/models/project_project.py:0 +msgid "%(name)s's Billable Timesheets" +msgstr "%(name)s's Feuille de temps facturable" + +#. module: project_dashboard_billable_hours +#. odoo-python +#: code:addons/project_dashboard_billable_hours/models/project_project.py:0 +#: model:ir.model.fields,field_description:project_dashboard_billable_hours.field_project_project__billable_hours +msgid "Billable Time" +msgstr "Temps facturable" + +#. module: project_dashboard_billable_hours +#: model:ir.model,name:project_dashboard_billable_hours.model_project_project +msgid "Project" +msgstr "Projet" + +#. module: project_dashboard_billable_hours +#: model:ir.model.fields,field_description:project_dashboard_billable_hours.field_project_project__smart_search +msgid "Smart Search" +msgstr "" diff --git a/project_dashboard_billable_hours/models/__init__.py b/project_dashboard_billable_hours/models/__init__.py new file mode 100644 index 0000000..f211b6d --- /dev/null +++ b/project_dashboard_billable_hours/models/__init__.py @@ -0,0 +1 @@ +from . import project_project \ No newline at end of file diff --git a/project_dashboard_billable_hours/models/project_project.py b/project_dashboard_billable_hours/models/project_project.py new file mode 100644 index 0000000..dbdd0ef --- /dev/null +++ b/project_dashboard_billable_hours/models/project_project.py @@ -0,0 +1,106 @@ +from odoo import api, fields, models + + +class Project(models.Model): + _inherit = "project.project" + + billable_hours = fields.Float( + string="Billable Time", + compute="_compute_billable_hours", + compute_sudo=True, + groups="hr_timesheet.group_hr_timesheet_user", + ) + + @api.depends('timesheet_ids.unit_amount', 'timesheet_ids.so_line') + def _compute_billable_hours(self): + print("LALAALALALALALLA", self._ids) + print("LALAALALALALALLA", self.ids) + if not any(self._ids): + for project in self: + filtered_timesheets = project.timesheet_ids.filtered( + lambda t: t.so_line + ) + project.billable_hours = sum( + filtered_timesheets.mapped("unit_amount") + ) + return + + timesheet_read_group = self.env["account.analytic.line"]._read_group( + [ + ("project_id", "in", self.ids), + ("so_line", "!=", False), + ], + ["project_id"], + ["unit_amount:sum"], + ) + + timesheets_per_project = { + project.id: unit_amount for project, unit_amount in timesheet_read_group + } + for project in self: + project.billable_hours = timesheets_per_project.get(project.id, 0.0) + + + + + def action_project_billable_timesheets(self): + self.ensure_one() + action = self.action_billable_time_button() # sale_timesheet + action['domain'] = [('project_id', '=', self.id), ('so_line', '!=', False)] + action['display_name'] = self.env._("%(name)s's Billable Timesheets", name=self.name) + return action + + + def _get_stat_buttons(self): + buttons = super()._get_stat_buttons() + if not (self.allow_billable and 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 + billable = self.billable_hours / uom_ratio + effective = self.total_timesheet_time / uom_ratio + + color = "" + if billable: + number = f"{round(billable)} / {round(effective)} {encode_uom.name}" + billable_rate = round(100 * billable / effective) + if billable_rate < 50: + number = self.env._( + "%(billable)s / %(effective)s %(uom_name)s", + effective=round(effective), + billable=round(billable), + uom_name=encode_uom.name, + ) + color = "text-danger" + else: + number = self.env._( + "%(billable)s / %(effective)s %(uom_name)s (%(billable_rate)s%%)", + effective=round(effective), + billable=round(billable), + uom_name=encode_uom.name, + billable_rate=billable_rate, + ) + if billable_rate < 80: + color = "text-warning" + else: + color = "text-success" + else: + number = self.env._( + "%(billable)s %(uom_name)s", + billable=round(billable), + uom_name=encode_uom.name, + ) + + buttons.append({ + "icon": f"money {color}", + "text": self.env._("Billable Time"), + "number": number, + "action_type": "object", + "action": "action_project_billable_timesheets", + "show": True, + "sequence": 3, + }) + return buttons \ No newline at end of file