# 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 UserError, ValidationError from odoo.tools import date_utils, mute_logger from odoo.tests import Form, tagged 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': '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): 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-1-1'), 'date_to': time.strftime('%Y-12-31'), }) allocation.action_validate() # 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-1-1'), 'date_to': time.strftime('%Y-12-31'), }) allocation.action_validate() 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_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, '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") @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.date_from = date(2019, 5, 6) allocation_form.date_to = date(2019, 5, 6) 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_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, '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-1-1'), 'date_to': time.strftime('%Y-1-1'), }) 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_validate() 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_validate() self.assertEqual(leave2.number_of_hours, 4) 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': 'no', }) 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_validate() 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_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, '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_validate() 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_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, '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') self.assertEqual(req1_form.number_of_days, 3) req1_form.save().action_approve() 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) self.env['hr.leave.allocation'].create(allocation_vals) # 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_validate() allocation_vals.update({'number_of_days': 1}) allocation_1day = Allocation.create(allocation_vals) allocation_1day.action_validate() 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_vals.update({ 'request_date_from': '2022-01-03', 'request_date_to': '2022-01-06', }) 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_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, '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_validate() 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_validate() # 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', }) 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_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, 'request_date_from': '2022-10-17', 'request_date_to': '2022-10-17', '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. """ 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': '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': 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_validate() 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': 'no', '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_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) @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', }) 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', }) comp_leave = self.env['hr.leave'].create({ 'name': 'OT Comp (4 hours)', 'employee_id': employee.id, 'holiday_status_id': comp_leave_type.id, 'request_unit_hours': True, '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', }) 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, '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_validate() self.env['hr.leave'].with_user(self.user_employee_id).create({ 'name': 'Holiday Request', 'employee_id': employee.id, 'holiday_status_id': self.holidays_type_4.id, 'request_date_from': '2024-01-23', 'request_date_to': '2024-01-27', }) holiday_status = self.holidays_type_4.with_user(self.user_employee_id) self._check_holidays_status(holiday_status, employee, 20.0, 0.0, 20.0, 16.0) 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_2 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_validate() self.assertEqual(allocation.state, 'validate') search_domain = ['|', ['requires_allocation', '=', 'no'], '&', ['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(args=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 = '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'