19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-25 12:00:11 +01:00
parent e1d89e11e3
commit a1f02d8cc7
225 changed files with 2335 additions and 775 deletions

View file

@ -35,3 +35,4 @@ from . import test_leave_type_data
from . import test_multi_contract
from . import test_time_off_allocation_tour
from . import test_flexible_resource_calendar
from . import test_hr_leave_report

View file

@ -112,6 +112,16 @@ class TestHrHolidaysCommon(common.TransactionCase):
self.assertEqual(allocation_data[employee][0][1]['remaining_leaves'],
value, f"Remaining leaves for date '{date}' are incorrect.")
def _take_leave(self, employee, leave_type, date_from, date_to):
leave = self.env['hr.leave'].create({
'name': 'Leave',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': date_from,
'request_date_to': date_to,
})
return leave
def _create_form_test_accrual_allocation(self, leave_type, date_from, employee, accrual_plan, date_to=None, creator_user=None):
allocation = self.env['hr.leave.allocation']
if creator_user:
@ -127,6 +137,21 @@ class TestHrHolidaysCommon(common.TransactionCase):
form.date_to = date_to
return form.record
def _create_form_test_regular_allocation(self, leave_type, date_from, employee, number_of_days, date_to=None, creator_user=None):
allocation = self.env['hr.leave.allocation']
if creator_user:
allocation = allocation.with_user(creator_user)
with Form(allocation, 'hr_holidays.hr_leave_allocation_view_form_manager') as form:
form.name = 'Test regular allocation'
form.allocation_type = 'regular'
form.employee_id = employee
form.holiday_status_id = leave_type
form.date_from = date_from
form.number_of_days_display = number_of_days
if date_to:
form.date_to = date_to
return form.record
class TestHolidayContract(TransactionCase):

View file

@ -76,6 +76,56 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_validation_type': 'no_validation',
'request_unit': 'day',
})
cls.accrual_plan_monthly_end = cls.env['hr.leave.accrual.plan'].create({
'name': 'Accrual Plan For Test',
'is_based_on_worked_time': False,
'accrued_gain_time': 'end',
'carryover_date': 'allocation',
'can_be_carryover': True,
'level_ids': [Command.create({
'start_count': 0,
'added_value_type': 'day',
'added_value': 2,
'frequency': 'monthly',
'action_with_unused_accruals': 'all',
'cap_accrued_time': False,
})],
})
cls.accrual_plan_monthly_end_max_leaves = cls.env['hr.leave.accrual.plan'].create({
'name': 'Accrual Plan For Test',
'is_based_on_worked_time': False,
'accrued_gain_time': 'end',
'carryover_date': 'allocation',
'can_be_carryover': True,
'level_ids': [Command.create({
'start_count': 0,
'added_value_type': 'day',
'added_value': 2,
'frequency': 'monthly',
'action_with_unused_accruals': 'all',
'cap_accrued_time': True,
'maximum_leave': 10,
})],
})
cls.accrual_plan_yearly_max_postponed_days_start = cls.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': '21 days per year, 5 carryover max',
'transition_mode': 'immediately',
'carryover_date': 'year_start',
'accrued_gain_time': 'start',
'can_be_carryover': True,
'level_ids': [
Command.create({
"start_count": 0,
"added_value": 21,
"frequency": "yearly",
"yearly_day": 1,
"yearly_month": "1",
"action_with_unused_accruals": "all",
"carryover_options": "limited",
"postpone_max_days": 5,
})
],
})
def setAllocationCreateDate(self, allocation_id, date):
""" This method is a hack in order to be able to define/redefine the create_date
@ -4500,6 +4550,253 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
self.assert_remaining_leaves_equal(self.leave_type_day, remaining_leaves, self.employee_emp, test_date, digits=3)
self.assertAlmostEqual(allocation.expiring_carryover_days, expiring_days, 2, msg=f'Incorrect number of expiring days for {test_date}')
def test_accrual_allocation_immediate_monthly_start_day(self):
""" Test fix for incorrect accrued days when changing date_from on accrual allocations. """
with freeze_time('2024-11-15'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'carryover_date': 'other',
'carryover_day': 1,
'carryover_month': '1',
'level_ids': [(0, 0, {
'added_value': 2,
'added_value_type': 'day',
'frequency': 'monthly',
'cap_accrued_time': True,
'maximum_leave': 10000,
'start_count': 0,
'start_type': 'day',
'action_with_unused_accruals': 'all',
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': datetime.date(2024, 11, 1),
})
allocation._onchange_date_from()
self.assertAlmostEqual(allocation.number_of_days_display, 2, places=2, msg="Accrued days should be 2 (2 days for Nov).")
allocation.date_from = datetime.date(2024, 10, 1)
allocation._onchange_date_from()
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days_display, 4, places=2, msg="Accrued days should be 4 (4 days for Nov).")
def test_modify_cap_accrued_days(self):
"""
Context: allocation with a 1 level accrual plan which
- Carry all days over
- Accrues monthly at the end of the period
Assert the virtual remaining leaves of the employee drop to `maximum_leave` when setting `cap_accrued_time` of the accrual plan
to `True` and the virtual remaining leaves was bigger than `maximum_leave`
"""
with freeze_time('2020-01-01'):
accrued_days = 2
accrual_plan = self.accrual_plan_monthly_end
leave_type_day = self.leave_type_day
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2020-01-01', self.employee_emp, accrual_plan)
allocation.action_approve()
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assert_remaining_leaves_equal(leave_type_day, 24 * accrued_days, self.employee_emp)
self.assert_remaining_leaves_equal(leave_type_day, 25 * accrued_days, self.employee_emp, date='2022-02-01')
accrual_plan.level_ids.update({'maximum_leave': 21, 'cap_accrued_time': True})
self.assert_remaining_leaves_equal(leave_type_day, 21, self.employee_emp, date='2022-02-01')
with freeze_time('2022-02-01'):
allocation._update_accrual()
self.assert_remaining_leaves_equal(leave_type_day, 21, self.employee_emp)
def test_modify_cap_accrued_days_with_leaves(self):
"""
Context: allocation with a 1 level accrual plan which
- Carry all days over
- Accrues monthly at the end of the period
Assert the virtual remaining leaves of the employee drop to `maximum_leave` when setting `cap_accrued_time` of the accrual plan
to `True` and the virtual remaining leaves was bigger than `maximum_leave`
Adds leaves in the computation (only difference with `test_modify_cap_accrued_days`)
"""
with freeze_time('2020-01-01'):
accrued_days = 2
accrual_plan = self.accrual_plan_monthly_end
leave_type_day = self.leave_type_day
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2020-01-01', self.employee_emp, accrual_plan)
allocation.action_approve()
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assert_remaining_leaves_equal(leave_type_day, before_leave_days := 24 * accrued_days, self.employee_emp)
# 35 days leave
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-02-18')._action_validate()
# 10 days leave
self._take_leave(self.employee_emp, leave_type_day, '2022-03-07', '2022-03-18')._action_validate()
with freeze_time('2022-03-01'):
allocation._update_accrual()
# before_leave_days - 35 days (first leave) + 2 months accrual
self.assert_remaining_leaves_equal(leave_type_day, after_leave := before_leave_days - 35 + 2 * accrued_days, self.employee_emp)
accrual_plan.level_ids.update({'maximum_leave': 21, 'cap_accrued_time': True})
self.assert_remaining_leaves_equal(leave_type_day, min(after_leave, 21), self.employee_emp)
with freeze_time('2022-04-01'):
allocation._update_accrual()
after_leave2 = min(after_leave, 21) - 10
self.assert_remaining_leaves_equal(leave_type_day, min(after_leave2 + accrued_days, 21), self.employee_emp)
self.assert_remaining_leaves_equal(leave_type_day, min(after_leave2 + 12 * accrued_days, 21), self.employee_emp, date='2023-03-01')
def test_get_allocation_actual_future_leaves(self):
"""
Context: allocation with a 1 level accrual plan which
- Carry all days over
- Accrues monthly at the end of the period
- Has maximum 10 leaves
Assert the virtual remaining leaves of the employee allocation are not frozen while taking multiple leaves
and using `get_allocation_data`.
"""
with freeze_time('2019-01-01'):
accrual_plan = self.accrual_plan_monthly_end_max_leaves
leave_type_day = self.leave_type_day
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2019-01-01', self.employee_emp, accrual_plan)
allocation.action_approve()
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp)
# 10 days leave
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-01-14')._action_validate()
# 10 days leave
self._take_leave(self.employee_emp, leave_type_day, '2023-01-02', '2023-01-13')._action_validate()
# 10 days leaves that shouldn't be taken into account in this test
self._take_leave(self.employee_emp, leave_type_day, '2025-10-06', '2025-10-17')._action_validate()
with freeze_time('2023-01-01'):
allocation._update_accrual()
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp)
with freeze_time('2023-02-01'):
allocation._update_accrual()
# 10 days - 10 days (leave) + 2 days (1 month accrual)
self.assert_remaining_leaves_equal(leave_type_day, 2, self.employee_emp)
def test_get_allocation_future_leaves(self):
"""
Context: allocation with a 1 level accrual plan which
- Carry all days over
- Accrues monthly at the end of the period
- Has maximum 10 leaves
Assert the virtual remaining leaves of the employee allocation are not frozen while taking multiple leaves
and using `get_allocation_data` with the `target_date` parameter set in the future.
"""
with freeze_time('2019-01-01'):
accrual_plan = self.accrual_plan_monthly_end_max_leaves
leave_type_day = self.leave_type_day
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2019-01-01', self.employee_emp, accrual_plan)
allocation.action_approve()
with freeze_time('2022-01-01'):
allocation._update_accrual()
# Max number of leaves for the only level of the accrual plan is 10
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp)
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp, date='2022-02-01')
# 10 days leave
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-01-14')._action_validate()
# 10 days leave
self._take_leave(self.employee_emp, leave_type_day, '2023-01-02', '2023-01-13')._action_validate()
# 10 days leaves that shouldn't be taken into account in this test
self._take_leave(self.employee_emp, leave_type_day, '2025-10-06', '2025-10-17')._action_validate()
# Right after spending all the 10 leaves ('2023-01-02' -> '2023-01-13')
# 10 days - 10 days (leave) + 2 days (1 month accrual)
self.assert_remaining_leaves_equal(leave_type_day, 2, self.employee_emp, date='2023-02-01')
def _test_get_allocation_future_leaves_regular(self, regular_before):
"""
Context:
1) Allocation with a 1 level accrual plan which
- Carry all days over
- Accrues monthly at the end of the period
- Has maximum 10 leaves
2) Regular allocation
Assert the virtual remaining leaves of the employee allocation are not frozen while taking multiple leaves
and using `get_allocation_data` with the `target_date` parameter set in the future.
:param regular_before: set the `date_from` of the regular allocation before the `date_from` of the accrual allocation
"""
leave_type_day = self.leave_type_day
if regular_before:
with freeze_time('2018-01-01'):
self._create_form_test_regular_allocation(leave_type_day, '2018-01-01', self.employee_emp, number_of_days=10)
with freeze_time('2019-01-01'):
accrual_plan = self.accrual_plan_monthly_end_max_leaves
accrual_allocation = self._create_form_test_accrual_allocation(leave_type_day, '2019-01-01', self.employee_emp, accrual_plan)
accrual_allocation.action_approve()
if not regular_before:
with freeze_time('2020-01-01'):
self._create_form_test_regular_allocation(leave_type_day, '2020-01-01', self.employee_emp, number_of_days=10)
with freeze_time('2022-01-01'):
accrual_allocation._update_accrual()
# Max number of leaves for the only level of the accrual plan is 10 + 10 for the regular allocation
self.assert_remaining_leaves_equal(leave_type_day, 20, self.employee_emp)
self.assert_remaining_leaves_equal(leave_type_day, 20, self.employee_emp, date='2022-02-01')
# 10 days leave
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-01-14')._action_validate()
# 10 days leave
self._take_leave(self.employee_emp, leave_type_day, '2023-01-02', '2023-01-13')._action_validate()
self.assert_remaining_leaves_equal(leave_type_day, 12, self.employee_emp, date='2023-02-01')
# 10 days leaves that shouldn't be taken into account in this test
self._take_leave(self.employee_emp, leave_type_day, '2023-10-06', '2023-10-17')._action_validate()
# Right after spending all the 10 leaves ('2023-01-02' -> '2023-01-13')
# 10 days - 10 days (leave) + 2 days (1 month accrual)
self.assert_remaining_leaves_equal(leave_type_day, 12, self.employee_emp, date='2023-02-01')
def test_get_allocation_future_leaves_regular1(self):
self._test_get_allocation_future_leaves_regular(regular_before=False)
def test_get_allocation_future_leaves_regular2(self):
self._test_get_allocation_future_leaves_regular(regular_before=True)
def test_accrual_days_left_over_carryover_maximum_with_leaves_around_carryover(self):
with freeze_time('2024-11-25'):
allocation = self._create_form_test_accrual_allocation(
self.leave_type, '2024-01-01', self.employee_emp, self.accrual_plan_yearly_max_postponed_days_start)
allocation.action_approve()
# take 10 days in the past
leave = self._take_leave(self.employee_emp, self.leave_type, '2024-12-09', '2024-12-20')
leave._action_validate()
# take 10 days in January
leave_2 = self._take_leave(self.employee_emp, self.leave_type, '2025-01-06', '2025-01-17')
leave_2._action_validate()
# The remaining leaves on a specific date should be:
# 25/11/2024 to 08/12/2024: 21 days, no leave are deducted
# 09/12/2024 to 31/12/2024: 11 days, the first leave is deducted as its start date is past
# 01/01/2025 to 05/01/2025: 26 days, carryover occured, from the 11 days only 5 are left, then the yearly 21 days are added
# from 06/01/2025: 16 days, the second leave is deducted as its start date is past
assertions = [
('2024-12-01', 21.0),
('2024-12-15', 11.0),
('2025-01-02', 26.0),
('2025-01-06', 16.0),
]
for test_date, expected_remaining_leaves in assertions:
self.assert_remaining_leaves_equal(self.leave_type, expected_remaining_leaves, self.employee_emp, test_date, 2)
def test_accrual_leaves_cancel_cron_with_refused_allocation(self):
""" Test that the _cancel_invalid_leaves cron cancels leaves without valid allocation"""
leave_type = self.env['hr.leave.type'].create({
@ -4537,3 +4834,50 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
allocation.action_refuse()
self.env['hr.leave']._cancel_invalid_leaves()
self.assertEqual(leave.state, 'cancel')
def test_timeoff_allocation_with_unused_accrual_lost(self):
"""
Create an accrual plan:
* Set the accrued gain time to "At the start of the accrual period"
* Set the carry-over time to "At the start of the year"
Create a milestone:
* Set the number of accrued days to 1
* Set the accrual frequency to "monthly" and
the carry over to "None.Accrued time reset to 0"
Create an allocation:
* Set the start date to 2025-01-01
* Set the accrual plan to the one created above
Use future allocations to see the number of days accrued on
2026-02-01(feb). It should be 2.
"""
with freeze_time('2025-01-01'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'level_ids': [Command.create({
'start_count': 0,
'frequency': 'monthly',
'action_with_unused_accruals': 'lost',
})],
})
allocation = self.env['hr.leave.allocation'].create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'date_from': '2025-01-01',
'allocation_type': 'accrual',
'number_of_days': 0,
'already_accrued': False,
})
allocation.action_approve()
assertions = (
# Do not run the update on 2026-01-01 otherwise the bug disappears on 2026-02-01
# ('2026-01-01', 1),
('2026-02-01', 2),
('2026-03-01', 3),
)
for test_date, expected_remaining_leaves in assertions:
with freeze_time(test_date):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, expected_remaining_leaves)

View file

@ -0,0 +1,84 @@
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
class TestHrLeaveReport(TestHrHolidaysCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.overtime_leave_type = cls.env['hr.leave.type'].create({
'name': 'Overtime Type',
'requires_allocation': 'yes',
'leave_validation_type': 'no_validation',
'request_unit': 'hour',
'time_type': 'leave',
})
def test_hr_leave_employee_report(self):
self.env['hr.leave.allocation'].create([
{
'name': 'Overtime',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.overtime_leave_type.id,
'date_from': '2025-12-01',
'number_of_days': '1.875',
},
{
'name': 'Overtime',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.overtime_leave_type.id,
'date_from': '2026-01-01',
'number_of_days': '12.6875',
},
{
'name': 'Overtime',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.overtime_leave_type.id,
'date_from': '2026-02-01',
'number_of_days': '1.5',
},
]).action_approve()
self.env['hr.leave'].create([
{
'employee_id': self.employee_emp.id,
'holiday_status_id': self.overtime_leave_type.id,
'request_date_from': '2025-12-02',
'request_date_to': '2025-12-02',
'request_unit_hours': True,
'request_hour_from': 8,
'request_hour_to': 17,
},
{
'employee_id': self.employee_emp.id,
'holiday_status_id': self.overtime_leave_type.id,
'request_date_from': '2026-01-02',
'request_date_to': '2026-01-02',
'request_unit_hours': True,
'request_hour_from': 8,
'request_hour_to': 17,
},
{
'employee_id': self.employee_emp.id,
'holiday_status_id': self.overtime_leave_type.id,
'request_date_from': '2026-02-02',
'request_date_to': '2026-02-02',
'request_unit_hours': True,
'request_hour_from': 8,
'request_hour_to': 17,
},
]).action_approve()
self.env.flush_all()
domain = [
('employee_id', '=', self.employee_emp.id),
('leave_type', '=', self.overtime_leave_type.id),
]
leave_balance = self.env['hr.leave.employee.type.report'].search(domain)
left_allocation = leave_balance.filtered(lambda l: l.holiday_status == 'left')
taken_allocation = leave_balance.filtered(lambda l: l.holiday_status == 'taken')
self.assertEqual(sum(left_allocation.mapped('number_of_hours')), 104.5)
self.assertEqual(sum(taken_allocation.mapped('number_of_hours')), 24.0)

View file

@ -45,6 +45,8 @@ class TestHrLeaveType(TestHrHolidaysCommon):
with freeze_time('2025-09-03 13:00:00'):
employee._compute_leave_status()
self.assertFalse(employee.is_absent)
self.assertEqual(employee.leave_date_from, leave_0.request_date_from)
self.assertEqual(employee.leave_date_to, leave_0.employee_id._get_first_working_interval(leave_0.date_to).date())
with self.assertRaises(ValidationError):
leave_1 = self.env['hr.leave'].create({

View file

@ -2185,3 +2185,137 @@ class TestLeaveRequests(TestHrHolidaysCommon):
})
self.assertEqual(leave.number_of_hours, 13.0)
def test_group_leave_conflicting_days_computation(self):
"""Test that a group leave that overrides existing approved time off days
correctly computes the duration of each leave.
"""
LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id)
self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).create({
'name': 'Annual Time Off',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_4.id,
'number_of_days': 20,
'date_from': '2026-01-01',
}).action_approve()
# Create existing approved time off: Feb 23 - Feb 24 (2 days) and Feb 26 - Feb 27
leave1, leave2 = self.env['hr.leave'].with_user(self.user_employee_id).create([
{
'name': 'Approved Leave 1',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_4.id,
'request_date_from': '2026-02-23',
'request_date_to': '2026-02-24',
},
{
'name': 'Approved Leave 2',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': '2026-02-26',
'request_date_to': '2026-02-27',
}])
leave1.with_user(self.user_hrmanager_id).action_approve()
self.assertEqual(leave1.number_of_days, 2, "Approved Leave 1 should be 2 days")
leave2.with_user(self.user_hrmanager_id).action_approve()
self.assertEqual(leave2.number_of_days, 2, "Approved Leave 2 should be 2 days")
# Create Training Leave Type
training_type = LeaveType.create({
'name': 'Training',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
})
# Use the Wizard to create a Training for the whole company: Feb 24 - Feb 26
# This overlaps with two days of approved allocated leaves
# Last day of leave 1 and first day of leave 2
leave_wizard_form = Form(self.env['hr.leave.generate.multi.wizard'].with_user(self.user_hrmanager_id))
leave_wizard_form.allocation_mode = 'company'
leave_wizard_form.company_id = self.env.company
leave_wizard_form.holiday_status_id = training_type
leave_wizard_form.date_from = date(2026, 2, 24)
leave_wizard_form.date_to = date(2026, 2, 26)
leave_wizard = leave_wizard_form.save()
leave_wizard.action_generate_time_off()
generated_training = self.env['hr.leave'].search([
('employee_id', '=', self.employee_emp_id),
('holiday_status_id', '=', training_type.id),
('request_date_from', '=', '2026-02-24')
])
# ASSERTS
# Assert correct duration calculation for the training leave
self.assertEqual(generated_training.number_of_days, 3.0,
"The training (Feb 25-27) should be 3 days, since it overrides other leaves.")
# Assert the original time off was split correctly
# It should now only cover Feb 23 (1 day) and Feb 27 (1 day)
leave1.invalidate_recordset(['number_of_days', 'request_date_to'])
self.assertEqual(leave1.request_date_to, date(2026, 2, 23),
"Leave 1 should have been shortened to end before the training.")
self.assertEqual(leave1.number_of_days, 1.0,
"Leave 1 duration should have been updated to 1 day.")
leave2.invalidate_recordset(['number_of_days', 'request_date_to'])
self.assertEqual(leave2.request_date_from, date(2026, 2, 27),
"Leave 2 should have been shortened to start after the training.")
self.assertEqual(leave2.number_of_days, 1.0,
"Leave 2 duration should have been updated to 1 day.")
def test_group_leave_hourly_conflict(self):
"""Ensure batch generation fails if overlapping hourly time off exists
and does not unlink the related calendar leaves."""
# Create an hourly leave and validate it
LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id)
hourly_type = LeaveType.create({
'name': 'Hourly Leave',
'request_unit': 'hour',
'requires_allocation': False,
'leave_validation_type': 'both',
})
hourly_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hourly Leave',
'employee_id': self.employee_emp_id,
'holiday_status_id': hourly_type.id,
'request_unit_hours': True,
'request_date_from': '2026-02-24',
'request_date_to': '2026-02-24',
'request_hour_from': 8,
'request_hour_to': 12,
})
hourly_leave.with_user(self.user_hrmanager_id).action_approve()
# Check that the leave exists and is linked to a calendar leave
calendar_leave = self.env['resource.calendar.leaves'].search([
('holiday_id', '=', hourly_leave.id)
])
self.assertTrue(calendar_leave)
# Create a group leave that overlaps with the hourly leave
training_type = LeaveType.create({
'name': 'Training',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
})
leave_wizard_form = Form(self.env['hr.leave.generate.multi.wizard'].with_user(self.user_hrmanager_id))
leave_wizard_form.allocation_mode = 'company'
leave_wizard_form.company_id = self.env.company
leave_wizard_form.holiday_status_id = training_type
leave_wizard_form.date_from = date(2026, 2, 24)
leave_wizard_form.date_to = date(2026, 2, 24)
leave_wizard = leave_wizard_form.save()
# ASSERTIONS
# Should raise an error and the approved leave should not be changed or removed
with self.assertRaises(UserError):
leave_wizard.action_generate_time_off()
self.assertTrue(calendar_leave.exists(), "Calendar leaves should not be unlinked on error")
hourly_leave.invalidate_recordset()
self.assertEqual(hourly_leave.state, 'validate')