From da1b2c0c3cdc6399b6ef24f071af3ad8f905c7d8 Mon Sep 17 00:00:00 2001 From: "Cedric Prieels (cpr)" Date: Wed, 6 Apr 2022 07:55:14 +0000 Subject: [PATCH] [IMP] hr_timesheet,timesheet_grid: add new pivot measures and generic improvements A new report was created for timesheets, also introducing new measures regarding the (non-)billable time (percentage) and the timesheets revenues/costs. Some general improvements were furthermore introduced in the timesheet apps. See task-2783955 closes odoo/odoo#88072 Related: odoo/upgrade#3509 Related: odoo/enterprise#25922 Signed-off-by: Laurent Stukkens (ltu) --- addons/hr_timesheet/report/__init__.py | 4 + .../report/hr_timesheet_report_view.xml | 251 +++++++++--------- .../report/timesheets_analysis_report.py | 73 +++++ .../security/hr_timesheet_security.xml | 5 + .../hr_timesheet/security/ir.model.access.csv | 2 + addons/sale_timesheet/__init__.py | 1 + addons/sale_timesheet/__manifest__.py | 1 + addons/sale_timesheet/models/account.py | 22 +- addons/sale_timesheet/report/__init__.py | 4 + .../report/timesheets_analysis_report.py | 41 +++ .../report/timesheets_analysis_views.xml | 111 ++++++++ .../views/hr_timesheet_views.xml | 76 ------ 12 files changed, 381 insertions(+), 210 deletions(-) create mode 100644 addons/hr_timesheet/report/timesheets_analysis_report.py create mode 100644 addons/sale_timesheet/report/__init__.py create mode 100644 addons/sale_timesheet/report/timesheets_analysis_report.py create mode 100644 addons/sale_timesheet/report/timesheets_analysis_views.xml diff --git a/addons/hr_timesheet/report/__init__.py b/addons/hr_timesheet/report/__init__.py index e56203fb759..6c55a4362c2 100644 --- a/addons/hr_timesheet/report/__init__.py +++ b/addons/hr_timesheet/report/__init__.py @@ -1 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import project_report +from . import timesheets_analysis_report diff --git a/addons/hr_timesheet/report/hr_timesheet_report_view.xml b/addons/hr_timesheet/report/hr_timesheet_report_view.xml index 8ac0985faec..f1b47aec754 100644 --- a/addons/hr_timesheet/report/hr_timesheet_report_view.xml +++ b/addons/hr_timesheet/report/hr_timesheet_report_view.xml @@ -1,12 +1,120 @@ + + timesheets.analysis.report.pivot + timesheets.analysis.report + + + + + + + + + + + + timesheets.analysis.report.graph + timesheets.analysis.report + + + + + + + + + + + timesheets.analysis.report.pivot + timesheets.analysis.report + + + + + + + + + + + + timesheets.analysis.report.graph + timesheets.analysis.report + + + + + + + + + + + timesheets.analysis.report.pivot + timesheets.analysis.report + + + + + + + + + + + + + timesheets.analysis.report.graph + timesheets.analysis.report + + + + + + + + + + + + timesheets.analysis.report.search + timesheets.analysis.report + + + + + + + + + + + + + + + + + + + + + + + + + + + Timesheets by Employee - account.analytic.line + timesheets.analysis.report [('project_id', '!=', False)] - {'grid_range': 'week'} - + {} + + pivot,graph

No data yet! @@ -20,66 +128,25 @@ pivot - + graph - + - - - tree - - - - - - - form - - - - - - - kanban - - - - - - account.analytic.line.pivot - account.analytic.line - - primary - - - - - - - - - account.analytic.line.graph - account.analytic.line - - primary - - - - - + Timesheets by Project - account.analytic.line + timesheets.analysis.report [('project_id', '!=', False)] - {'grid_range': 'week'} - + {} + + pivot,graph

No data yet! @@ -93,68 +160,25 @@ pivot - + graph - + - - - tree - - - - - - - form - - - - - - - kanban - - - - - - account.analytic.line.pivot - account.analytic.line - - primary - - - - - - - - - account.analytic.line.graph - account.analytic.line - - primary - - - - - - - + Timesheets by Task - account.analytic.line + timesheets.analysis.report [('project_id', '!=', False)] - {'grid_range': 'week'} - + {} + + pivot,graph

No data yet! @@ -168,35 +192,14 @@ pivot - + graph - - - - - - - tree - - - - - - - form - - - - - - - kanban - + diff --git a/addons/hr_timesheet/report/timesheets_analysis_report.py b/addons/hr_timesheet/report/timesheets_analysis_report.py new file mode 100644 index 00000000000..265bada94e0 --- /dev/null +++ b/addons/hr_timesheet/report/timesheets_analysis_report.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from psycopg2 import sql + +from odoo import api, tools, fields, models + + +class TimesheetsAnalysisReport(models.Model): + _name = "timesheets.analysis.report" + _description = "Timesheets Analysis Report" + _auto = False + + name = fields.Char("Description", readonly=True) + user_id = fields.Many2one("res.users", string="User", readonly=True) + project_id = fields.Many2one("project.project", string="Project", readonly=True) + task_id = fields.Many2one("project.task", string="Task", readonly=True) + ancestor_task_id = fields.Many2one("project.task", string="Ancestor Task", readonly=True) + employee_id = fields.Many2one("hr.employee", string="Employee", readonly=True) + manager_id = fields.Many2one("hr.employee", "Manager", readonly=True) + company_id = fields.Many2one("res.company", string="Company", readonly=True) + department_id = fields.Many2one("hr.department", string="Department", readonly=True) + currency_id = fields.Many2one('res.currency', string="Currency", readonly=True) + date = fields.Date("Date", readonly=True) + amount = fields.Monetary("Amount", readonly=True) + unit_amount = fields.Float("Hours Spent", readonly=True) + + @property + def _table_query(self): + return "%s %s %s" % (self._select(), self._from(), self._where()) + + @api.model + def _select(self): + return """ + SELECT + A.id AS id, + A.name AS name, + A.user_id AS user_id, + A.project_id AS project_id, + A.task_id AS task_id, + A.ancestor_task_id AS ancestor_task_id, + A.employee_id AS employee_id, + A.manager_id AS manager_id, + A.company_id AS company_id, + A.department_id AS department_id, + A.currency_id AS currency_id, + A.date AS date, + A.amount AS amount, + A.unit_amount AS unit_amount + """ + + @api.model + def _from(self): + return "FROM account_analytic_line A" + + @api.model + def _where(self): + return "WHERE A.project_id IS NOT NULL" + + @api.model + def _get_view(self, view_id=None, view_type='form', **options): + result = super()._get_view(view_id, view_type, **options) + if view_type in ["pivot", "graph"] and self.env.company.timesheet_encode_uom_id == self.env.ref("uom.product_uom_day"): + result["arch"] = self.env["account.analytic.line"]._apply_time_label(result["arch"], related_model=self._name) + return result + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute( + sql.SQL("CREATE or REPLACE VIEW {} as ({})").format( + sql.Identifier(self._table), + sql.SQL(self._table_query) + ) + ) diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 30081f6ff6a..f58b7fa4df0 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -86,5 +86,10 @@ + + Timesheets Analysis Report multi-company + + [('company_id', 'in', company_ids)] + diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv index cb11fb119f7..f12cd6b6ea7 100644 --- a/addons/hr_timesheet/security/ir.model.access.csv +++ b/addons/hr_timesheet/security/ir.model.access.csv @@ -4,3 +4,5 @@ access_account_analytic_user,analytic.account.analytic.timesheet.user,analytic.m access_uom_uom_hr_timesheet,uom.uom.timesheet.user,uom.model_uom_uom,hr_timesheet.group_hr_timesheet_user,1,0,0,0 access_project_project,project.project.timesheet.user,model_project_project,hr_timesheet.group_hr_timesheet_user,1,0,0,0 access_project_task,project.task.timesheet.user,model_project_task,hr_timesheet.group_hr_timesheet_user,1,1,0,0 +access_timesheets_analysis_report_manager,timesheets.analysis.report,model_timesheets_analysis_report,hr_timesheet.group_timesheet_manager,1,1,1,1 +access_timesheets_analysis_report_user,timesheets.analysis.report,model_timesheets_analysis_report,hr_timesheet.group_hr_timesheet_user,1,0,1,0 diff --git a/addons/sale_timesheet/__init__.py b/addons/sale_timesheet/__init__.py index db436c2f782..3cad7f0fc48 100644 --- a/addons/sale_timesheet/__init__.py +++ b/addons/sale_timesheet/__init__.py @@ -3,6 +3,7 @@ from . import controllers from . import models from . import wizard +from . import report from odoo import api, SUPERUSER_ID diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py index 71b950acc41..0a926f0cac1 100644 --- a/addons/sale_timesheet/__manifest__.py +++ b/addons/sale_timesheet/__manifest__.py @@ -16,6 +16,7 @@ have real delivered quantities in sales orders. 'depends': ['sale_project', 'hr_timesheet'], 'data': [ 'data/sale_service_data.xml', + 'report/timesheets_analysis_views.xml', 'security/ir.model.access.csv', 'security/sale_timesheet_security.xml', 'views/account_invoice_views.xml', diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 5728e609289..b204126d6f6 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -6,6 +6,17 @@ from odoo.exceptions import UserError from odoo import api, fields, models, _ from odoo.osv import expression +TIMESHEET_INVOICE_TYPES = [ + ('billable_time', 'Billed on Timesheets'), + ('billable_fixed', 'Billed at a Fixed price'), + ('billable_milestones', 'Billed on Milestones'), + ('billable_manual', 'Billed Manually'), + ('non_billable', 'Non Billable Tasks'), + ('timesheet_revenues', 'Timesheet Revenues'), + ('service_revenues', 'Service Revenues'), + ('other_revenues', 'Other Revenues'), + ('other_costs', 'Other Costs'), +] class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' @@ -14,16 +25,7 @@ class AccountAnalyticLine(models.Model): domain = super(AccountAnalyticLine, self)._default_sale_line_domain() return expression.OR([domain, [('qty_delivered_method', '=', 'timesheet')]]) - timesheet_invoice_type = fields.Selection([ - ('billable_time', 'Billed on Timesheets'), - ('billable_fixed', 'Billed at a Fixed price'), - ('billable_milestones', 'Billed on Milestones'), - ('billable_manual', 'Billed Manually'), - ('non_billable', 'Non Billable Tasks'), - ('timesheet_revenues', 'Timesheet Revenues'), - ('service_revenues', 'Service Revenues'), - ('other_revenues', 'Other Revenues'), - ('other_costs', 'Other Costs')], string="Billing Type", + timesheet_invoice_type = fields.Selection(TIMESHEET_INVOICE_TYPES, string="Billable Type", compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True) commercial_partner_id = fields.Many2one('res.partner', compute="_compute_commercial_partner") timesheet_invoice_id = fields.Many2one('account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet") diff --git a/addons/sale_timesheet/report/__init__.py b/addons/sale_timesheet/report/__init__.py new file mode 100644 index 00000000000..50dc678f4bc --- /dev/null +++ b/addons/sale_timesheet/report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import timesheets_analysis_report diff --git a/addons/sale_timesheet/report/timesheets_analysis_report.py b/addons/sale_timesheet/report/timesheets_analysis_report.py new file mode 100644 index 00000000000..549ace946fb --- /dev/null +++ b/addons/sale_timesheet/report/timesheets_analysis_report.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + +from odoo.addons.sale_timesheet.models.account import TIMESHEET_INVOICE_TYPES + + +class TimesheetsAnalysisReport(models.Model): + _inherit = "timesheets.analysis.report" + + order_id = fields.Many2one("sale.order", string="Sales Order", readonly=True) + so_line = fields.Many2one("sale.order.line", string="Sales Order Item", readonly=True) + timesheet_invoice_type = fields.Selection(TIMESHEET_INVOICE_TYPES, string="Billable Type", readonly=True) + timesheet_invoice_id = fields.Many2one("account.move", string="Invoice", readonly=True, help="Invoice created from the timesheet") + timesheet_revenues = fields.Float("Timesheet Revenues", readonly=True, help="Number of hours spent multiplied by the unit price per hour/day.") + margin = fields.Float("Margin", readonly=True, help="Timesheets revenues minus the costs") + billable_time = fields.Float("Billable Hours", readonly=True, help="Number of hours/days linked to a SOL.") + non_billable_time = fields.Float("Non-billable Hours", readonly=True, help="Number of hours/days not linked to a SOL.") + + @property + def _table_query(self): + return """ + SELECT A.*, + (timesheet_revenues + A.amount) AS margin, + (A.unit_amount - billable_time) AS non_billable_time + FROM ( + %s %s %s + ) A + """ % (self._select(), self._from(), self._where()) + + @api.model + def _select(self): + return super()._select() + """, + A.order_id AS order_id, + A.so_line AS so_line, + A.timesheet_invoice_type AS timesheet_invoice_type, + A.timesheet_invoice_id AS timesheet_invoice_id, + (-1 * A.unit_amount * A.amount) AS timesheet_revenues, + CASE WHEN order_id IS NULL THEN 0 ELSE unit_amount END AS billable_time + """ diff --git a/addons/sale_timesheet/report/timesheets_analysis_views.xml b/addons/sale_timesheet/report/timesheets_analysis_views.xml new file mode 100644 index 00000000000..5a7c6b146b0 --- /dev/null +++ b/addons/sale_timesheet/report/timesheets_analysis_views.xml @@ -0,0 +1,111 @@ + + + + + timesheets.analysis.report.pivot + timesheets.analysis.report + + + + + + + + + + + timesheets.analysis.report.graph + timesheets.analysis.report + + + + + + + + + + + timesheets.analysis.report.pivot + timesheets.analysis.report + + + + + + + + + + + + timesheets.analysis.report.graph + timesheets.analysis.report + + + + + + + + + + + timesheets.analysis.report.search + timesheets.analysis.report + + + + + + + + + + + + + + + + + + + + + + + Timesheets by Billing Type + timesheets.analysis.report + [('project_id', '!=', False)] + +

+ No data yet! +

+

Review your timesheets by billing type and make sure your time is billable.

+
+ + pivot,graph +
+ + + + pivot + + + + + + + graph + + + + + + +
diff --git a/addons/sale_timesheet/views/hr_timesheet_views.xml b/addons/sale_timesheet/views/hr_timesheet_views.xml index 4eac9a14c34..a2a6487be3f 100644 --- a/addons/sale_timesheet/views/hr_timesheet_views.xml +++ b/addons/sale_timesheet/views/hr_timesheet_views.xml @@ -160,82 +160,6 @@ - - - Timesheets by Billing Type - account.analytic.line - tree,form,pivot,graph,kanban - [('project_id', '!=', False)] - { - 'pivot_row_groupby': ['date:month'], - } - -

- No data yet! -

- Analyze the projects and tasks on which your employees spend their time.
- Evaluate which part is billable and what costs it represents. -

-
- -
- - - account.analytic.line.graph - account.analytic.line - - primary - - - - - - - - - - - form - - - - - - - kanban - - - - - - - pivot - - - - - - - graph - - - - - - - tree - - - - - -