Skip to content

Commit

Permalink
[FIX] hr{,_contract,_holidays}: default to company calendar
Browse files Browse the repository at this point in the history
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 8f87e10 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#149908

Signed-off-by: Bertrand Dossogne (bedo) <bedo@odoo.com>
  • Loading branch information
lvsz committed Jan 25, 2024
1 parent 8afdad1 commit 8faed46
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 9 deletions.
3 changes: 2 additions & 1 deletion addons/hr/models/hr_employee.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,8 @@ def _get_tz_batch(self):
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,
Expand Down
22 changes: 15 additions & 7 deletions addons/hr_contract/models/hr_employee.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,21 @@ def _get_calendar_attendances(self, date_from, date_to):
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)
Expand Down
19 changes: 19 additions & 0 deletions addons/hr_contract/tests/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,22 @@ def test_get_valid_work_intervals(self):
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.")
4 changes: 3 additions & 1 deletion addons/hr_holidays/models/hr_leave_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,11 @@ def get_allocation_data(self, employees, target_date=None):
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']\
Expand Down
24 changes: 24 additions & 0 deletions addons/hr_holidays/tests/test_leave_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,3 +1082,27 @@ def run_validation_flow(leave_validation_type):
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)

0 comments on commit 8faed46

Please sign in to comment.