From 8faed465fc78254f8619ffe2c9e70236f7dc5611 Mon Sep 17 00:00:00 2001 From: "Levi Siuzdak (sile)" Date: Thu, 18 Jan 2024 11:41:28 +0100 Subject: [PATCH] [FIX] hr{,_contract,_holidays}: default to company calendar Versions -------- - 17.0+ Steps ----- 1. Go to Employees app as admin; 2. clear the "Working Hours" field & save; 3. go to Time Off app. Issue ----- Odoo Server Error. Cause ----- Commit 8f87e102a95412aa7dd1b0ce07365d9d3bbdba6a added the `_get_consumed_leaves` method to `hr.employee`, which calls on `resource.calendar` methods with `ensure_one()` active. The `resource_calendar_id` is not a required field for employees, so an error occurs when these methods are called on an empty record. Solution -------- Default to `company_id.resource_calendar_id` where a calendar is expected. Also fixes a potential issue in `hr_contract` when getting attendances between a time interval that includes multiple contracts using different calendars. opw-3665412 closes odoo/odoo#149908 Signed-off-by: Bertrand Dossogne (bedo) --- addons/hr/models/hr_employee.py | 3 ++- addons/hr_contract/models/hr_employee.py | 22 +++++++++++------ addons/hr_contract/tests/test_resource.py | 19 +++++++++++++++ addons/hr_holidays/models/hr_leave_type.py | 4 +++- .../hr_holidays/tests/test_leave_requests.py | 24 +++++++++++++++++++ 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index 34aaab56415..8327497207f 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -517,7 +517,8 @@ class HrEmployeePrivate(models.Model): def _get_calendar_attendances(self, date_from, date_to): self.ensure_one() employee_timezone = timezone(self.tz) if self.tz else None - return self.resource_calendar_id\ + calendar = self.resource_calendar_id or self.company_id.resource_calendar_id + return calendar\ .with_context(employee_timezone=employee_timezone)\ .get_work_duration_data( date_from, diff --git a/addons/hr_contract/models/hr_employee.py b/addons/hr_contract/models/hr_employee.py index 4c5b17d75f2..c765a1654a8 100644 --- a/addons/hr_contract/models/hr_employee.py +++ b/addons/hr_contract/models/hr_employee.py @@ -145,13 +145,21 @@ class Employee(models.Model): valid_contracts = self.sudo()._get_contracts(date_from, date_to, states=['open', 'close']) if not valid_contracts: return super()._get_calendar_attendances(date_from, date_to) - employee_timezone = timezone(self.tz) if self.tz else None - return valid_contracts.resource_calendar_id\ - .with_context(employee_timezone=employee_timezone)\ - .get_work_duration_data( - date_from, - date_to, - domain=[('company_id', 'in', [False, self.company_id.id])]) + employee_tz = timezone(self.tz) if self.tz else None + duration_data = {'days': 0, 'hours': 0} + for contract in valid_contracts: + contract_start = datetime.combine(contract.date_start, time.min, employee_tz) + contract_end = datetime.combine(contract.date_end or date.max, time.max, employee_tz) + calendar = contract.resource_calendar_id or contract.company_id.resource_calendar_id + contract_duration_data = calendar\ + .with_context(employee_timezone=employee_tz)\ + .get_work_duration_data( + max(date_from, contract_start), + min(date_to, contract_end), + domain=[('company_id', 'in', [False, contract.company_id.id])]) + duration_data['days'] += contract_duration_data['days'] + duration_data['hours'] += contract_duration_data['hours'] + return duration_data def write(self, vals): res = super().write(vals) diff --git a/addons/hr_contract/tests/test_resource.py b/addons/hr_contract/tests/test_resource.py index 3549124aeed..4cfd4a1582f 100644 --- a/addons/hr_contract/tests/test_resource.py +++ b/addons/hr_contract/tests/test_resource.py @@ -111,3 +111,22 @@ class TestResource(TestContractCommon): work_intervals, _ = self.employee.resource_id._get_valid_work_intervals(start, end) sum_work_intervals = sum_intervals(work_intervals[self.employee.resource_id.id]) self.assertEqual(75, sum_work_intervals, "Sum of the work intervals for the employee should be 35h+40h = 75h") + + def test_multi_contract_attendance(self): + """ Verify whether retrieving an employee's calendar attendances can + handle multiple contracts with different calendars. + """ + + date_from = utc.localize(datetime(2021, 10, 1, 0, 0, 0)) + date_to = utc.localize(datetime(2021, 11, 30, 0, 0, 0)) + + attendances = self.employee._get_calendar_attendances(date_from, date_to) + self.assertEqual(21 * 7, attendances['hours'], + "Attendances should only include running or finished contracts.") + + self.contract_cdd.state = 'close' + self.contract_cdi.state = 'open' + + attendances = self.employee._get_calendar_attendances(date_from, date_to) + self.assertEqual(21 * 7 + 21 * 8, attendances['hours'], + "Attendances should add up multiple contracts with varying work weeks.") diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index fc68036fbce..6439d11831e 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -462,9 +462,11 @@ class HolidaysType(models.Model): closest_allocation_remaining += allocations_leaves_consumed[employee][leave_type][closest_allocation]['virtual_remaining_leaves'] if closest_allocation.date_to: closest_allocation_expire = format_date(self.env, closest_allocation.date_to) + calendar = employee.resource_calendar_id\ + or employee.company_id.resource_calendar_id # closest_allocation_duration corresponds to the time remaining before the allocation expires closest_allocation_duration =\ - employee.resource_calendar_id._attendance_intervals_batch( + calendar._attendance_intervals_batch( datetime.combine(closest_allocation.date_to, time.min).replace(tzinfo=pytz.UTC), datetime.combine(target_date, time.max).replace(tzinfo=pytz.UTC))\ if leave_type.request_unit in ['hour']\ diff --git a/addons/hr_holidays/tests/test_leave_requests.py b/addons/hr_holidays/tests/test_leave_requests.py index 7959d158531..6a39d262dae 100644 --- a/addons/hr_holidays/tests/test_leave_requests.py +++ b/addons/hr_holidays/tests/test_leave_requests.py @@ -1082,3 +1082,27 @@ class TestLeaveRequests(TestHrHolidaysCommon): for leave_validation_type in types: with self.assertRaises(RuntimeError), self.env.cr.savepoint(): run_validation_flow(leave_validation_type) + + @freeze_time('2024-01-18') + def test_undefined_working_hours(self): + """ Ensure time-off can also be allocated without ResourceCalendar. """ + employee = self.employee_emp + employee.resource_calendar_id = False + self.env['hr.leave.allocation'].create({ + 'name': 'Annual Time Off', + 'employee_id': employee.id, + 'holiday_status_id': self.holidays_type_4.id, + 'number_of_days': 20, + 'state': 'confirm', + 'date_from': '2024-01-01', + 'date_to': '2024-12-31', + }) + self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Holiday Request', + 'employee_id': employee.id, + 'holiday_status_id': self.holidays_type_4.id, + 'request_date_from': '2024-01-23', + 'request_date_to': '2024-01-27', + }) + holiday_status = self.holidays_type_4.with_user(self.user_employee_id) + self._check_holidays_status(holiday_status, employee, 20.0, 0.0, 20.0, 16.0)