oca-ocb-hr/odoo-bringout-oca-ocb-hr_holidays/hr_holidays/tests/test_leave_requests.py
Ernad Husremovic a1f02d8cc7 19.0 vanilla
2026-03-25 12:00:11 +01:00

2321 lines
108 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, date, timedelta
import time
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from pytz import timezone
from odoo import fields, Command
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import date_utils, mute_logger
from odoo.tests import Form, tagged, users
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('leave_requests')
class TestLeaveRequests(TestHrHolidaysCommon):
def _check_holidays_status(self, holiday_status, employee, ml, lt, rl, vrl):
result = holiday_status.get_allocation_data(employee)[employee][0][1]
self.assertEqual(
result['max_leaves'], ml,
'hr_holidays: wrong type days computation')
self.assertEqual(
result['leaves_taken'], lt,
'hr_holidays: wrong type days computation')
self.assertEqual(
result['remaining_leaves'], rl,
'hr_holidays: wrong type days computation')
self.assertEqual(
result['virtual_remaining_leaves'], vrl,
'hr_holidays: wrong type days computation')
@classmethod
def setUpClass(cls):
super(TestLeaveRequests, cls).setUpClass()
# Make sure we have the rights to create, validate and delete the leaves, leave types and allocations
LeaveType = cls.env['hr.leave.type'].with_user(cls.user_hrmanager_id).with_context(tracking_disable=True)
cls.holidays_type_1 = LeaveType.create({
'name': 'NotLimitedHR',
'requires_allocation': False,
'leave_validation_type': 'hr',
})
cls.holidays_type_2 = LeaveType.create({
'name': 'Limited',
'requires_allocation': True,
'employee_requests': True,
'leave_validation_type': 'hr',
})
cls.holidays_type_3 = LeaveType.create({
'name': 'TimeNotLimited',
'requires_allocation': False,
'leave_validation_type': 'manager',
})
cls.holidays_type_4 = LeaveType.create({
'name': 'Limited with 2 approvals',
'requires_allocation': True,
'employee_requests': True,
'leave_validation_type': 'both',
})
cls.holidays_support_document = LeaveType.create({
'name': 'Time off with support document',
'support_document': True,
'requires_allocation': False,
'leave_validation_type': 'no_validation',
})
cls.holidays_type_half = LeaveType.create({
'name': 'Time off in half-days',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
'request_unit': 'half_day',
})
cls.holidays_type_hours = LeaveType.create({
'name': 'Time off in hours',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
'request_unit': 'hour',
})
cls.irregular_calendar = cls.env['resource.calendar'].create({
'name': 'Irregular Calendar With Gaps',
'tz': 'Europe/Brussels',
'company_id': False,
'attendance_ids': [(5, 0, 0),
## Hours Per Week: 33, Avg hours_per_day = 6.6, 75% = 4.95
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Lunch', 'dayofweek': '0', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17.6, 'day_period': 'afternoon'}),
## For a single period day, an attendance is considered a full day if hours are more than 75% of avg hours per day
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 14, 'hour_to': 19.6, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 6, 'hour_to': 10, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Lunch', 'dayofweek': '2', 'hour_from': 10, 'hour_to': 11, 'day_period': 'lunch'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 11, 'hour_to': 15.6, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 7, 'hour_to': 10, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 10, 'hour_to': 11, 'day_period': 'lunch'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 11, 'hour_to': 14.6, 'day_period': 'afternoon'}),
## Normal Half-Day since it doesn't exceed 75% of average hours per day
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 11.6, 'day_period': 'morning'}),
],
})
cls.set_employee_create_date(cls.employee_emp_id, '2010-02-03 00:00:00')
cls.set_employee_create_date(cls.employee_hruser_id, '2010-02-03 00:00:00')
def _check_holidays_count(self, holidays_count_result, ml, lt, rl, vrl, vlt):
self.assertEqual(holidays_count_result['max_leaves'], ml)
self.assertEqual(holidays_count_result['remaining_leaves'], rl)
self.assertEqual(holidays_count_result['virtual_remaining_leaves'], vrl)
self.assertEqual(holidays_count_result['leaves_taken'], lt)
self.assertEqual(holidays_count_result['virtual_leaves_taken'], vlt)
@classmethod
def set_employee_create_date(cls, _id, newdate):
""" This method is a hack in order to be able to define/redefine the create_date
of the employees.
This is done in SQL because ORM does not allow to write onto the create_date field.
"""
cls.env.cr.execute("""
UPDATE
hr_employee
SET create_date = '%s'
WHERE id = %s
""" % (newdate, _id))
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_overlapping_requests(self):
""" Employee cannot create a new leave request at the same time, avoid interlapping """
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': (date.today() - relativedelta(days=1)),
'request_date_to': date.today(),
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol21',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': (datetime.today() - relativedelta(days=1)),
'request_date_to': datetime.today(),
})
def test_limited_type_not_enough_days(self):
with freeze_time('2022-01-05'):
allocation = self.env['hr.leave.allocation'].with_user(self.user_hruser_id).create({
'name': 'Days for limited category',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 2,
'state': 'confirm',
'date_from': time.strftime('%Y-01-01'),
'date_to': time.strftime('%Y-12-31'),
})
allocation.action_approve()
# Employee cannot take a leave longer than the allocation
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Invalid Hol21',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': time.strftime('2022-02-01'),
'request_date_to': time.strftime('2022-02-04'),
})
# A leave cannot be modified so that it's longer than the allocation
valid_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Valid Hol21',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': time.strftime('2022-02-02'),
'request_date_to': time.strftime('2022-02-03'),
})
with self.assertRaises(ValidationError):
valid_leave.write({
'request_date_from': time.strftime('2022-02-01'),
'request_date_to': time.strftime('2022-02-05'),
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_limited_type_days_left(self):
""" Employee creates a leave request in a limited category and has enough days left """
with freeze_time('2022-01-05'):
allocation = self.env['hr.leave.allocation'].with_user(self.user_hruser_id).create({
'name': 'Days for limited category',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 2,
'state': 'confirm',
'date_from': time.strftime('%Y-01-01'),
'date_to': time.strftime('%Y-12-31'),
})
allocation.action_approve()
holiday_status = self.holidays_type_2.with_user(self.user_employee_id)
self._check_holidays_status(holiday_status, self.employee_emp, 2.0, 0.0, 2.0, 2.0)
hol = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': (datetime.today() - relativedelta(days=1)),
'request_date_to': datetime.today(),
})
holiday_status.invalidate_model()
self._check_holidays_status(holiday_status, self.employee_emp, 2.0, 0.0, 2.0, 0.0)
hol.with_user(self.user_hrmanager_id).action_approve()
holiday_status.invalidate_model(['max_leaves'])
self._check_holidays_status(holiday_status, self.employee_emp, 2.0, 2.0, 0.0, 0.0)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_accrual_validity_time_valid(self):
""" Employee ask leave during a valid validity time """
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).create({
'name': 'Sick Time Off',
'holiday_status_id': self.holidays_type_2.id,
'employee_id': self.employee_emp.id,
'date_from': fields.Datetime.from_string('2017-01-01 00:00:00'),
'date_to': fields.Datetime.from_string('2017-06-01 00:00:00'),
'number_of_days': 10,
})
allocation.action_approve()
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Valid time period',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': fields.Date.from_string('2017-03-03'),
'request_date_to': fields.Date.from_string('2017-03-11'),
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_department_leave(self):
""" Create a department leave """
self.employee_hrmanager.write({'department_id': self.hr_dept.id})
self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)]))
leave_wizard_form = Form(self.env['hr.leave.generate.multi.wizard'].with_user(self.user_hrmanager))
leave_wizard_form.allocation_mode = 'department'
leave_wizard_form.department_id = self.hr_dept
leave_wizard_form.holiday_status_id = self.holidays_type_1
leave_wizard_form.date_from = date(2019, 5, 6)
leave_wizard_form.date_to = date(2019, 5, 6)
leave_wizard = leave_wizard_form.save()
leave_wizard.action_generate_time_off()
member_ids = self.hr_dept.member_ids.ids
self.assertEqual(self.env['hr.leave'].search_count([('employee_id', 'in', member_ids)]), len(member_ids), "Time Off should be created for members of department")
@users('Titus')
def test_create_group_leave_without_hr_right(self):
employee_1, employee_2 = self.env['hr.employee'].sudo().create([
{
'name': 'Emp1',
'leave_manager_id': self.user_responsible_id,
}, {
'name': 'Emp2',
'leave_manager_id': self.user_responsible_id,
},
])
leave_wizard = self.env['hr.leave.generate.multi.wizard'].create({
'holiday_status_id': self.holidays_type_1.id,
'date_from': date(2019, 5, 6),
'date_to': date(2019, 5, 6),
'employee_ids': (employee_1 + employee_2).ids
})
leave_wizard.action_generate_time_off()
@users('Titus')
def test_create_conflicting_group_leave_without_hr_right(self):
employee_1, employee_2 = self.env['hr.employee'].sudo().create([
{
'name': 'Emp1',
'leave_manager_id': self.user_responsible_id,
}, {
'name': 'Emp2',
'leave_manager_id': self.user_responsible_id,
},
])
leave_wizard = self.env['hr.leave.generate.multi.wizard'].create({
'holiday_status_id': self.holidays_type_1.id,
'date_from': date(2019, 5, 6),
'date_to': date(2019, 5, 8),
'employee_ids': (employee_1 + employee_2).ids,
})
leave_wizard.action_generate_time_off()
leave_wizard = self.env['hr.leave.generate.multi.wizard'].create({
'holiday_status_id': self.holidays_type_1.id,
'date_from': date(2019, 5, 7),
'date_to': date(2019, 5, 9),
'employee_ids': (employee_1 + employee_2).ids,
})
leave_wizard.action_generate_time_off()
@users('Titus')
def test_create_group_leave_form_allocation_mode_without_hr_right(self):
employee_1, employee_2, employee_3 = self.env['hr.employee'].sudo().create([
{
'name': 'Emp1',
'leave_manager_id': self.user_responsible_id,
}, {
'name': 'Emp2',
'leave_manager_id': self.user_responsible_id,
}, {
'name': 'Emp3',
},
])
leave_wizard = self.env['hr.leave.generate.multi.wizard'].create({
'holiday_status_id': self.holidays_type_1.id,
'date_from': date(2019, 5, 6),
'date_to': date(2019, 5, 6),
'allocation_mode': 'employee',
})
leave_wizard.action_generate_time_off()
generated_leaves = self.env['hr.leave'].search([
('employee_id', 'in', (employee_1 + employee_2 + employee_3).ids),
('holiday_status_id', '=', self.holidays_type_1.id),
])
self.assertEqual(len(generated_leaves), 2, "Only 2 leaves should be generated")
@users('Titus')
def test_create_differnt_calendars_group_leave_without_hr_right(self):
flexible_calendar = self.env['resource.calendar'].sudo().create({
'name': 'flexible calendar',
'flexible_hours': True,
'full_time_required_hours': 21,
'hours_per_day': 3,
'hours_per_week': 21,
})
employee_1, employee_2 = self.env['hr.employee'].sudo().create([
{
'name': 'Emp1',
'leave_manager_id': self.user_responsible_id,
}, {
'name': 'Emp2',
'leave_manager_id': self.user_responsible_id,
'resource_calendar_id': flexible_calendar.id,
},
])
leave_wizard = self.env['hr.leave.generate.multi.wizard'].create({
'holiday_status_id': self.holidays_type_1.id,
'date_from': date(2019, 5, 6),
'date_to': date(2019, 5, 8),
'employee_ids': (employee_1 + employee_2).ids,
})
leave_wizard.action_generate_time_off()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_allocation_request(self):
""" Create an allocation request """
# employee should be set to current user
allocation_form = Form(self.env['hr.leave.allocation'].with_user(self.user_employee))
allocation_form.holiday_status_id = self.holidays_type_2
allocation_form.name = 'New Allocation Request'
allocation_form.save()
def test_allocation_constrain_dates_check(self):
with self.assertRaises(UserError):
self.env['hr.leave.allocation'].create({
'name': 'Test allocation',
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 1,
'employee_id': self.employee_emp_id,
'state': 'confirm',
'date_from': time.strftime('%Y-%m-10'),
'date_to': time.strftime('%Y-%m-01'),
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_employee_is_absent(self):
""" Only the concerned employee should be considered absent """
user_employee_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': (date.today() - relativedelta(days=1)),
'request_date_to': date.today() + relativedelta(days=1),
})
(self.employee_emp | self.employee_hrmanager).mapped('is_absent') # compute in batch
self.assertFalse(self.employee_emp.is_absent, "He should not be considered absent")
self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent")
user_employee_leave.sudo().write({
'state': 'validate',
})
(self.employee_emp | self.employee_hrmanager)._compute_leave_status()
self.assertTrue(self.employee_emp.is_absent, "He should be considered absent")
self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_timezone_employee_leave_request(self):
""" Create a leave request for an employee in another timezone """
self.employee_emp.tz = 'Pacific/Auckland' # GMT+12
leave = self.env['hr.leave'].new({
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': date(2019, 5, 6),
'request_date_to': date(2019, 5, 6),
'request_hour_from': 8, # 8:00 AM in the employee's timezone
'request_hour_to': 17, # 5:00 PM in the employee's timezone
})
self.assertEqual(leave.date_from, datetime(2019, 5, 5, 20, 0, 0), "It should have been localized before saving in UTC")
self.assertEqual(leave.date_to, datetime(2019, 5, 6, 5, 0, 0), "It should have been localized before saving in UTC")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_timezone_company_leave_request(self):
""" Create a leave request for a company in another timezone """
company = self.env['res.company'].create({'name': "Hergé"})
company.resource_calendar_id.tz = 'Australia/Sydney' # GMT+12
leave = self.env['hr.leave'].new({
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'company_id': company.id,
'request_date_from': date(2019, 5, 6),
'request_date_to': date(2019, 5, 6),
'request_hour_from': 8, # 8:00 AM in the company's timezone
'request_hour_to': 17, # 5:00 PM in the company's timezone
})
self.assertEqual(leave.date_from, datetime(2019, 5, 6, 6, 0, 0), "It should have been localized in the Employee timezone")
self.assertEqual(leave.date_to, datetime(2019, 5, 6, 15, 0, 0), "It should have been localized in the Employee timezone")
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_timezone_company_validated(self):
""" Create a leave request for a company in another timezone and validate it """
self.env.user.tz = 'Australia/Sydney' # GMT+12
company = self.env['res.company'].create({'name': "Hergé"})
employee = self.env['hr.employee'].create({'name': "Remi", 'company_id': company.id})
leave_wizard_form = Form(self.env['hr.leave.generate.multi.wizard'])
leave_wizard_form.allocation_mode = 'company'
leave_wizard_form.company_id = company
leave_wizard_form.holiday_status_id = self.holidays_type_1
leave_wizard_form.date_from = date(2019, 5, 6)
leave_wizard_form.date_to = date(2019, 5, 6)
leave_wizard = leave_wizard_form.save()
leave_wizard.action_generate_time_off()
employee_leave = self.env['hr.leave'].search([('employee_id', '=', employee.id)])
self.assertEqual(
employee_leave.request_date_from, date(2019, 5, 6),
"Timezone should be be adapted on the employee leave"
)
def test_number_of_hours_display(self):
# Test that the field number_of_hours_dispay doesn't change
# after time off validation, as it takes the attendances
# minus the resource leaves to compute that field.
calendar = self.env['resource.calendar'].create({
'name': 'Monday Morning Else Full Time 38h/week',
'hours_per_day': 7.6,
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Lunch', 'dayofweek': '1', 'hour_from': 12.5, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Lunch', 'dayofweek': '2', 'hour_from': 12.5, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 12.5, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Lunch', 'dayofweek': '4', 'hour_from': 12.5, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'})
],
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
self.env.user.company_id.resource_calendar_id = calendar
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'request_unit': 'hour',
'leave_validation_type': 'both',
})
self.env['hr.leave.allocation'].create({
'name': '20 days allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 20,
'employee_id': employee.id,
'state': 'confirm',
'date_from': time.strftime('2018-01-01'),
'date_to': time.strftime('%Y-01-01'),
}).action_approve()
leave1 = self.env['hr.leave'].create({
'name': 'Holiday 1 week',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Date.from_string('2019-12-23'),
'request_date_to': fields.Date.from_string('2019-12-27'),
})
self.assertEqual(leave1.number_of_hours, 38)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours, 38)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours, 38)
leave2 = self.env['hr.leave'].create({
'name': 'Holiday 1 Day',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Datetime.from_string('2019-12-30'),
'request_date_to': fields.Datetime.from_string('2019-12-30'),
})
self.assertEqual(leave2.number_of_hours, 4)
leave2.action_approve()
self.assertEqual(leave2.number_of_hours, 4)
leave2.action_approve()
self.assertEqual(leave2.number_of_hours, 4)
def test_number_of_hours_display_flexible_calendar(self):
# Test that the field number_of_hours_dispay do change for flexible calendars
calendar = self.env['resource.calendar'].create({
'name': 'Full Time 24h/8day',
'hours_per_day': 24,
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 12, 'hour_to': 24, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 12, 'hour_to': 24, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 12, 'hour_to': 24, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 12, 'hour_to': 24, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 12, 'hour_to': 24, 'day_period': 'afternoon'})
],
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
self.env.user.company_id.resource_calendar_id = calendar
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'request_unit': 'hour',
'leave_validation_type': 'both',
})
self.env['hr.leave.allocation'].create({
'name': '20 days allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 20,
'employee_id': employee.id,
'state': 'confirm',
'date_from': time.strftime('2018-01-01'),
'date_to': time.strftime('%Y-01-01'),
}).action_approve()
leave0 = self.env['hr.leave'].create({
'name': 'Holiday 1 day',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Date.from_string('2019-12-09'),
'request_date_to': fields.Date.from_string('2019-12-09'),
})
self.assertAlmostEqual(leave0.number_of_hours, 24, 2)
calendar.write({
'flexible_hours': True,
'hours_per_day': 8.0,
'hours_per_week': 40,
'full_time_required_hours': 40
})
leave1 = self.env['hr.leave'].create({
'name': 'Holiday 1 week',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Date.from_string('2019-12-16'),
'request_date_to': fields.Date.from_string('2019-12-20'),
})
self.assertEqual(leave1.number_of_hours, 5 * 8)
leave2 = self.env['hr.leave'].create({
'name': 'Holiday 1 Day',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Datetime.from_string('2019-12-23'),
'request_date_to': fields.Datetime.from_string('2019-12-23'),
})
self.assertEqual(leave2.number_of_hours, 8)
leave_type.request_unit = 'half_day'
leave3 = self.env['hr.leave'].create({
'name': 'Holiday 1/2 Day',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Datetime.from_string('2019-12-24'),
'request_date_to': fields.Datetime.from_string('2019-12-24'),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
})
self.assertEqual(leave3.number_of_hours, 4)
leave_type.request_unit = 'hour'
leave4 = self.env['hr.leave'].create({
'name': 'Holiday 3 Hours',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Datetime.from_string('2019-12-25'),
'request_date_to': fields.Datetime.from_string('2019-12-25'),
'request_hour_from': 7,
'request_hour_to': 10,
})
self.assertEqual(leave4.number_of_hours, 3)
leave5 = self.env['hr.leave'].create({
'name': 'Holiday 10 hours',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Datetime.from_string('2019-12-26'),
'request_date_to': fields.Datetime.from_string('2019-12-26'),
'request_hour_from': 7,
'request_hour_to': 17,
})
self.assertEqual(leave5.number_of_hours, 10)
def test_number_of_hours_display_global_leave(self):
# Check that the field number_of_hours
# takes the global leaves into account, even
# after validation
calendar = self.env['resource.calendar'].create({
'name': 'Classic 40h/week',
'hours_per_day': 8.0,
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Lunch', 'dayofweek': '0', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Lunch', 'dayofweek': '1', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Lunch', 'dayofweek': '2', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Lunch', 'dayofweek': '4', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
],
'global_leave_ids': [(0, 0, {
'name': 'Christmas Time Off',
'date_from': fields.Datetime.from_string('2019-12-25 00:00:00'),
'date_to': fields.Datetime.from_string('2019-12-26 23:59:59'),
'resource_id': False,
'time_type': 'leave',
})]
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
self.env.user.company_id.resource_calendar_id = calendar
leave_type = self.env['hr.leave.type'].create({
'name': 'Sick',
'request_unit': 'hour',
'leave_validation_type': 'both',
'requires_allocation': False,
})
leave1 = self.env['hr.leave'].create({
'name': 'Sick 1 week during christmas snif',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': fields.Date.from_string('2019-12-23'),
'request_date_to': fields.Date.from_string('2019-12-27'),
})
self.assertEqual(leave1.number_of_hours, 24)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours, 24)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours, 24)
def _test_leave_with_tz(self, tz, local_date_from, local_date_to, number_of_days):
self.user_employee.tz = tz
tz = timezone(tz)
# We use new instead of create to avoid the leaves generated for the
# different timezones clashing with each other.
leave = self.env['hr.leave'].with_user(self.user_employee_id).new({
'name': 'Test',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': local_date_from,
'request_date_to': local_date_to,
})
self.assertEqual(leave.number_of_days, number_of_days)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_defaults_with_timezones(self):
""" Make sure that leaves start with correct defaults for non-UTC timezones """
timezones_to_test = ('UTC', 'Pacific/Midway', 'America/Los_Angeles', 'Asia/Taipei', 'Pacific/Kiritimati') # UTC, UTC -11, UTC -8, UTC +8, UTC +14
# January 2020
# Su Mo Tu We Th Fr Sa
# 1 2 3 4
# 5 6 7 8 9 10 11
# 12 13 14 15 16 17 18
# 19 20 21 22 23 24 25
# 26 27 28 29 30 31
local_date_from = date(2020, 1, 1)
local_date_to = date(2020, 1, 1)
for tz in timezones_to_test:
self._test_leave_with_tz(tz, local_date_from, local_date_to, 1)
# We, Th, Fr, Mo, Tu, We => 6 days
local_date_from = date(2020, 1, 2)
local_date_to = date(2020, 1, 9)
for tz in timezones_to_test:
self._test_leave_with_tz(tz, local_date_from, local_date_to, 6)
def test_expired_allocation(self):
allocation = self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 20,
'state': 'confirm',
'date_from': '2020-01-01',
'date_to': '2020-12-31',
})
allocation.action_approve()
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': '2021-09-01',
'request_date_to': '2021-09-01',
})
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': '2020-09-01',
'request_date_to': '2020-09-01',
})
def test_no_days_expired(self):
# First expired allocation
allocation_one = self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 20,
'state': 'confirm',
'date_from': '2020-01-01',
'date_to': '2020-12-31',
})
allocation_one.action_approve()
allocation_two = self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 3,
'state': 'confirm',
'date_from': '2021-01-01',
'date_to': '2021-12-31',
})
allocation_two.action_approve()
# Try creating a request that could be validated if allocation1 was still valid
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-10',
})
# This time we have enough days
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-08',
})
def test_company_leaves(self):
# First expired allocation
self.env['hr.leave.allocation.generate.multi.wizard'].create({
'name': 'Allocation',
'company_id': self.env.company.id,
'holiday_status_id': self.holidays_type_1.id,
'duration': 20,
'date_from': '2021-01-01',
})
req1_form = Form(self.env['hr.leave'].sudo())
req1_form.employee_id = self.employee_emp
req1_form.holiday_status_id = self.holidays_type_1
req1_form.request_date_from = fields.Date.to_date('2021-12-06')
req1_form.request_date_to = fields.Date.to_date('2021-12-08')
req1_form.save().action_approve()
self.assertEqual(req1_form.record.number_of_days, 3)
def test_leave_with_public_holiday_other_company(self):
other_company = self.env['res.company'].create({
'name': 'Test Company 2',
})
# Create a public holiday for the second company
p_leave = self.env['resource.calendar.leaves'].create({
'date_from': datetime(2022, 3, 11),
'date_to': datetime(2022, 3, 11, 23, 59, 59),
})
p_leave.company_id = other_company
leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': date(2022, 3, 11),
'request_date_to': date(2022, 3, 11),
})
self.assertEqual(leave.number_of_days, 1)
def test_several_allocations(self):
allocation_vals = {
'name': 'Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 5,
'state': 'confirm',
'date_from': '2022-01-01',
'date_to': '2022-12-31',
}
self.env['hr.leave.allocation'].create(allocation_vals).action_approve()
self.env['hr.leave.allocation'].create(allocation_vals).action_approve()
# Able to create a leave of 10 days with two allocations of 5 days
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': '2022-01-01',
'request_date_to': '2022-01-15',
})
def test_several_allocations_split(self):
Allocation = self.env['hr.leave.allocation']
allocation_vals = {
'name': 'Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'state': 'confirm',
'date_from': '2022-01-01',
'date_to': '2022-12-31',
}
Leave = self.env['hr.leave'].with_user(self.user_employee_id).sudo()
leave_vals = {
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
}
for unit in ['hour', 'day']:
self.holidays_type_2.request_unit = unit
allocation_vals.update({'number_of_days': 4})
allocation_4days = Allocation.create(allocation_vals)
allocation_4days.action_approve()
allocation_vals.update({'number_of_days': 1})
allocation_1day = Allocation.create(allocation_vals)
allocation_1day.action_approve()
allocations = (allocation_4days + allocation_1day)
leave_vals.update({
'request_date_from': '2022-01-03',
'request_date_to': '2022-01-06',
})
leave_confirm = Leave.create(leave_vals)
leave_confirm.action_refuse()
leave_4days = Leave.create(leave_vals)
leave_vals.update({
'request_date_from': '2022-01-07',
'request_date_to': '2022-01-07',
})
leave_1day = Leave.create(leave_vals)
leaves = (leave_4days + leave_1day)
leaves.action_approve()
allocation_days = self.employee_emp._get_consumed_leaves(self.holidays_type_2)[0]
self.assertEqual(
allocation_days[self.employee_emp][self.holidays_type_2][allocation_4days]['leaves_taken'],
leave_4days['number_of_%ss' % unit],
'As 4 days were available in this allocation, they should have been taken')
self.assertEqual(
allocation_days[self.employee_emp][self.holidays_type_2][allocation_1day]['leaves_taken'],
leave_1day['number_of_%ss' % unit],
'As no days were available in previous allocation, they should have been taken in this one')
leaves.action_refuse()
allocations.action_refuse()
def test_time_off_recovery_on_create(self):
time_off = self.env['hr.leave'].create([
{
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': '2021-12-06',
'request_date_to': '2021-12-10',
},
{
'name': 'Holiday Request',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': '2021-12-06',
'request_date_to': '2021-12-10',
}
])
self.assertEqual(time_off[0].number_of_days, 5)
self.assertEqual(time_off[1].number_of_days, 5)
self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off',
'date_from': '2021-12-07 00:00:00',
'date_to': '2021-12-07 23:59:59',
})
self.assertEqual(time_off[0].number_of_days, 4)
self.assertEqual(time_off[1].number_of_days, 4)
def test_time_off_recovery_on_write(self):
global_time_off = self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off',
'date_from': '2021-12-07 00:00:00',
'date_to': '2021-12-07 23:59:59',
})
time_off_1 = self.env['hr.leave'].create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': '2021-12-06',
'request_date_to': '2021-12-10',
})
self.assertEqual(time_off_1.number_of_days, 4)
time_off_2 = self.env['hr.leave'].create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': '2021-12-13',
'request_date_to': '2021-12-17',
})
self.assertEqual(time_off_2.number_of_days, 5)
# adding 1 day to the global time off
global_time_off.write({
'date_to': '2021-12-08 23:59:59',
})
self.assertEqual(time_off_1.number_of_days, 3)
# moving the global time off to the next week
global_time_off.write({
'date_from': '2021-12-15 00:00:00',
'date_to': '2021-12-15 23:59:59',
})
self.assertEqual(time_off_1.number_of_days, 5)
self.assertEqual(time_off_2.number_of_days, 4)
def test_time_off_recovery_on_unlink(self):
global_time_off = self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off',
'date_from': '2021-12-07 00:00:00',
'date_to': '2021-12-07 23:59:59',
})
time_off = self.env['hr.leave'].create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': '2021-12-06',
'request_date_to': '2021-12-10',
})
self.assertEqual(time_off.number_of_days, 4)
global_time_off.unlink()
self.assertEqual(time_off.number_of_days, 5)
def test_time_off_duration_zero(self):
time_off = self.env['hr.leave'].create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': '2021-11-15',
'request_date_to': '2021-11-19',
})
self.assertEqual(time_off.number_of_days, 5)
self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off',
'date_from': '2021-11-15 00:00:00',
'date_to': '2021-11-19 23:59:59',
})
self.assertEqual(time_off.state, 'confirm')
self.assertEqual(time_off.number_of_days, 0)
def test_time_off_irregular_working_schedule(self):
# Test a specific case where `_get_attendances` bugged out when a
# very specific working schedule was used.
calendar = self.env['resource.calendar'].create({
'name': 'Irregular Working Schedule (monday morning - wednesday afternoon)',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
self.employee_emp.resource_calendar_id = calendar
# Take a time off on the next tuesday (when the employee is not
# supposed to work) Previously this would raise a ValidationError.
next_tuesday = date_utils.start_of(fields.Date.today() + relativedelta(days=7), 'week') + relativedelta(days=1)
time_off = self.env['hr.leave'].create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': next_tuesday,
'request_date_to': next_tuesday,
})
self.assertEqual(time_off.number_of_days, 0)
def test_holiday_type_allocation(self):
with freeze_time('2020-09-15'):
allocation = self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 5,
'state': 'confirm',
'date_from': '2020-01-01',
'date_to': '2020-12-31',
})
allocation.action_approve()
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': '2020-09-07',
'request_date_to': '2020-09-09',
})
self._check_holidays_count(
self.employee_emp._get_consumed_leaves(self.holidays_type_2)[0][self.employee_emp][self.holidays_type_2][allocation],
ml=5, lt=0, rl=5, vrl=2, vlt=3,
)
def test_archived_allocation(self):
with freeze_time('2022-09-15'):
allocation_2021 = self.env['hr.leave.allocation'].create({
'name': 'Annual Time Off 2021',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 10,
'state': 'confirm',
'date_from': '2021-06-01',
'date_to': '2021-12-31',
})
allocation_2021.action_approve()
allocation_2022 = self.env['hr.leave.allocation'].create({
'name': 'Annual Time Off 2022',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 20,
'state': 'confirm',
'date_from': '2022-01-01',
'date_to': '2022-12-31',
})
allocation_2022.action_approve()
# Leave taken in 2021
leave_2021 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': datetime(2021, 8, 9),
'request_date_to': datetime(2021, 8, 13),
})
leave_2021.with_user(self.user_hrmanager_id).action_approve()
# The holidays count only takes into account the valid allocations at that date
self._check_holidays_count(
self.holidays_type_2.get_allocation_data(self.employee_emp, target_date=date(2021, 12, 1))[self.employee_emp][0][1],
ml=10, lt=5, rl=5, vrl=5, vlt=5,
)
# Days remaining before the allocation ends is equal to 1 because there is only one day remaining in the allocation based on its validity
self.assertEqual(
self.holidays_type_2.get_allocation_data(self.employee_emp, target_date=date(2021, 12, 31))[self.employee_emp][0][1]['closest_allocation_duration'],
1,
"Only one day should remain before the allocation expires"
)
leave_2022 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': datetime(2022, 8, 9),
'request_date_to': datetime(2022, 8, 13),
})
leave_2022.with_user(self.user_hrmanager_id).action_approve()
# The holidays count in 2022 is not affected by the first leave taken in 2021
self._check_holidays_count(
self.holidays_type_2.get_allocation_data(self.employee_emp)[self.employee_emp][0][1],
ml=20, lt=4, rl=16, vrl=16, vlt=4,
)
# The holidays count in 2021 is not affected by the leave taken in 2022
self._check_holidays_count(
self.holidays_type_2.get_allocation_data(self.employee_emp, target_date=date(2021, 12, 1))[self.employee_emp][0][1],
ml=10, lt=5, rl=5, vrl=5, vlt=5,
)
def test_cancel_leave(self):
with freeze_time('2020-09-15'):
self.env['hr.leave.allocation'].create({
'name': 'Annual Time Off',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_4.id,
'number_of_days': 20,
'state': 'confirm',
'date_from': '2020-01-01',
'date_to': '2020-12-31',
}).action_approve()
leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_4.id,
'request_date_from': '2020-09-21',
'request_date_to': '2020-09-23',
})
# A meeting is only created once the leave is validated
self.assertFalse(leave.meeting_id)
leave.with_user(self.user_responsible_id).action_approve()
self.assertFalse(leave.meeting_id)
# A meeting is created in the user's calendar when a leave is validated
leave.with_user(self.user_hrmanager_id).action_approve()
self.assertTrue(leave.meeting_id.active)
# The meeting is archived when the leave is cancelled
leave.with_user(self.user_employee_id)._action_user_cancel('Cancel leave')
self.assertFalse(leave.meeting_id.active)
def test_create_support_document_in_the_past(self):
with freeze_time('2022-10-19'):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_support_document.id,
'request_date_from': '2022-10-17',
'request_date_to': '2022-10-17',
'supported_attachment_ids': [(6, 0, [])], # Sent by webclient
})
def test_create_supported_attachments_link_attachment_ids(self):
with freeze_time('2025-10-21'):
attachment = self.env['ir.attachment'].create({
'name': "an attachment",
'datas': 'My attachment',
'res_model': 'hr.leave',
})
leave = self.env['hr.leave'].create({
'employee_id': 1,
'state': 'confirm',
'holiday_status_id': self.holidays_support_document.id,
'request_date_from': '2025-10-24',
'request_date_to': '2025-10-24',
'supported_attachment_ids': [[4, attachment.id]],
})
self.assertTrue(leave.attachment_ids)
def test_prevent_misplacement_of_allocations_without_end_date(self):
"""
The objective is to check that it is not possible to place leaves
for which the interval does not correspond to the interval of allocations.
"""
leave_type_A = self.env['hr.leave.type'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Type A',
'requires_allocation': True,
'employee_requests': True,
'leave_validation_type': 'hr',
})
# Create allocations with no end date
allocations = self.env['hr.leave.allocation'].create([
{
'name': 'Type A march 1 day without date to',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type_A.id,
'number_of_days': 1,
'state': 'confirm',
'date_from': '2023-01-03',
},
{
'name': 'Type A april 5 day without date to',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type_A.id,
'number_of_days': 5,
'state': 'confirm',
'date_from': '2023-04-01',
},
])
allocations.action_approve()
trigger_error_leave = {
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type_A.id,
'request_date_from': '2023-03-14',
'request_date_to': '2023-03-16',
}
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create(trigger_error_leave)
@freeze_time('2022-06-13 10:00:00')
def test_current_leave_status(self):
types = ('no_validation', 'manager', 'hr', 'both')
employee = self.employee_emp
def run_validation_flow(leave_validation_type):
LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id)
leave_type = LeaveType.with_context(tracking_disable=True).create({
'name': leave_validation_type.capitalize(),
'leave_validation_type': leave_validation_type,
'requires_allocation': False,
'responsible_ids': [Command.link(self.env.ref('base.user_admin').id)],
})
current_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': datetime.today() - timedelta(days=1),
'date_to': datetime.today() + timedelta(days=1),
})
if leave_validation_type in ('manager', 'both'):
self.assertFalse(employee.is_absent)
self.assertFalse(employee.current_leave_id)
self.assertEqual(employee.filtered_domain([('is_absent', '=', False)]), employee)
self.assertFalse(employee.filtered_domain([('is_absent', '=', True)]))
current_leave.with_user(self.user_responsible_id).action_approve()
if leave_validation_type in ('hr', 'both'):
self.assertFalse(employee.is_absent)
self.assertFalse(employee.current_leave_id)
self.assertEqual(employee.filtered_domain([('is_absent', '=', False)]), employee)
self.assertFalse(employee.filtered_domain([('is_absent', '=', True)]))
current_leave.with_user(self.user_hrmanager_id).action_approve()
employee.invalidate_recordset(fnames=["is_absent", "current_leave_id"])
self.assertTrue(employee.is_absent)
self.assertEqual(employee.current_leave_id, current_leave.holiday_status_id)
self.assertFalse(employee.filtered_domain([('is_absent', '=', False)]))
self.assertEqual(employee.filtered_domain([('is_absent', '=', True)]), employee)
raise RuntimeError()
for leave_validation_type in types:
with self.assertRaises(RuntimeError):
run_validation_flow(leave_validation_type)
@freeze_time('2019-11-01')
def test_duration_display_global_leave(self):
""" Ensure duration_display stays in sync with leave duration. """
employee = self.employee_emp
calendar = employee.resource_calendar_id
sick_leave_type = self.env['hr.leave.type'].create({
'name': 'Sick Leave (days)',
'request_unit': 'day',
'leave_validation_type': 'hr',
'requires_allocation': False,
})
sick_leave = self.env['hr.leave'].create({
'name': 'Sick 3 days',
'employee_id': employee.id,
'holiday_status_id': sick_leave_type.id,
'request_date_from': '2019-12-23',
'request_date_to': '2019-12-25',
})
comp_leave_type = self.env['hr.leave.type'].create({
'name': 'OT Compensation (hours)',
'request_unit': 'hour',
'leave_validation_type': 'manager',
'requires_allocation': False,
})
comp_leave = self.env['hr.leave'].create({
'name': 'OT Comp (4 hours)',
'employee_id': employee.id,
'holiday_status_id': comp_leave_type.id,
'request_date_from': '2019-12-26',
'request_date_to': '2019-12-26',
'request_hour_from': 8,
'request_hour_to': 12,
})
self.assertEqual(sick_leave.duration_display, '3 days')
self.assertEqual(comp_leave.duration_display, '4:00 hours')
calendar.global_leave_ids = [(0, 0, {
'name': 'Winter Holidays',
'date_from': '2019-12-25 00:00:00',
'date_to': '2019-12-26 23:59:59',
'time_type': 'leave',
})]
msg = "hr_holidays: duration_display should update after adding an overlapping holiday"
self.assertEqual(sick_leave.duration_display, '2 days', msg)
self.assertEqual(comp_leave.duration_display, '0:00 hours', msg)
def test_duration_display_public_leave_include(self):
"""
The purpose is to test whether the duration_display
computation considers public holidays when the
`include_public_holidays_in_duration` is set to True.
"""
employee = self.employee_emp
calendar = employee.resource_calendar_id
sick_leave_type = self.env['hr.leave.type'].create({
'name': 'Sick Leave (days)',
'request_unit': 'day',
'leave_validation_type': 'hr',
'requires_allocation': False,
})
sick_leave = self.env['hr.leave'].create({
'name': 'Sick 3 days',
'employee_id': employee.id,
'holiday_status_id': sick_leave_type.id,
'request_date_from': '2021-11-15',
'request_date_to': '2021-11-17',
})
self.assertEqual(sick_leave.duration_display, '3 days')
calendar.global_leave_ids = [(0, 0, {
'name': 'Autumn Holidays',
'date_from': '2021-11-16 00:00:00',
'date_to': '2021-11-16 23:59:59',
'time_type': 'leave',
})]
self.assertEqual(sick_leave.duration_display, '2 days', "hr_holidays: duration_display should not count public holiday")
sick_leave_type.include_public_holidays_in_duration = True
sick_leave.unlink()
sick_leave = self.env['hr.leave'].create({
'name': 'Sick 3 days',
'employee_id': employee.id,
'holiday_status_id': sick_leave_type.id,
'request_date_from': '2021-11-15',
'request_date_to': '2021-11-17',
})
self.assertEqual(sick_leave.duration_display, '3 days', "hr_holidays: duration_display should not update after adding an overlapping holiday")
@freeze_time('2024-01-18')
def test_undefined_working_hours(self):
""" Ensure time-off can also be allocated without ResourceCalendar. """
employee = self.employee_emp
# set a flexible working schedule
calendar = self.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'hours_per_day': 8.0,
'hours_per_week': 40,
'full_time_required_hours': 40,
'flexible_hours': True,
})
employee.resource_calendar_id = calendar
allocation = 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',
})
allocation.action_approve()
leave = 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, 15.0)
self.assertEqual(leave.duration_display, '5 days')
def test_default_request_date_timezone(self):
"""
The purpose is to test whether the timezone is
taken into account when requesting a leave.
"""
self.user_employee.tz = 'Asia/Hong_Kong' # UTC +08:00
context = {
# `date_from/to` in UTC to simulate client values
'default_date_from': '2024-03-27 23:00:00',
'default_date_to': '2024-03-28 08:00:00',
}
leave_form = Form(self.env['hr.leave'].with_user(self.user_employee).with_context(context))
leave_form.holiday_status_id = self.holidays_type_3
leave = leave_form.save()
self.assertEqual(leave.number_of_days, 1.0)
def test_filter_time_off_type_multiple_employees(self):
""" This test mimics the behavior of creating time off for multiple employees.
We check that the time off types that the user can select are correct.
In this example, we use a time off type that requires allocations.
Only the current user has an allocation for the time off type.
This time off type should not appear when multiple employees are select (user included or not).
"""
self.assertFalse(self.env['hr.leave.allocation'].search([['holiday_status_id', '=', self.holidays_type_2.id]]))
self.env.user.employee_id = self.employee_hruser_id
allocation = self.env['hr.leave.allocation'].create({
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_type_2.id,
'allocation_type': 'regular'
})
allocation.action_approve()
self.assertEqual(allocation.state, 'validate')
search_domain = ['|',
['requires_allocation', '=', False],
'&',
['has_valid_allocation', '=', True],
'&',
['max_leaves', '>', '0'],
'|',
['allows_negative', '=', True],
'&',
['virtual_remaining_leaves', '>', 0],
['allows_negative', '=', False]]
search_result = self.env['hr.leave.type'].with_context(employee_id=False).name_search(domain=search_domain)
self.assertFalse(self.holidays_type_2.id in [alloc_id for (alloc_id, _) in search_result])
def test_holiday_type_allocation_requirement_edit(self):
# Does not raise an error since no leave of this type exists yet
self.holidays_type_2.requires_allocation = False
self.assertFalse(self.holidays_type_2.requires_allocation, 'Allocations should no longer be required')
self.env['hr.leave'].create({
'name': 'Test leave',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
})
with self.assertRaises(UserError):
self.holidays_type_2.requires_allocation = True
def test_activity_update_with_time_off_officer(self):
""" Test activity creation flow when approval settings involve Time Off Officer and Employee's Approver. """
# Case 1: Approved by Time Off Officer but no Time Off Officer is set
self.holidays_type_1.responsible_ids = False # No Time Off Officer set
test_holiday_1 = self.env['hr.leave'].create({
'name': 'Test leave',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'date_from': (datetime.today() - timedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
})
activities = test_holiday_1.activity_ids
self.assertFalse(activities, "No activity should be created if no Time Off Officer is set for approval.")
allocation = self.env['hr.leave.allocation'].create({
'name': 'Allocation for hruser',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 5,
'state': 'confirm',
'date_from': '2024-01-01',
})
allocation.action_approve()
self.holidays_type_2.responsible_ids = [Command.link(self.user_employee.id)]
test_holiday_2 = self.env['hr.leave'].create({
'name': 'Test leave',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_type_2.id,
'date_from': (datetime.today() - timedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
})
activities = test_holiday_2.activity_ids
self.assertEqual(len(activities), 1, "One activity should be created for the Employee's Approver.")
self.assertEqual(activities.activity_type_id, self.env.ref('hr_holidays.mail_act_leave_approval'), "The activity type should be for leave approval by the Employee's Approver.")
self.assertEqual(activities.user_id.id, self.user_employee_id, "The activity should be assigned to the Employee's Approver.")
# Case 2: Approved by Time Off Officer and Employee's Approver, but no Time Off Officer is set
self.holidays_type_4.responsible_ids = False # No Time Off Officer set
allocation = self.env['hr.leave.allocation'].create({
'name': 'Allocation for hrmanager',
'employee_id': self.employee_hrmanager_id,
'holiday_status_id': self.holidays_type_4.id,
'number_of_days': 5,
'state': 'confirm',
'date_from': '2024-01-01',
})
allocation.action_approve()
test_holiday_3 = self.env['hr.leave'].create({
'name': 'Test leave',
'employee_id': self.employee_hrmanager_id,
'holiday_status_id': self.holidays_type_4.id,
'date_from': datetime.today(),
'date_to': (datetime.today() + timedelta(days=1)),
'number_of_days': 1,
'state': 'confirm',
})
activities = test_holiday_3.activity_ids
self.assertEqual(len(activities), 1, "One activity should be created for the Employee's Approver.")
self.assertEqual(activities.activity_type_id, self.env.ref('hr_holidays.mail_act_leave_approval'), "The activity type should be for leave approval by the Employee's Approver.")
self.assertEqual(activities.user_id, self.employee_hrmanager.leave_manager_id, "The activity should be assigned to the Employee's Approver.")
def test_time_off_date_edit(self):
user_id = self.employee_emp.user_id
employee_id = self.employee_emp.id
leave = self.env['hr.leave'].with_user(user_id).create({
'name': 'Test leave',
'employee_id': employee_id,
'holiday_status_id': self.holidays_type_1.id,
'date_from': (datetime.today() - relativedelta(days=2)),
'date_to': datetime.today()
})
two_days_after = (datetime.today() + relativedelta(days=2)).date()
with Form(leave.with_user(user_id)) as leave_form:
leave_form.request_date_from = two_days_after
leave_form.request_date_to = two_days_after
modified_leave = leave_form.save()
self.assertEqual(modified_leave.request_date_from, two_days_after)
self.assertEqual(modified_leave.request_date_to, two_days_after)
def test_public_holiday_in_the_middle_of_flexible_request(self):
calendar = self.env['resource.calendar'].create({
'name': 'Test calendar',
'hours_per_day': 8,
'hours_per_week': 56,
'full_time_required_hours': 56,
'flexible_hours': True
})
self.employee_emp.resource_calendar_id = calendar
# Create a public holiday for the flexible calendar
self.env['resource.calendar.leaves'].create({
'date_from': datetime(2022, 3, 11),
'date_to': datetime(2022, 3, 11, 23, 59, 59),
'calendar_id': calendar.id,
})
leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': date(2022, 3, 10),
'request_date_to': date(2022, 3, 12),
})
self.assertEqual(leave.number_of_days, 2)
def test_get_default_leave_type(self):
# Description: If the user is applying for leave from the calendar dashboard and has selected a duration in weeks or days.
# This indicates that the user intends to apply for an hourly leave type.
# As a result, only hourly leave types should be shown, if available.
# ===================================================================
# | Case 1 -> Choose hour leave type if hour leave type exists |
# | Case 2 -> Choose first leave type if hour leave type not exists |
# | Case 3 -> Choose none if not leave type exists |
# ===================================================================
self.env['hr.leave.type'].search([]).action_archive()
half_day_leave_type = self.env['hr.leave.type'].create({
'name': 'Test half day Leave Type',
'requires_allocation': False,
'request_unit': 'half_day',
'company_id': self.company.id,
'sequence': 10,
})
hour_leave_type = self.env['hr.leave.type'].create({
'name': 'Test hour Leave Type',
'requires_allocation': False,
'request_unit': 'hour',
'company_id': self.company.id,
})
hr_leave_default_value = self.env['hr.leave'].with_context({
'default_request_unit_hours': True,
}).default_get(list(self.env['hr.leave'].fields_get()) + ['holiday_status_id'])
self.assertEqual(hr_leave_default_value.get('holiday_status_id'), hour_leave_type.id)
self.env['hr.leave.type'].search([('id', '=', hour_leave_type.id)]).unlink()
hr_leave_default_value = self.env['hr.leave'].with_context({
'default_request_unit_hours': True,
}).default_get(list(self.env['hr.leave'].fields_get()) + ['holiday_status_id'])
self.assertEqual(hr_leave_default_value.get('holiday_status_id'), half_day_leave_type.id)
self.env['hr.leave.type'].search([('id', '=', half_day_leave_type.id)]).unlink()
hr_leave_default_value = self.env['hr.leave'].with_context({
'default_request_unit_hours': True,
}).default_get(list(self.env['hr.leave'].fields_get()) + ['holiday_status_id'])
self.assertEqual(hr_leave_default_value.get('holiday_status_id'), False)
def test_leave_duration_on_public_holiday_with_flexible_request(self):
"""
Test the cases with flexible request and having no calendar in public holidays
Scenarios covered:
- Leave fully on a public holiday: duration should be 0.
- Leave partially overlapping with a multi-day public holiday: only working days should count.
- Leave fully outside any public holidays: all days should count.
- Single-day leave that falls entirely on a public holiday: duration should be 0.
- Leave starting before and ending during a public holiday: only non-overlapping portion counts.
"""
calendar = self.env['resource.calendar'].create({
'name': 'Test calendar',
'hours_per_day': 8,
'hours_per_week': 56,
'full_time_required_hours': 56,
'flexible_hours': True
})
self.employee_emp.resource_calendar_id = calendar
self.env['resource.calendar.leaves'].create([
{
'date_from': datetime(2022, 3, 8, 0, 0, 0),
'date_to': datetime(2022, 3, 10, 23, 59, 59),
'calendar_id': calendar.id,
'company_id': self.employee_emp.company_id.id,
'resource_id': False,
},
{
'date_from': datetime(2022, 3, 15, 0, 0, 0),
'date_to': datetime(2022, 3, 17, 23, 59, 59),
'calendar_id': calendar.id,
'company_id': self.employee_emp.company_id.id,
'resource_id': False,
}
])
leave_data = [
{
'name': 'Leave fully on last day of first public holidays (Mar 10)',
'request_date_from': date(2022, 3, 10),
'request_date_to': date(2022, 3, 10),
},
{
'name': 'Leave partially overlapping first holiday (Mar 7 to Mar 9)',
'request_date_from': date(2022, 3, 7),
'request_date_to': date(2022, 3, 9),
},
{
'name': 'Leave fully outside holidays (Mar 11 to Mar 13)',
'request_date_from': date(2022, 3, 11),
'request_date_to': date(2022, 3, 13),
},
{
'name': 'Single day leave overlapping second holiday (Mar 17)',
'request_date_from': date(2022, 3, 17),
'request_date_to': date(2022, 3, 17),
},
{
'name': 'Leave partially overlapping second holiday (Mar 14 to Mar 16)',
'request_date_from': date(2022, 3, 14),
'request_date_to': date(2022, 3, 16),
}
]
leaves = self.env["hr.leave"].with_user(self.user_employee_id).create([
{
**data,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
}
for data in leave_data
])
expected_days_list = [0, 1, 3, 0, 1]
for leave, expected_days, data in zip(leaves, expected_days_list, leave_data):
self.assertEqual(
leave.number_of_days,
expected_days,
f"{data['name']} should have {expected_days} days duration"
)
@freeze_time('2011-12-24 10:00:00')
def test_validated_leave_back_to_approval(self):
"""
=====================================================================================================================
| case 1: An approved leave can be moved back to confirm state if user has group `group_hr_holidays_user` |
| case 2: An approved leave can't be moved back to confirm state if user doesn't have group `group_hr_holidays_user`|
=====================================================================================================================
"""
sick_leave_type = self.env['hr.leave.type'].create({
'name': 'leave in days',
'request_unit': 'day',
'leave_validation_type': 'both',
'requires_allocation': False,
})
sick_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'leave for 3 days',
'employee_id': self.employee_emp_id,
'holiday_status_id': sick_leave_type.id,
'request_date_from': '2011-12-23',
'request_date_to': '2011-12-25',
})
sick_leave.with_user(self.user_hruser_id).action_approve()
sick_leave.with_user(self.user_hruser_id).action_back_to_approval()
self.assertEqual(sick_leave.state, 'confirm')
sick_leave.with_user(self.user_hruser_id).action_approve()
sick_leave.with_user(self.user_employee_id).action_back_to_approval()
self.assertEqual(sick_leave.state, 'validate')
def test_unified_time_off_half_day_scenarios(self):
leave_half_day_am = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Half Day Morning',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-01'),
'request_date_to': time.strftime('2024-04-01'),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
})
self.assertEqual(leave_half_day_am.duration_display, '0.5 days')
leave_half_day_pm = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Half Day Afternoon',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-02'),
'request_date_to': time.strftime('2024-04-02'),
'request_date_from_period': 'pm',
'request_date_to_period': 'pm',
})
self.assertEqual(leave_half_day_pm.duration_display, '0.5 days')
leave_half_day_multi = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Day and half starting Afternoon',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-03'),
'request_date_to': time.strftime('2024-04-04'),
'request_date_from_period': 'pm',
'request_date_to_period': 'pm',
})
self.assertEqual(leave_half_day_multi.duration_display, '1.5 days')
leave_half_day_multi2 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Day and half starting Morning',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-08'),
'request_date_to': time.strftime('2024-04-09'),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
})
self.assertEqual(leave_half_day_multi2.duration_display, '1.5 days')
leave_half_day_multi3 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': '2 Days starting Morning',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-10'),
'request_date_to': time.strftime('2024-04-11'),
'request_date_from_period': 'am',
'request_date_to_period': 'pm',
})
self.assertEqual(leave_half_day_multi3.duration_display, '2 days')
leave_half_day_multi4 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': '2 Days starting Afternoon',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-12'),
'request_date_to': time.strftime('2024-04-16'),
'request_date_from_period': 'pm',
'request_date_to_period': 'am',
})
self.assertEqual(leave_half_day_multi4.duration_display, '2 days')
def test_unified_time_off_half_day_scenarios_irregular_calendar(self):
employee = self.employee_emp
employee.resource_calendar_id = self.irregular_calendar
irregular_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Monday and Tuesday Morning',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-01'),
'request_date_to': time.strftime('2024-04-02'),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
})
self.assertEqual(irregular_leave.duration_display, '1 days')
irregular_leave2 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Tuesday Afternoon and Wednesday Morning',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-02'),
'request_date_to': time.strftime('2024-04-03'),
'request_date_from_period': 'pm',
'request_date_to_period': 'am',
})
self.assertEqual(irregular_leave2.duration_display, '1.5 days')
irregular_leave3 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Thursday and Friday',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-04'),
'request_date_to': time.strftime('2024-04-05'),
'request_date_from_period': 'am',
'request_date_to_period': 'pm',
})
self.assertEqual(irregular_leave3.duration_display, '1.5 days')
irregular_leave4 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Friday/Weekend/Monday Morning',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': time.strftime('2024-04-12'),
'request_date_to': time.strftime('2024-04-15'),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
})
self.assertEqual(irregular_leave4.duration_display, '1 days')
def test_unified_time_off_hours_scenarios(self):
with self.assertRaises(UserError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Non Working Hours',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_hours.id,
'request_date_from': time.strftime('2024-04-01'),
'request_date_to': time.strftime('2024-04-01'),
'request_hour_from': 4,
'request_hour_to': 6,
})
leave_hours_day_mid_break = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'A leave that surrounds the lunch break',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_hours.id,
'request_date_from': time.strftime('2024-04-01'),
'request_date_to': time.strftime('2024-04-01'),
'request_hour_from': 11,
'request_hour_to': 15,
})
self.assertEqual(leave_hours_day_mid_break.duration_display, '3:00 hours')
leave_hours_day_end = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Leave at end of day',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_hours.id,
'request_date_from': time.strftime('2024-04-01'),
'request_date_to': time.strftime('2024-04-01'),
'request_hour_from': 16,
'request_hour_to': 23,
})
self.assertEqual(leave_hours_day_end.duration_display, '1:00 hours')
leave_hours_multi = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Leave Spanning 2 days',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_hours.id,
'request_date_from': time.strftime('2024-04-02'),
'request_date_to': time.strftime('2024-04-03'),
'request_hour_from': 8,
'request_hour_to': 9,
})
self.assertEqual(leave_hours_multi.duration_display, '9:00 hours')
leave_hours_multi2 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Leave at the end of a day spanning 3 days',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_hours.id,
'request_date_from': time.strftime('2024-04-03'),
'request_date_to': time.strftime('2024-04-05'),
'request_hour_from': 22,
'request_hour_to': 14,
})
self.assertEqual(leave_hours_multi2.duration_display, '13:00 hours')
def test_unified_time_off_hours_scenarios_irregular_calendar(self):
employee = self.employee_emp
employee.resource_calendar_id = self.irregular_calendar
irregular_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hour in Monday and all Tuesday',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_hours.id,
'request_date_from': time.strftime('2024-04-01'),
'request_date_to': time.strftime('2024-04-02'),
'request_hour_from': 17,
'request_hour_to': 20,
})
self.assertEqual(irregular_leave.duration_display, '6:12 hours')
irregular_leave2 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Wednesday To Friday',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_hours.id,
'request_date_from': time.strftime('2024-04-03'),
'request_date_to': time.strftime('2024-04-05'),
'request_hour_from': 14.6,
'request_hour_to': 9,
})
self.assertEqual(irregular_leave2.duration_display, '8:36 hours')
def test_time_off_creation_without_allocation(self):
leave_type = self.env['hr.leave.type'].create({
'name': 'Smart Leave',
'requires_allocation': True,
'leave_validation_type': 'hr',
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].create({
'name': 'Smart Leave Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type.id,
'request_date_from': '2024-07-01',
'request_date_to': '2024-07-02',
})
def test_coextensive_holidays_one_include_public_leave(self):
"""
The purpose is to test whether two holidays that span the same time frame,
one with the include_public_leave active, will both work correctly.
"""
employee = self.employee_emp
employee_hr = self.employee_hrmanager
calendar = employee.resource_calendar_id
calendar = employee_hr.resource_calendar_id
sick_leave_type = self.env['hr.leave.type'].create({
'name': 'Sick Leave (days)',
'request_unit': 'day',
'leave_validation_type': 'hr',
'requires_allocation': False,
})
sick_leave_type_paid = self.env['hr.leave.type'].create({
'name': 'Paid Leave (days)',
'request_unit': 'day',
'leave_validation_type': 'hr',
'requires_allocation': False,
})
sick_leave_type.include_public_holidays_in_duration = True
sick_leave = self.env['hr.leave'].create({
'name': 'Sick 3 days',
'employee_id': employee.id,
'holiday_status_id': sick_leave_type.id,
'request_date_from': '2021-11-15',
'request_date_to': '2021-11-17',
})
sick_leave_hr = self.env['hr.leave'].create({
'name': 'Paid 3 days',
'employee_id': employee_hr.id,
'holiday_status_id': sick_leave_type_paid.id,
'request_date_from': '2021-11-15',
'request_date_to': '2021-11-17',
})
calendar.global_leave_ids = [Command.create({
'name': 'Autumn Holidays',
'date_from': '2021-11-16 00:00:00',
'date_to': '2021-11-16 23:59:59',
'time_type': 'leave',
})]
self.assertEqual(sick_leave.duration_display, '3 days', "hr_holidays: duration_display should not update after adding an overlapping holiday")
self.assertEqual(sick_leave_hr.duration_display, '2 days', "hr_holidays: duration_display should update after adding an overlapping holiday")
def test_leave_request_by_removing_dates_holiday_status_id(self):
"""
Test that removing the dates of a leave request or a holiday_status_id
does not raise a traceback.
"""
with Form(self.env['hr.leave']) as leave_form:
leave_form.name = 'Test leave'
leave_form.employee_id = self.employee_emp
leave_form.holiday_status_id = self.holidays_type_1
leave_form.request_date_from = date(2022, 3, 11)
leave_form.request_date_to = date(2022, 3, 11)
leave_form.request_date_from = False
leave_form.request_date_to = False
leave_form.holiday_status_id = self.env['hr.leave.type']
leave_form.request_date_from = date(2022, 3, 11)
leave_form.request_date_to = date(2022, 3, 11)
leave_form.holiday_status_id = self.holidays_type_1
def test_calendar_event_create_access_rights(self):
"""Test that a manager can validate a leave request for an employee linked to a portal user.
Customers defined custom ACLs and record rules to support the possibility to assign a portal user to employees
and still be able to manage their holidays.
"""
# Add the required ACLs and record rules to allow portal users to create `calendar.event`.
# This reflects the customization done by customers for the reason explained above.
self.env['ir.model.access'].create([
# Read access on `mail.activity.type` for portal required for
# https://github.com/odoo/odoo/blob/cc0060e889603eb2e47fa44a8a22a70d7d784185/addons/calendar/models/calendar_event.py#L734
{
'name': 'Portal can read mail.activity.type',
'model_id': self.env.ref('mail.model_mail_activity_type').id,
'group_id': self.env.ref('base.group_portal').id,
'perm_read': True, 'perm_create': False, 'perm_write': False, 'perm_unlink': False,
},
# Read access on `mail.activity` for portal required for
# https://github.com/odoo/odoo/blob/cc0060e889603eb2e47fa44a8a22a70d7d784185/addons/calendar/models/calendar_event.py#L786
# https://github.com/odoo/odoo/blob/cc0060e889603eb2e47fa44a8a22a70d7d784185/addons/calendar/models/calendar_event.py#L882
{
'name': 'Portal can read mail.activity',
'model_id': self.env.ref('mail.model_mail_activity').id,
'group_id': self.env.ref('base.group_portal').id,
'perm_read': True, 'perm_create': False, 'perm_write': False, 'perm_unlink': False,
},
# Read and create acess on `calendar.event` for portal required for
# https://github.com/odoo/odoo/blob/cc0060e889603eb2e47fa44a8a22a70d7d784185/addons/hr_holidays/models/hr_leave.py#L894-L898
# Write and unlink added to match the customer customization + out of common sense,
# if you give create to portal for their own events,
# you give write and unlink so they can manage their own events
{
'name': 'Portal all CRUD on calendar.event',
'model_id': self.env.ref('calendar.model_calendar_event').id,
'group_id': self.env.ref('base.group_portal').id,
'perm_read': True, 'perm_create': True, 'perm_write': True, 'perm_unlink': True,
},
# Read and create acess on `calendar.event` for portal required for
# https://github.com/odoo/odoo/blob/cc0060e889603eb2e47fa44a8a22a70d7d784185/addons/calendar/models/calendar_event.py#L760-L768
# Write and unlink added to match the customer customization + out of common sense,
# if you give create to portal for their own events attendees,
# you give write and unlink so they can manage their own attendees
{
'name': 'Portal all CRUD on calendar.attendee',
'model_id': self.env.ref('calendar.model_calendar_attendee').id,
'group_id': self.env.ref('base.group_portal').id,
'perm_read': True, 'perm_create': True, 'perm_write': True, 'perm_unlink': True,
}])
self.env['ir.rule'].create([
# Restrict portals to their own activities
# so they cannot read the activities of other users
{
'name': 'Portal own mail activity',
'model_id': self.env.ref('mail.model_mail_activity').id,
'groups': [(4, self.env.ref('base.group_portal').id)],
'domain_force': "['|', ('user_id', '=', user.id), ('create_uid', '=', user.id)]",
},
# Restrict portals to their own events
# so they cannot read the events of other users
{
'name': 'Portal own calendar events',
'model_id': self.env.ref('calendar.model_calendar_event').id,
'groups': [(4, self.env.ref('base.group_portal').id)],
'domain_force': "[('partner_ids', 'in', user.partner_id.id)]",
},
# Restrict portals to their own attendees
# so they cannot read the attendees of other users
{
'name': 'Portal own calendar attendees',
'model_id': self.env.ref('calendar.model_calendar_attendee').id,
'groups': [(4, self.env.ref('base.group_portal').id)],
'domain_force': "[('partner_id', '=', user.partner_id.id)]",
}
])
# Create a portal user and assign it to the employee
user_portal = self.env['res.users'].create({
'name': 'Portal', 'login': 'portal_user', 'password': 'portal_user',
'group_ids': [(6, 0, [self.env.ref('base.group_portal').id])],
})
self.employee_emp.user_id = user_portal
# As a manager, create a leave request for the employee linked to a portal user
with freeze_time('2025, 1, 8'):
leave = self.env['hr.leave'].with_user(self.user_hrmanager_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
'request_date_from': (datetime.today() - relativedelta(days=7)),
'request_date_to': datetime.today(),
'number_of_days': 1,
})
# Assert the employee cannot approve his own leave request
with self.assertRaises(AccessError):
leave.with_user(self.user_employee_id).action_approve()
# Assert the manager can approve the leave request assign to portal employee
leave.with_user(self.user_hrmanager_id).action_approve()
def test_set_employee_on_leave_req_without_start_date(self):
"""Test setting the employee on a leave request without a start date."""
leave_req_form = Form(self.env['hr.leave'].with_user(self.user_hrmanager_id))
leave_req_form.request_date_from = False
leave_req_form.employee_id = self.employee_responsible
self.assertFalse(leave_req_form.can_approve)
def test_change_leave_type_on_leave_req_without_end_date(self):
"""Test changing the leave type on a leave request without an end date."""
leave_req_form = Form(self.env['hr.leave'].with_user(self.user_hrmanager_id))
leave_req_form.request_date_to = False
leave_req_form.holiday_status_id = self.holidays_type_hours
self.assertFalse(leave_req_form.date_to)
def test_flexible_schedule_full_day_off(self):
"""this tests checks that if the morning and afternoon have been selected as time off and the schedule type of
the employee is flexible, the time considered off is a full day."""
calendar = self.env['resource.calendar'].sudo().create({
'company_id': False,
'name': 'Flexible 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'hours_per_week': 40.0,
'full_time_required_hours': 40.0,
'flexible_hours': True,
'schedule_type': 'flexible',
})
self.employee_hruser.write({
'is_flexible': True,
'resource_calendar_id': calendar.id
})
flex_leave = self.env['hr.leave'].create({
'name': "Full Day Leave",
'employee_id': self.employee_hruser.id,
'holiday_status_id': self.holidays_type_half.id,
'request_date_from': "2025-08-29",
'request_date_to': "2025-08-29",
'request_date_from_period': 'am',
'request_date_to_period': 'pm',
})
leave = flex_leave._get_durations()
self.assertEqual(leave[flex_leave.id][0], 1, "The total leaves should be 1.")
@freeze_time("2025-12-19")
def test_duration_flexible_employee_different_timezone(self):
calendar = self.env['resource.calendar'].create({
'name': 'Test calendar',
'hours_per_day': 8,
'hours_per_week': 56,
'full_time_required_hours': 56,
'flexible_hours': True
})
self.employee_emp.tz = 'Australia/Darwin'
self.employee_emp.resource_calendar_id = calendar
self.env.user.tz = 'Europe/Brussels'
self.holidays_type_1.request_unit = 'hour'
leave = self.env['hr.leave'].with_user(self.env.user).create({
'name': 'Test',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_unit_hours': True,
'request_hour_from': 8,
'request_hour_to': 21,
})
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')