[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 8f87e102a9 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) <bedo@odoo.com>
This commit is contained in:
Levi Siuzdak (sile)
2024-01-25 13:42:45 +00:00
parent 8afdad1ac1
commit 8faed465fc
5 changed files with 63 additions and 9 deletions
+2 -1
View File
@@ -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,
+15 -7
View File
@@ -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)
+19
View File
@@ -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.")
+3 -1
View File
@@ -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']\
@@ -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)