From 964c2d9deda8a39446922f320025d47becb58c52 Mon Sep 17 00:00:00 2001 From: Morita Shinnosuke Date: Wed, 22 Jul 2026 03:58:08 +0000 Subject: [PATCH] [6883][IMP] hr_timesheet_sheet: add configurable approver field Port of OCA/timesheet#945: add a configurable approver field so that a user designated by an employee field can review the timesheet sheet, in addition to the users allowed by the review policy. --- .../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 aa748a2..9b34f75 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 b6582fb..51715a6 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 1e932b6..120290c 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 d0cb7ca..7ac7f3d 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 4030f6d..47f45d9 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 3dcaeb4..98dcdf7 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" /> + + +