Odoo18-Base/addons/hr_holidays/tests/test_leave_requests.py
2025-03-10 11:12:23 +07:00

1158 lines
54 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, UTC
from odoo import fields
from odoo.exceptions import UserError, ValidationError
from odoo.tools import mute_logger
from odoo.tests.common import Form
from odoo.tests import tagged
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('leave_requests')
class TestLeaveRequests(TestHrHolidaysCommon):
def _check_holidays_status(self, holiday_status, ml, lt, rl, vrl):
self.assertEqual(holiday_status.max_leaves, ml,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.leaves_taken, lt,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.remaining_leaves, rl,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.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': 'no',
'leave_validation_type': 'hr',
})
cls.holidays_type_2 = LeaveType.create({
'name': 'Limited',
'requires_allocation': 'yes',
'employee_requests': 'yes',
'leave_validation_type': 'hr',
})
cls.holidays_type_3 = LeaveType.create({
'name': 'TimeNotLimited',
'requires_allocation': 'no',
'leave_validation_type': 'manager',
})
cls.holidays_type_4 = LeaveType.create({
'name': 'Limited with 2 approvals',
'requires_allocation': 'yes',
'employee_requests': 'yes',
'leave_validation_type': 'both',
})
cls.holidays_support_document = LeaveType.create({
'name': 'Time off with support document',
'support_document': True,
'requires_allocation': 'no',
'leave_validation_type': 'no_validation',
})
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, closest_allocation):
self.assertEqual(
holidays_count_result,
{
'closest_allocation_to_expire': closest_allocation,
'max_leaves': ml,
'leaves_taken': lt,
'remaining_leaves': rl,
'virtual_remaining_leaves': vrl,
'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,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
})
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,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_limited_type_no_days(self):
# Deprecated as part of https://github.com/odoo/odoo/pull/96545
# TODO: remove in master
return
@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'):
aloc1_user_group = 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-1-1'),
'date_to': time.strftime('%Y-12-31'),
})
aloc1_user_group.action_validate()
holiday_status = self.holidays_type_2.with_user(self.user_employee_id)
self._check_holidays_status(holiday_status, 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,
'date_from': (datetime.today() - relativedelta(days=2)),
'date_to': datetime.today(),
'number_of_days': 2,
})
holiday_status.invalidate_model()
self._check_holidays_status(holiday_status, 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, 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 """
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,
}).action_validate()
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,
'date_from': fields.Datetime.from_string('2017-03-03 06:00:00'),
'date_to': fields.Datetime.from_string('2017-03-11 19:00:00'),
'number_of_days': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_accrual_validity_time_not_valid(self):
# Deprecated as part of https://github.com/odoo/odoo/pull/96545
# TODO: remove in master
return
@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_form = Form(self.env['hr.leave'].with_user(self.user_hrmanager), view='hr_holidays.hr_leave_view_form_manager')
leave_form.holiday_type = 'department'
leave_form.department_id = self.hr_dept
leave_form.holiday_status_id = self.holidays_type_1
leave_form.request_date_from = date(2019, 5, 6)
leave_form.request_date_to = date(2019, 5, 6)
leave = leave_form.save()
leave.action_approve()
member_ids = self.hr_dept.member_ids.ids
self.assertEqual(self.env['hr.leave'].search_count([('employee_id', 'in', member_ids)]), len(member_ids), "Leave should be created for members of department")
@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.name = 'New Allocation Request'
allocation_form.holiday_status_id = self.holidays_type_2
allocation_form.date_from = date(2019, 5, 6)
allocation_form.date_to = date(2019, 5, 6)
allocation = 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,
'date_from': (fields.Datetime.now() - relativedelta(days=1)),
'date_to': fields.Datetime.now() + relativedelta(days=1),
'number_of_days': 2,
})
(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_unit_hours': True,
'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,
'request_unit_hours': True,
'holiday_type': 'company',
'mode_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_form = Form(self.env['hr.leave'], view='hr_holidays.hr_leave_view_form_manager')
leave_form.holiday_type = 'company'
leave_form.mode_company_id = company
leave_form.holiday_status_id = self.holidays_type_1
leave_form.request_date_from = date(2019, 5, 6)
leave_form.request_date_to = date(2019, 5, 6)
# TODO: The test is wrong by modifying `date_from` and `date_to`, which are invisible
# It should edit only `request_date_from` and `request_date_to` instead
# And there is really a bug. Using the web client, when you put your PC in Auckland timezone,
# and the admin preferences in Auckland Timezone
# and create a time off for the current day, the computation is completely wrong
# and compute the date to before the date from *-)
# For instance, for a time-off from 06/16/2022 to 06/16/2022 (1 day) it computes
# 06/16/2022 08:00:00 as date_from and 06/15/2022 17:00:00 as date_to
# Bug reported to the rd-fun-vidange channel to the dev who introduced the bug
# https://discord.com/channels/678381219515465750/687337760452902925/986918361768263710
leave_form._view['modifiers']['date_from']['invisible'] = False
leave_form._view['modifiers']['date_to']['invisible'] = False
leave_form.date_from = datetime(2019, 5, 6, 0, 0, 0)
leave_form.date_to = datetime(2019, 5, 6, 23, 59, 59)
leave = leave_form.save()
leave.state = 'confirm'
leave.action_validate()
employee_leave = self.env['hr.leave'].search([('employee_id', '=', employee.id)])
self.assertEqual(
employee_leave.request_date_from, date(2019, 5, 5),
"Timezone should be be adapted on the employee leave"
)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_timezone_department_leave_request(self):
""" Create a leave request for a department in another timezone """
company = self.env['res.company'].create({'name': "Hergé"})
company.resource_calendar_id.tz = 'Australia/Sydney' # GMT+12
department = self.env['hr.department'].create({'name': "Museum", 'company_id': company.id})
leave = self.env['hr.leave'].new({
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'request_unit_hours': True,
'holiday_type': 'department',
'department_id': department.id,
'request_date_from': date(2019, 5, 6),
'request_date_to': date(2019, 5, 6),
'request_hour_from': '8', # 8:00 AM in the department's timezone
'request_hour_to': '17', # 5:00 PM in the department'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")
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 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 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 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 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',
})
allocation = 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-1-1'),
'date_to': time.strftime('%Y-1-1'),
})
allocation.action_validate()
leave1 = self.env['hr.leave'].create({
'name': 'Holiday 1 week',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-23 06:00:00'),
'date_to': fields.Datetime.from_string('2019-12-27 20:00:00'),
'number_of_days': 5,
})
self.assertEqual(leave1.number_of_hours_display, 38)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours_display, 38)
leave1.action_validate()
self.assertEqual(leave1.number_of_hours_display, 38)
leave2 = self.env['hr.leave'].create({
'name': 'Holiday 1 Day',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-30 06:00:00'),
'date_to': fields.Datetime.from_string('2019-12-30 13:00:00'),
'number_of_days': 1,
})
self.assertEqual(leave2.number_of_hours_display, 4)
leave2.action_approve()
self.assertEqual(leave2.number_of_hours_display, 4)
leave2.action_validate()
self.assertEqual(leave2.number_of_hours_display, 4)
def test_number_of_hours_display_global_leave(self):
# Check that the field number_of_hours_display
# 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 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 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 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 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 Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
],
'global_leave_ids': [(0, 0, {
'name': 'Christmas Leave',
'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': 'no',
})
leave1 = self.env['hr.leave'].create({
'name': 'Sick 1 week during christmas snif',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-23 06:00:00'),
'date_to': fields.Datetime.from_string('2019-12-27 20:00:00'),
'number_of_days': 5,
})
self.assertEqual(leave1.number_of_hours_display, 24)
leave1.action_approve()
self.assertEqual(leave1.number_of_hours_display, 24)
leave1.action_validate()
self.assertEqual(leave1.number_of_hours_display, 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)
# Mimic what is done by the calendar widget when clicking on a day. It
# will take the local datetime from 0:00 to 23:59
values = {
'date_from': tz.localize(local_date_from).astimezone(UTC).replace(tzinfo=None),
'date_to': tz.localize(local_date_to).astimezone(UTC).replace(tzinfo=None), # note that this can be the next day in UTC
}
values.update(self.env['hr.leave'].with_user(self.user_employee_id)._default_get_request_parameters(values))
# Dates should be local to the user
self.assertEqual(values['request_date_from'], local_date_from.date())
self.assertEqual(values['request_date_to'], local_date_to.date())
values.update({
'name': 'Test',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
})
leave = self.env['hr.leave'].with_user(self.user_employee_id).new(values)
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 = datetime(2020, 1, 1, 0, 0, 0)
local_date_to = datetime(2020, 1, 1, 23, 59, 59)
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 = datetime(2020, 1, 1, 0, 0, 0)
local_date_to = datetime(2020, 1, 8, 23, 59, 59)
for tz in timezones_to_test:
self._test_leave_with_tz(tz, local_date_from, local_date_to, 6)#TODO JUD check why this fails
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_validate()
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,
'date_from': '2021-09-01',
'date_to': '2021-09-02',
'number_of_days': 1,
})
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,
'date_from': '2020-09-01',
'date_to': '2020-09-02',
'number_of_days': 1,
})
def test_no_days_expired(self):
# First expired allocation
allocation1 = 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',
})
allocation2 = 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',
})
allocation1.action_validate()
allocation2.action_validate()
# 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,
'date_from': '2021-09-06',
'date_to': '2021-09-10',
'number_of_days': 5,
})
# 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,
'date_from': '2021-09-06',
'date_to': '2021-09-08',
'number_of_days': 3,
})
def test_company_leaves(self):
# First expired allocation
allocation = self.env['hr.leave.allocation'].create({
'name': 'Allocation',
'holiday_type': 'company',
'mode_company_id': self.env.company.id,
'holiday_status_id': self.holidays_type_1.id,
'number_of_days': 20,
'state': 'confirm',
'date_from': '2021-01-01',
})
allocation.action_validate()
req1_form = Form(self.env['hr.leave'].sudo())
req1_form.employee_ids.add(self.employee_emp)
req1_form.employee_ids.add(self.employee_hrmanager)
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')
self.assertEqual(req1_form.number_of_days_display, 3)
req1_form.save().action_approve()
req2_form = Form(self.env['hr.leave'].sudo())
req2_form.employee_ids.add(self.employee_hruser)
req2_form.holiday_status_id = self.holidays_type_1
req2_form.request_date_from = fields.Date.to_date('2021-12-06')
req2_form.request_date_to = fields.Date.to_date('2021-12-08')
self.assertEqual(req2_form.number_of_days_display, 3)
def test_leave_with_public_holiday_other_company(self):
other_company = self.env['res.company'].create({
'name': 'Test Company',
})
# 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',
'holiday_type': 'employee',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_1.id,
'date_from': datetime(2022, 3, 11),
'date_to': datetime(2022, 3, 11, 23, 59, 59),
})
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',
}
allocation1 = self.env['hr.leave.allocation'].create(allocation_vals)
allocation2 = self.env['hr.leave.allocation'].create(allocation_vals)
allocation1.action_validate()
allocation2.action_validate()
# 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,
'date_from': '2022-01-01',
'date_to': '2022-01-15',
'number_of_days': 10,
})
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_vals.update({'number_of_days': 1})
allocation_1day = Allocation.create(allocation_vals)
allocations = (allocation_4days + allocation_1day)
allocations.action_validate()
leave_vals.update({
'date_from': '2022-01-03 00:00:00',
'date_to': '2022-01-06 23:59:59',
'number_of_days': 4,
})
leave_draft = Leave.create(leave_vals)
leave_draft.action_refuse()
leave_vals.update({
'date_from': '2022-01-03 00:00:00',
'date_to': '2022-01-06 23:59:59',
'number_of_days': 4,
})
leave_4days = Leave.create(leave_vals)
leave_vals.update({
'date_from': '2022-01-07 00:00:00',
'date_to': '2022-01-07 23:59:59',
'number_of_days': 1,
})
leave_1day = Leave.create(leave_vals)
leaves = (leave_4days + leave_1day)
leaves.action_approve()
allocation_days = self.holidays_type_2._get_employees_days_per_allocation([self.employee_emp_id])
self.assertEqual(allocation_days[self.employee_emp_id][self.holidays_type_2][allocation_4days]['leaves_taken'], leave_4days['number_of_%ss_display' % unit], 'As 4 days were available in this allocation, they should have been taken')
self.assertEqual(allocation_days[self.employee_emp_id][self.holidays_type_2][allocation_1day]['leaves_taken'], leave_1day['number_of_%ss_display' % 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,
'date_from': '2021-12-06 00:00:00',
'date_to': '2021-12-10 23:59:59',
},
{
'name': 'Holiday Request',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_type_1.id,
'date_from': '2021-12-06 00:00:00',
'date_to': '2021-12-10 23:59:59',
}
])
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,
'date_from': '2021-12-06 00:00:00',
'date_to': '2021-12-10 23:59:59',
})
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,
'date_from': '2021-12-13 00:00:00',
'date_to': '2021-12-17 23:59:59',
})
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, 2)
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,
'date_from': '2021-12-06 00:00:00',
'date_to': '2021-12-10 23:59:59',
})
self.assertEqual(time_off.number_of_days, 4)
global_time_off.unlink()
self.assertEqual(time_off.number_of_days, 3)
def test_time_off_auto_cancel(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,
'date_from': '2021-11-15 00:00:00',
'date_to': '2021-11-19 23:59:59',
})
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.active, False)
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_validate()
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,
'date_from': '2020-09-06',
'date_to': '2020-09-08',
'number_of_days': 3,
})
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=5, lt=0, rl=5, vrl=2, vlt=3, closest_allocation=allocation,
)
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_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_2021.action_validate()
allocation_2022.action_validate()
# Leave taken in 2021
leave_2021 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'holiday_type': 'employee',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_2.id,
'date_from': datetime(2021, 8, 9, 0, 0, 0),
'date_to': datetime(2021, 8, 13, 23, 59, 59),
})
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_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=5, vlt=5, closest_allocation=allocation_2021,
)
# Virtual remaining leave is equal to 1 because there is only one day remaining in the allocation based on its validity
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 31))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=1, vlt=5, closest_allocation=allocation_2022,
)
leave_2022 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'holiday_type': 'employee',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.holidays_type_2.id,
'date_from': datetime(2022, 8, 9, 0, 0, 0),
'date_to': datetime(2022, 8, 13, 23, 59, 59),
})
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_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=20, lt=4, rl=16, vrl=16, vlt=4, closest_allocation=allocation_2022,
)
# The holidays count in 2021 is not affected by the leave taken in 2022
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=5, vlt=5, closest_allocation=allocation_2021,
)
with self.assertRaisesRegex(UserError,
r'You cannot archive an allocation which is in confirm or validate state.'):
# The logic of the test is relevant, so we do not remove it.
# However, the behaviour will change.
# Indeed, a confirmed or validated allocation cannot be archived
allocation_2021.active = False
# If the allocation is archived, the leaves taken are still counted on this allocation
# but the max leaves and remaining leaves are not counted anymore
# If there are no virtual_remaining_leaves, then there is no upcoming allocation (closest_allocation_to_expire) to expire
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=0, lt=5, rl=0, vrl=0, vlt=5, closest_allocation=False,
)
# The holidays count in 2022 is not affected by the archived allocation in 2021
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=20, lt=4, rl=16, vrl=16, vlt=4, closest_allocation=allocation_2022,
)
allocation_2021.active = True
# The holidays count in 2021 is back to what it was when the allocation was active
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=5, vlt=5, closest_allocation=allocation_2021,
)
# The holidays count in 2022 is still not affected by the allocation in 2021
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=20, lt=4, rl=16, vrl=16, vlt=4, closest_allocation=allocation_2022,
)
def test_cancel_leave(self):
with freeze_time('2020-09-15'):
allocation = 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',
})
allocation.action_validate()
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,
'date_from': '2020-09-21',
'date_to': '2020-09-23',
'number_of_days': 3,
})
# A meeting is only created once the leave is validated
self.assertFalse(leave.meeting_id)
leave.with_user(self.user_hrmanager_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_validate()
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,
'date_from': '2022-10-17',
'date_to': '2022-10-17',
'number_of_days': 1,
'supported_attachment_ids': [(6, 0, [])], # Sent by webclient
})
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.
"""
holiday_type_A = self.env['hr.leave.type'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Type A',
'requires_allocation': 'yes',
'employee_requests': 'yes',
'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': holiday_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': holiday_type_A.id,
'number_of_days': 5,
'state': 'confirm',
'date_from': '2023-04-01',
},
])
allocations.action_validate()
trigger_error_leave = {
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
'holiday_status_id': holiday_type_A.id,
'date_from': '2023-03-14',
'date_to': '2023-03-16',
'number_of_days': 3,
}
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': 'no',
})
current_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'holiday_type': 'employee',
'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_hruser_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_validate()
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), self.env.cr.savepoint():
run_validation_flow(leave_validation_type)
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',
})
sick_leave = self.env['hr.leave'].create({
'name': 'Sick 3 days',
'employee_id': employee.id,
'holiday_status_id': sick_leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-23 06:00:00'),
'date_to': fields.Datetime.from_string('2019-12-25 20:00:00'),
})
comp_leave_type = self.env['hr.leave.type'].create({
'name': 'OT Compensation (hours)',
'request_unit': 'hour',
'leave_validation_type': 'manager',
})
comp_leave = self.env['hr.leave'].create({
'name': 'OT Comp (12 hours)',
'employee_id': employee.id,
'holiday_status_id': comp_leave_type.id,
'date_from': fields.Datetime.from_string('2019-12-26 12:00:00'),
'date_to': fields.Datetime.from_string('2019-12-27 20:00:00'),
})
self.assertEqual(sick_leave.duration_display, '3 days')
self.assertEqual(comp_leave.duration_display, '12 hours')
calendar.global_leave_ids = [(0, 0, {
'name': 'Winter Holidays',
'date_from': fields.Datetime.from_string('2019-12-25 00:00:00'),
'date_to': fields.Datetime.from_string('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, '8 hours', msg)
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 = 'no'
self.assertEqual(self.holidays_type_2.requires_allocation, 'no', '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 = 'yes'
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_2.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)