From be47624aa0e1cabdbda5421c8fe796cec1bcecac Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 8 Oct 2026 17:17:46 -0300 Subject: [PATCH 1/3] [MIG] hr_holidays_ux: Migration to 20.0 - hr.leave.type is merged into hr.work.entry.type: move pre_approved_instance and its form view there. - hr.leave.holiday_status_id is now work_entry_type_id. - Drop request_unit_half/request_unit_hours, removed in 20.0. - supported_attachment_ids is only in the dashboard form in 20.0: move the visibility override there. - Allocation multi wizard: follow the 20.0 core method, still without the automatic approval. - Add tests for month split, pre-approved flow, allocation reset and the multi allocation wizard. --- hr_holidays_ux/__manifest__.py | 6 +- hr_holidays_ux/models/__init__.py | 2 +- hr_holidays_ux/models/hr_leave.py | 8 +- ...hr_leave_type.py => hr_work_entry_type.py} | 4 +- hr_holidays_ux/tests/__init__.py | 1 + hr_holidays_ux/tests/test_hr_holidays_ux.py | 155 ++++++++++++++++++ hr_holidays_ux/views/hr_leave_type_views.xml | 16 -- hr_holidays_ux/views/hr_leave_views.xml | 16 +- .../views/hr_work_entry_type_views.xml | 13 ++ ..._leave_allocation_generate_multi_wizard.py | 14 +- 10 files changed, 199 insertions(+), 36 deletions(-) rename hr_holidays_ux/models/{hr_leave_type.py => hr_work_entry_type.py} (71%) create mode 100644 hr_holidays_ux/tests/__init__.py create mode 100644 hr_holidays_ux/tests/test_hr_holidays_ux.py delete mode 100644 hr_holidays_ux/views/hr_leave_type_views.xml create mode 100644 hr_holidays_ux/views/hr_work_entry_type_views.xml diff --git a/hr_holidays_ux/__manifest__.py b/hr_holidays_ux/__manifest__.py index 962b603d..8b88f3fd 100644 --- a/hr_holidays_ux/__manifest__.py +++ b/hr_holidays_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Holidays UX", - "version": "19.0.1.1.0", + "version": "20.0.1.0.0", "category": "Human Resources", "sequence": 14, "summary": "Split time off records by month", @@ -32,11 +32,11 @@ ], "data": [ "data/ir_actions_server.xml", - "views/hr_leave_type_views.xml", + "views/hr_work_entry_type_views.xml", "views/hr_leave_views.xml", ], "demo": [], - "installable": False, + "installable": True, "auto_install": False, "application": False, } diff --git a/hr_holidays_ux/models/__init__.py b/hr_holidays_ux/models/__init__.py index 78ad8977..ce518145 100644 --- a/hr_holidays_ux/models/__init__.py +++ b/hr_holidays_ux/models/__init__.py @@ -1,3 +1,3 @@ from . import hr_leave from . import hr_leave_allocation -from . import hr_leave_type +from . import hr_work_entry_type diff --git a/hr_holidays_ux/models/hr_leave.py b/hr_holidays_ux/models/hr_leave.py index d9a44ac8..2a5d7ae4 100644 --- a/hr_holidays_ux/models/hr_leave.py +++ b/hr_holidays_ux/models/hr_leave.py @@ -174,7 +174,7 @@ def write(self, vals): # no overlapping records, safe to create base_vals = { "employee_id": record.employee_id.id, - "holiday_status_id": record.holiday_status_id.id, + "work_entry_type_id": record.work_entry_type_id.id, } # add fields that exist in vals or record, safely @@ -186,8 +186,6 @@ def write(self, vals): "date_to", "name", "description", - "request_unit_half", - "request_unit_hours", ] for field in safe_fields: @@ -226,14 +224,14 @@ def action_approve(self, check_state=True): if ( self.state == "validate" and not self.supported_attachment_ids - and self.holiday_status_id.pre_approved_instance + and self.work_entry_type_id.pre_approved_instance ): self.state = "pre-validate" return res def action_validate(self, check_state=True): res = super().action_validate() - if not self.supported_attachment_ids and self.holiday_status_id.pre_approved_instance: + if not self.supported_attachment_ids and self.work_entry_type_id.pre_approved_instance: self.state = "pre-validate" return res diff --git a/hr_holidays_ux/models/hr_leave_type.py b/hr_holidays_ux/models/hr_work_entry_type.py similarity index 71% rename from hr_holidays_ux/models/hr_leave_type.py rename to hr_holidays_ux/models/hr_work_entry_type.py index 5f9aecbe..8c123b94 100644 --- a/hr_holidays_ux/models/hr_leave_type.py +++ b/hr_holidays_ux/models/hr_work_entry_type.py @@ -1,8 +1,8 @@ from odoo import fields, models -class HrLeaveType(models.Model): - _inherit = "hr.leave.type" +class HrWorkEntryType(models.Model): + _inherit = "hr.work.entry.type" pre_approved_instance = fields.Boolean( help="This instance is necessary when the supported document is added after the leave" diff --git a/hr_holidays_ux/tests/__init__.py b/hr_holidays_ux/tests/__init__.py new file mode 100644 index 00000000..2f28082a --- /dev/null +++ b/hr_holidays_ux/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_holidays_ux diff --git a/hr_holidays_ux/tests/test_hr_holidays_ux.py b/hr_holidays_ux/tests/test_hr_holidays_ux.py new file mode 100644 index 00000000..3b450830 --- /dev/null +++ b/hr_holidays_ux/tests/test_hr_holidays_ux.py @@ -0,0 +1,155 @@ +from datetime import date + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon +from odoo.exceptions import UserError +from odoo.tests import tagged + + +@tagged("post_install", "-at_install") +class TestHrHolidaysUx(TestHrHolidaysCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.no_alloc_type = cls.env["hr.work.entry.type"].create( + { + "name": "UX No Allocation", + "code": "UXNA", + "requires_allocation": False, + "leave_validation_type": "hr", + "request_unit": "day", + "unit_of_measure": "day", + } + ) + cls.pre_approved_type = cls.env["hr.work.entry.type"].create( + { + "name": "UX Pre Approved", + "code": "UXPA", + "requires_allocation": False, + "leave_validation_type": "hr", + "support_document": True, + "pre_approved_instance": True, + "request_unit": "day", + "unit_of_measure": "day", + } + ) + cls.alloc_type = cls.env["hr.work.entry.type"].create( + { + "name": "UX With Allocation", + "code": "UXWA", + "requires_allocation": True, + "leave_validation_type": "hr", + "allocation_validation_type": "manager", + "request_unit": "day", + "unit_of_measure": "day", + } + ) + + def _create_leave(self, date_from, date_to, work_entry_type=None): + # Created by the employee so it is not auto-approved on create + leaves = ( + self.env["hr.leave"] + .with_user(self.user_employee) + .create( + { + "employee_id": self.employee_emp.id, + "work_entry_type_id": (work_entry_type or self.no_alloc_type).id, + "request_date_from": date_from, + "request_date_to": date_to, + } + ) + ) + return leaves.with_env(self.env) + + def _employee_leaves(self): + return self.env["hr.leave"].search([("employee_id", "=", self.employee_emp.id)], order="request_date_from") + + def test_create_splits_by_month(self): + self._create_leave(date(2027, 1, 25), date(2027, 2, 3)) + self.assertEqual( + [(leave.request_date_from, leave.request_date_to) for leave in self._employee_leaves()], + [(date(2027, 1, 25), date(2027, 1, 31)), (date(2027, 2, 1), date(2027, 2, 3))], + ) + + def test_create_same_month_not_split(self): + self._create_leave(date(2027, 1, 18), date(2027, 1, 22)) + self.assertEqual(len(self._employee_leaves()), 1) + + def test_write_splits_by_month(self): + leave = self._create_leave(date(2027, 3, 1), date(2027, 3, 5)) + leave.write({"request_date_to": date(2027, 4, 2)}) + self.assertEqual( + [(leave.request_date_from, leave.request_date_to) for leave in self._employee_leaves()], + [(date(2027, 3, 1), date(2027, 3, 31)), (date(2027, 4, 1), date(2027, 4, 2))], + ) + + def test_write_split_skips_overlapping_period(self): + leave = self._create_leave(date(2027, 3, 1), date(2027, 3, 5)) + existing = self._create_leave(date(2027, 4, 1), date(2027, 4, 2)) + leave.write({"request_date_to": date(2027, 4, 2)}) + self.assertEqual(self._employee_leaves(), leave | existing) + self.assertEqual(leave.request_date_to, date(2027, 3, 31)) + + def test_pre_approved_flow(self): + leave = self._create_leave(date(2027, 3, 8), date(2027, 3, 9), self.pre_approved_type) + leave.action_approve() + self.assertEqual(leave.state, "pre-validate") + self.env["ir.attachment"].create( + {"name": "certificate.txt", "raw": b"doc", "res_model": "hr.leave", "res_id": leave.id} + ) + leave.invalidate_recordset() + self.assertTrue(leave.supported_attachment_ids) + leave.action_post_approve() + self.assertEqual(leave.state, "validate") + + def test_approve_without_pre_approved_instance(self): + leave = self._create_leave(date(2027, 3, 8), date(2027, 3, 9)) + leave.action_approve() + self.assertEqual(leave.state, "validate") + + def test_pre_validate_back_to_confirm_requires_officer(self): + leave = self._create_leave(date(2027, 3, 8), date(2027, 3, 9), self.pre_approved_type) + leave.action_approve() + self.assertEqual(leave.state, "pre-validate") + with self.assertRaisesRegex(UserError, "pre-validated"): + leave.with_user(self.user_employee)._check_approval_update("confirm") + + def _create_allocation(self): + allocation = self.env["hr.leave.allocation"].create( + { + "employee_id": self.employee_emp.id, + "work_entry_type_id": self.alloc_type.id, + "number_of_days": 10, + "date_from": date(2027, 1, 1), + } + ) + allocation.action_approve() + self.assertEqual(allocation.state, "validate") + return allocation + + def test_reset_allocation_to_confirm(self): + allocation = self._create_allocation() + allocation.action_reset_to_confirm() + self.assertEqual(allocation.state, "confirm") + + def test_reset_allocation_with_leaves_taken(self): + allocation = self._create_allocation() + self._create_leave(date(2027, 3, 8), date(2027, 3, 9), self.alloc_type).action_approve() + allocation.invalidate_recordset() + self.assertGreater(allocation.leaves_taken, 0) + with self.assertRaisesRegex(UserError, "already have leaves taken"): + allocation.action_reset_to_confirm() + self.assertEqual(allocation.state, "validate") + + def test_generate_multi_allocations_not_approved(self): + wizard = self.env["hr.leave.allocation.generate.multi.wizard"].create( + { + "work_entry_type_id": self.alloc_type.id, + "employee_ids": [(6, 0, (self.employee_emp | self.employee_hruser).ids)], + "duration": 5, + "date_from": date(2027, 1, 1), + } + ) + action = wizard.action_generate_allocations() + allocations = self.env["hr.leave.allocation"].search(action["domain"]) + self.assertEqual(len(allocations), 2) + self.assertEqual(set(allocations.mapped("state")), {"confirm"}) diff --git a/hr_holidays_ux/views/hr_leave_type_views.xml b/hr_holidays_ux/views/hr_leave_type_views.xml deleted file mode 100644 index 6d57f708..00000000 --- a/hr_holidays_ux/views/hr_leave_type_views.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - hr.leave.type.form - hr.leave.type - - - - - - - - - - diff --git a/hr_holidays_ux/views/hr_leave_views.xml b/hr_holidays_ux/views/hr_leave_views.xml index e2f3a7c7..068da674 100644 --- a/hr_holidays_ux/views/hr_leave_views.xml +++ b/hr_holidays_ux/views/hr_leave_views.xml @@ -4,15 +4,23 @@ hr.leave.form hr.leave + + +