From fb95436edc754c3596561ae6c059b5ae6281fdeb Mon Sep 17 00:00:00 2001 From: Morita Shinnosuke Date: Wed, 8 Jul 2026 01:21:00 +0000 Subject: [PATCH 1/2] [IMP] hr_timesheet_sheet: configurable approver field Add a company setting that points to a res.users field on hr.employee. The user set in that field for an employee can review the employee's timesheet sheets, in addition to the users granted by the review policy. The field is referenced dynamically by name, so fields added by other modules can be used without depending on them. task-6883 --- .../models/hr_timesheet_sheet.py | 23 ++++++++++++++++++- hr_timesheet_sheet/models/res_company.py | 10 ++++++++ hr_timesheet_sheet/models/res_config.py | 7 ++++++ .../tests/test_hr_timesheet_sheet.py | 15 ++++++++++++ .../views/hr_timesheet_sheet_views.xml | 1 + .../views/res_config_settings_views.xml | 6 +++++ 6 files changed, 61 insertions(+), 1 deletion(-) diff --git a/hr_timesheet_sheet/models/hr_timesheet_sheet.py b/hr_timesheet_sheet/models/hr_timesheet_sheet.py index 729753e71e..4e706359ec 100644 --- a/hr_timesheet_sheet/models/hr_timesheet_sheet.py +++ b/hr_timesheet_sheet/models/hr_timesheet_sheet.py @@ -149,6 +149,14 @@ def _default_department_id(self): compute="_compute_available_task_ids", ) total_time = fields.Float(compute="_compute_total_time", store=True) + approver_id = fields.Many2one( + comodel_name="res.users", + string="Approver", + compute="_compute_approver_id", + help="User able to review this sheet, in addition to the users allowed " + "by the review policy. It is read from the employee field configured " + "as the approver field on the company.", + ) can_review = fields.Boolean( compute="_compute_can_review", search="_search_can_review" ) @@ -183,7 +191,18 @@ def _compute_total_time(self): for sheet in self: sheet.total_time = sum(sheet.mapped("timesheet_ids.unit_amount")) - @api.depends("review_policy") + @api.depends("employee_id", "company_id.timesheet_sheet_approver_field_id") + def _compute_approver_id(self): + for sheet in self: + company = sheet.company_id or self.env.company + field = company.sudo().timesheet_sheet_approver_field_id + approver = self.env["res.users"] + employee = sheet.employee_id + if field and employee and field.name in employee._fields: + approver = employee[field.name] + sheet.approver_id = approver + + @api.depends("review_policy", "approver_id") def _compute_can_review(self): for sheet in self: sheet.can_review = self.env.user in sheet._get_possible_reviewers() @@ -329,6 +348,8 @@ def _get_possible_reviewers(self): res |= self.env.ref("hr.group_hr_manager").users elif self.review_policy == "timesheet_manager": res |= self.env.ref("hr_timesheet.group_hr_timesheet_approver").users + if self.approver_id: + res |= self.approver_id return res def _get_timesheet_sheet_company(self): diff --git a/hr_timesheet_sheet/models/res_company.py b/hr_timesheet_sheet/models/res_company.py index b6582fbca8..51715a6449 100644 --- a/hr_timesheet_sheet/models/res_company.py +++ b/hr_timesheet_sheet/models/res_company.py @@ -37,3 +37,13 @@ class ResCompany(models.Model): default="hr", help="How Timesheet Sheets review is performed.", ) + timesheet_sheet_approver_field_id = fields.Many2one( + comodel_name="ir.model.fields", + string="Timesheet Approver Field", + domain="[('model', '=', 'hr.employee'), ('relation', '=', 'res.users'), " + "('ttype', '=', 'many2one')]", + ondelete="set null", + help="Employee field pointing to a user (res.users). The user found in " + "this field on the employee can review the employee's timesheet sheets, " + "in addition to the review policy.", + ) diff --git a/hr_timesheet_sheet/models/res_config.py b/hr_timesheet_sheet/models/res_config.py index 1e932b60c1..120290c00e 100644 --- a/hr_timesheet_sheet/models/res_config.py +++ b/hr_timesheet_sheet/models/res_config.py @@ -25,3 +25,10 @@ class ResConfig(models.TransientModel): timesheet_sheet_review_policy = fields.Selection( related="company_id.timesheet_sheet_review_policy", readonly=False ) + + timesheet_sheet_approver_field_id = fields.Many2one( + related="company_id.timesheet_sheet_approver_field_id", + readonly=False, + domain="[('model', '=', 'hr.employee'), ('relation', '=', 'res.users'), " + "('ttype', '=', 'many2one')]", + ) diff --git a/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py b/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py index d0cb7ca760..7ac7f3dd27 100644 --- a/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py +++ b/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py @@ -1003,6 +1003,21 @@ def test_review_policy_default(self): sheet.unlink() self.assertFalse(sheet.exists()) + @mute_logger("odoo.models.unlink") + def test_approver_from_configured_field(self): + sheet = Form(self.sheet_model.with_user(self.user)).save() + # No approver field configured yet, so there is no approver. + self.assertFalse(sheet.approver_id) + # Configure which employee field designates the approver. The user set + # in that field on the employee becomes the approver of the sheet and + # can review it. + self.company.timesheet_sheet_approver_field_id = self.env[ + "ir.model.fields" + ]._get("hr.employee", "user_id") + sheet.invalidate_recordset(["approver_id"]) + self.assertEqual(sheet.approver_id, self.employee.user_id) + self.assertIn(self.employee.user_id, sheet._get_possible_reviewers()) + def test_same_week_different_years(self): sheet_form = Form(self.sheet_model.with_user(self.user)) sheet_form.date_start = date(2019, 12, 30) diff --git a/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml b/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml index 4030f6d2ee..47f45d93e4 100644 --- a/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml +++ b/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml @@ -108,6 +108,7 @@ + diff --git a/hr_timesheet_sheet/views/res_config_settings_views.xml b/hr_timesheet_sheet/views/res_config_settings_views.xml index 3dcaeb4ec1..98dcdf7877 100644 --- a/hr_timesheet_sheet/views/res_config_settings_views.xml +++ b/hr_timesheet_sheet/views/res_config_settings_views.xml @@ -38,6 +38,12 @@ required="1" /> + + + From 30ddb3536a1fcef035118967002593bbfca94507 Mon Sep 17 00:00:00 2001 From: Morita Shinnosuke Date: Tue, 4 Aug 2026 12:28:53 +0000 Subject: [PATCH 2/2] [IMP] hr_timesheet_sheet: resolve approver without a sheet field Read the configured employee field directly in _get_possible_reviewers() instead of exposing it through a computed approver_id field on the sheet. The field was not needed for the feature. Since the employee field name is chosen at runtime, it cannot be declared in @api.depends, so the field could not be stored without going stale, and showing a single approver on the form was misleading: the users allowed by the review policy may review the sheet as well. Document the new setting in readme/CONFIGURE.md. --- hr_timesheet_sheet/README.rst | 13 +++--- .../models/hr_timesheet_sheet.py | 40 +++++++++---------- hr_timesheet_sheet/readme/CONFIGURE.md | 7 ++++ .../static/description/index.html | 38 +++++++++--------- .../tests/test_hr_timesheet_sheet.py | 22 +++++----- .../views/hr_timesheet_sheet_views.xml | 1 - 6 files changed, 65 insertions(+), 56 deletions(-) diff --git a/hr_timesheet_sheet/README.rst b/hr_timesheet_sheet/README.rst index 2e0a9896b4..cf79684164 100644 --- a/hr_timesheet_sheet/README.rst +++ b/hr_timesheet_sheet/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ================== HR Timesheet Sheet ================== @@ -17,7 +13,7 @@ HR Timesheet Sheet .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Ftimesheet-lightgray.png?logo=github @@ -71,6 +67,13 @@ accordingly. For adding more review policies, look at the *hr_timesheet_sheet_policy_xxx* extra modules. +To let a specific user review an employee's sheets in addition to the +users allowed by the review policy, go to *Configuration > Settings > +Timesheet Options* and set **Timesheet Approver Field** to an employee +field pointing to a user. For every sheet, the user found in that field +on the employee is allowed to review it. Leave the setting empty to rely +on the review policy only. + Usage ===== diff --git a/hr_timesheet_sheet/models/hr_timesheet_sheet.py b/hr_timesheet_sheet/models/hr_timesheet_sheet.py index 4e706359ec..3b65921cba 100644 --- a/hr_timesheet_sheet/models/hr_timesheet_sheet.py +++ b/hr_timesheet_sheet/models/hr_timesheet_sheet.py @@ -149,14 +149,6 @@ def _default_department_id(self): compute="_compute_available_task_ids", ) total_time = fields.Float(compute="_compute_total_time", store=True) - approver_id = fields.Many2one( - comodel_name="res.users", - string="Approver", - compute="_compute_approver_id", - help="User able to review this sheet, in addition to the users allowed " - "by the review policy. It is read from the employee field configured " - "as the approver field on the company.", - ) can_review = fields.Boolean( compute="_compute_can_review", search="_search_can_review" ) @@ -191,18 +183,11 @@ def _compute_total_time(self): for sheet in self: sheet.total_time = sum(sheet.mapped("timesheet_ids.unit_amount")) - @api.depends("employee_id", "company_id.timesheet_sheet_approver_field_id") - def _compute_approver_id(self): - for sheet in self: - company = sheet.company_id or self.env.company - field = company.sudo().timesheet_sheet_approver_field_id - approver = self.env["res.users"] - employee = sheet.employee_id - if field and employee and field.name in employee._fields: - approver = employee[field.name] - sheet.approver_id = approver - - @api.depends("review_policy", "approver_id") + @api.depends( + "review_policy", + "employee_id", + "company_id.timesheet_sheet_approver_field_id", + ) def _compute_can_review(self): for sheet in self: sheet.can_review = self.env.user in sheet._get_possible_reviewers() @@ -348,10 +333,21 @@ def _get_possible_reviewers(self): res |= self.env.ref("hr.group_hr_manager").users elif self.review_policy == "timesheet_manager": res |= self.env.ref("hr_timesheet.group_hr_timesheet_approver").users - if self.approver_id: - res |= self.approver_id + res |= self._get_employee_approver() return res + def _get_employee_approver(self): + """Return the user set in the employee field configured on the company. + + `sudo` is required because `ir.model.fields` is not readable by + `base.group_user`. + """ + self.ensure_one() + field = self.company_id.sudo().timesheet_sheet_approver_field_id + if not field or field.name not in self.employee_id._fields: + return self.env["res.users"] + return self.employee_id[field.name] + def _get_timesheet_sheet_company(self): self.ensure_one() employee = self.employee_id diff --git a/hr_timesheet_sheet/readme/CONFIGURE.md b/hr_timesheet_sheet/readme/CONFIGURE.md index 89ebdd4e99..51c5e08398 100644 --- a/hr_timesheet_sheet/readme/CONFIGURE.md +++ b/hr_timesheet_sheet/readme/CONFIGURE.md @@ -11,3 +11,10 @@ accordingly. For adding more review policies, look at the *hr_timesheet_sheet_policy_xxx* extra modules. + +To let a specific user review an employee's sheets in addition to the +users allowed by the review policy, go to *Configuration \> Settings \> +Timesheet Options* and set **Timesheet Approver Field** to an employee +field pointing to a user. For every sheet, the user found in that field +on the employee is allowed to review it. Leave the setting empty to rely +on the review policy only. diff --git a/hr_timesheet_sheet/static/description/index.html b/hr_timesheet_sheet/static/description/index.html index 49009b4855..c7eca4fe9d 100644 --- a/hr_timesheet_sheet/static/description/index.html +++ b/hr_timesheet_sheet/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +HR Timesheet Sheet -
+
+

HR Timesheet Sheet

- - -Odoo Community Association - -
-

HR Timesheet Sheet

-

Beta License: AGPL-3 OCA/timesheet Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/timesheet Translate me on Weblate Try me on Runboat

This module supplies a new screen enabling you to manage your work encoding (timesheet) by period. Timesheet entries are made by employees each day. At the end of the defined period, employees submit their @@ -399,7 +394,7 @@

HR Timesheet Sheet

-

Installation

+

Installation

This module relies on:

  • The OCA module ‘2D matrix for x2many fields’, and can be downloaded @@ -408,7 +403,7 @@

    Installation

-

Configuration

+

Configuration

If you want other default ranges different from weekly, you need to go:

  • In the menu Configuration -> Settings -> Timesheet Options, and @@ -421,9 +416,15 @@

    Configuration

    accordingly.

    For adding more review policies, look at the hr_timesheet_sheet_policy_xxx extra modules.

    +

    To let a specific user review an employee’s sheets in addition to the +users allowed by the review policy, go to Configuration > Settings > +Timesheet Options and set Timesheet Approver Field to an employee +field pointing to a user. For every sheet, the user found in that field +on the employee is allowed to review it. Leave the setting empty to rely +on the review policy only.

-

Usage

+

Usage

If you modify the Details tab, automatically the Summary tab is updated. But if you modify the Summary tab, you need to save in order to have the Details tab updated.

@@ -432,7 +433,7 @@

Usage

the Details tab, please save before.

-

Known issues / Roadmap

+

Known issues / Roadmap

  • The timesheet grid is limited to display a max. of 1M cells, due to a limitation of the tree view limit parameter not being able to @@ -442,7 +443,7 @@

    Known issues / Roadmap

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -450,9 +451,9 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • ForgeFlow
  • Onestein
  • @@ -460,7 +461,7 @@

    Authors

-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -492,6 +493,5 @@

Maintainers

-
diff --git a/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py b/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py index 7ac7f3dd27..f2627540d9 100644 --- a/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py +++ b/hr_timesheet_sheet/tests/test_hr_timesheet_sheet.py @@ -1005,18 +1005,22 @@ def test_review_policy_default(self): @mute_logger("odoo.models.unlink") def test_approver_from_configured_field(self): - sheet = Form(self.sheet_model.with_user(self.user)).save() - # No approver field configured yet, so there is no approver. - self.assertFalse(sheet.approver_id) - # Configure which employee field designates the approver. The user set - # in that field on the employee becomes the approver of the sheet and - # can review it. + # `user_3` is not an HR Officer, so the "hr" review policy alone does + # not allow them to review the sheet. + sheet = Form(self.sheet_model.with_user(self.user_3)).save() + self.assertEqual(sheet.employee_id, self.department_manager) + self.assertEqual(sheet.review_policy, "hr") + self.assertNotIn(self.user_3, sheet._get_possible_reviewers()) + self.assertFalse(sheet.with_user(self.user_3).can_review) + # Designating an employee field as the approver field grants review + # rights to the user it points to, on top of the review policy. self.company.timesheet_sheet_approver_field_id = self.env[ "ir.model.fields" ]._get("hr.employee", "user_id") - sheet.invalidate_recordset(["approver_id"]) - self.assertEqual(sheet.approver_id, self.employee.user_id) - self.assertIn(self.employee.user_id, sheet._get_possible_reviewers()) + self.assertIn(self.user_3, sheet._get_possible_reviewers()) + self.assertTrue(sheet.with_user(self.user_3).can_review) + sheet.unlink() + self.assertFalse(sheet.exists()) def test_same_week_different_years(self): sheet_form = Form(self.sheet_model.with_user(self.user)) diff --git a/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml b/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml index 47f45d93e4..4030f6d2ee 100644 --- a/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml +++ b/hr_timesheet_sheet/views/hr_timesheet_sheet_views.xml @@ -108,7 +108,6 @@ -