19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_access_rights
@ -14,8 +13,25 @@ from . import test_leave_requests
from . import test_out_of_office
from . import test_company_leave
from . import test_res_partner
from . import test_stress_days
from . import test_resource_calendar
from . import test_mandatory_days
from . import test_global_leaves
from . import test_uninstall
from . import test_holidays_calendar
from . import test_holidays_mail
from . import test_negative
from . import test_past_accruals
from . import test_allocations
from . import test_multicompany
from . import test_timeoff_event
from . import test_working_hours
from . import test_dashboard
from . import test_expiring_leaves
from . import test_hr_departure_wizard
from . import test_time_off_card_tour
from . import test_hr_leave_type_tour
from . import test_time_off_graph_view_tour
from . import test_leave_type_data
from . import test_multi_contract
from . import test_time_off_allocation_tour
from . import test_flexible_resource_calendar

View file

@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo import Command, fields
from odoo.tests import common, Form
from odoo.tests.common import TransactionCase
from odoo.fields import Datetime
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.tests import common
class TestHrHolidaysCommon(common.TransactionCase):
@ -13,6 +17,26 @@ class TestHrHolidaysCommon(common.TransactionCase):
cls.env.user.tz = 'Europe/Brussels'
cls.env.user.company_id.resource_calendar_id.tz = "Europe/Brussels"
cls.company = cls.env['res.company'].create({'name': 'Test company'})
cls.external_company = cls.env['res.company'].create({'name': 'External Test company'})
cls.env.user.company_id = cls.company
# The available time off types are the ones whose:
# 1. Company is one of the selected companies.
# 2. Company is false but whose country is one the countries of the selected companies.
# 3. Company is false and country is false
# Thus, a time off type is defined to be available for `Test company`
# For example, the tour 'time_off_request_calendar_view' would succeed (false positive) without this leave type.
# However, the tour won't create a time-off request (as expected)because no time-off type is available to be selected on the leave
# This would cause the test case that uses the tour to fail.
cls.env['hr.leave.type'].create({
'name': 'Test Leave Type',
'requires_allocation': False,
'request_unit': 'day',
'company_id': cls.company.id,
})
# Test users to use through the various tests
cls.user_hruser = mail_new_test_user(cls.env, login='armande', groups='base.group_user,hr_holidays.group_hr_holidays_user')
cls.user_hruser_id = cls.user_hruser.id
@ -21,9 +45,12 @@ class TestHrHolidaysCommon(common.TransactionCase):
cls.user_hrmanager_id = cls.user_hrmanager.id
cls.user_hrmanager.tz = 'Europe/Brussels'
cls.user_employee = mail_new_test_user(cls.env, login='david', groups='base.group_user')
cls.user_responsible = mail_new_test_user(cls.env, login='Titus', groups='base.group_user,hr_holidays.group_hr_holidays_responsible')
cls.user_responsible_id = cls.user_responsible.id
cls.user_employee = mail_new_test_user(cls.env, login='enguerran', password='enguerran', groups='base.group_user')
cls.user_employee_id = cls.user_employee.id
cls.external_user_employee = mail_new_test_user(cls.env, login='external', password='external', groups='base.group_user')
cls.external_user_employee_id = cls.external_user_employee.id
# Hr Data
Department = cls.env['hr.department'].with_context(tracking_disable=True)
@ -34,13 +61,28 @@ class TestHrHolidaysCommon(common.TransactionCase):
'name': 'Research and devlopment',
})
cls.employee_responsible = cls.env['hr.employee'].create({
'name': 'David Employee',
'user_id': cls.user_responsible_id,
'department_id': cls.rd_dept.id,
})
cls.employee_emp = cls.env['hr.employee'].create({
'name': 'David Employee',
'user_id': cls.user_employee_id,
'leave_manager_id': cls.user_responsible_id,
'department_id': cls.rd_dept.id,
'company_id': cls.company.id,
})
cls.employee_emp_id = cls.employee_emp.id
cls.employee_external = cls.env['hr.employee'].create({
'name': 'external Employee',
'user_id': cls.external_user_employee_id,
'company_id': cls.external_company.id,
})
cls.external_employee_id = cls.employee_external.id
cls.employee_hruser = cls.env['hr.employee'].create({
'name': 'Armande HrUser',
'user_id': cls.user_hruser_id,
@ -58,3 +100,110 @@ class TestHrHolidaysCommon(common.TransactionCase):
cls.rd_dept.write({'manager_id': cls.employee_hruser_id})
cls.hours_per_day = cls.employee_emp.resource_id.calendar_id.hours_per_day or 8
def assert_remaining_leaves_equal(self, leave_type, value, employee, date=None, digits=None):
allocation_data = leave_type.get_allocation_data(employee, date)
if not date:
date = fields.Date.today()
if digits:
self.assertAlmostEqual(allocation_data[employee][0][1]['remaining_leaves'], value,
digits, f"Remaining leaves for date '{date}' are incorrect.")
else:
self.assertEqual(allocation_data[employee][0][1]['remaining_leaves'],
value, f"Remaining leaves for date '{date}' are incorrect.")
def _create_form_test_accrual_allocation(self, leave_type, date_from, employee, accrual_plan, date_to=None, creator_user=None):
allocation = self.env['hr.leave.allocation']
if creator_user:
allocation = allocation.with_user(creator_user)
with Form(allocation, 'hr_holidays.hr_leave_allocation_view_form_manager') as form:
form.name = 'Test accrual allocation'
form.allocation_type = 'accrual'
form.accrual_plan_id = accrual_plan
form.employee_id = employee
form.holiday_status_id = leave_type
form.date_from = date_from
if date_to:
form.date_to = date_to
return form.record
class TestHolidayContract(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Legal Leaves',
'time_type': 'leave',
'requires_allocation': False,
'responsible_ids': [Command.link(cls.env.ref('base.user_admin').id)],
})
cls.env.ref('base.user_admin').notification_type = 'inbox'
cls.dep_rd = cls.env['hr.department'].create({
'name': 'Research & Development - Test',
})
# I create a new employee "Jules"
cls.jules_emp = cls.env['hr.employee'].create({
'name': 'Jules',
'sex': 'male',
'birthday': '1984-05-01',
'country_id': cls.env.ref('base.be').id,
'department_id': cls.dep_rd.id,
})
cls.calendar_35h = cls.env['resource.calendar'].create({
'name': '35h calendar',
'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 Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, '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 Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, '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 Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, '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 Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, '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 Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
]
})
cls.calendar_40h = cls.env['resource.calendar'].create({'name': 'Default calendar'})
# This contract ends at the 15th of the month
cls.jules_emp.version_id.write({ # Fixed term contract
'contract_date_end': datetime.strptime('2015-11-15', '%Y-%m-%d'),
'contract_date_start': datetime.strptime('2015-01-01', '%Y-%m-%d'),
'date_version': datetime.strptime('2015-01-01', '%Y-%m-%d'),
'name': 'First CDD Contract for Jules',
'resource_calendar_id': cls.calendar_40h.id,
'wage': 5000.0,
})
cls.contract_cdd = cls.jules_emp.version_id
# This contract starts the next day
cls.contract_cdi = cls.jules_emp.create_version({
'date_version': datetime.strptime('2015-11-16', '%Y-%m-%d'),
'contract_date_start': datetime.strptime('2015-11-16', '%Y-%m-%d'),
'contract_date_end': False,
'name': 'Contract for Jules',
'resource_calendar_id': cls.calendar_35h.id,
'wage': 5000.0,
})
@classmethod
def create_leave(cls, date_from=None, date_to=None, name="", employee_id=False):
return cls.env['hr.leave'].create({
'name': name or 'Holiday!!!',
'employee_id': employee_id or cls.richard_emp.id,
'holiday_status_id': cls.leave_type.id,
'request_date_to': date_to or Datetime.today(),
'request_date_from': date_from or Datetime.today(),
})

View file

@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
import unittest
from datetime import date
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import tests
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import date_utils
from odoo.tools import mute_logger
@ -19,51 +22,47 @@ class TestHrHolidaysAccessRightsCommon(TestHrHolidaysCommon):
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Unlimited',
'leave_validation_type': 'hr',
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.rd_dept.manager_id = False
cls.hr_dept.manager_id = False
cls.employee_emp.parent_id = False
leave_day = date_utils.start_of(date.today() + relativedelta(days=30), 'week')
cls.employee_leave = cls.env['hr.leave'].with_user(cls.user_employee_id).create({
'name': 'Test',
'holiday_status_id': cls.leave_type.id,
'department_id': cls.employee_emp.department_id.id,
'employee_id': cls.employee_emp.id,
'date_from': datetime.now() + relativedelta(days=30),
'date_to': datetime.now() + relativedelta(days=31),
'number_of_days': 1,
'request_date_from': leave_day,
'request_date_to': leave_day,
})
cls.lt_no_validation = cls.env['hr.leave.type'].create({
'name': 'Validation = no_validation',
'leave_validation_type': 'hr',
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.lt_validation_hr = cls.env['hr.leave.type'].create({
'name': 'Validation = HR',
'leave_validation_type': 'hr',
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.lt_validation_manager = cls.env['hr.leave.type'].create({
'name': 'Validation = manager',
'leave_validation_type': 'hr',
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.lt_validation_both = cls.env['hr.leave.type'].create({
'name': 'Validation = both',
'leave_validation_type': 'hr',
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.draft_status = [
cls.lt_validation_hr,
cls.lt_validation_manager,
cls.lt_validation_both
]
cls.confirm_status = [
cls.lt_no_validation,
cls.lt_validation_hr,
@ -72,314 +71,16 @@ class TestHrHolidaysAccessRightsCommon(TestHrHolidaysCommon):
]
# Here we only test access rights, prevent any conflict with
# existing stress days - they are tested someplace else.
cls.env['hr.leave.stress.day'].search([]).unlink()
# existing mandatory days - they are tested someplace else.
cls.env['hr.leave.mandatory.day'].search([]).unlink()
def request_leave(self, user_id, date_from, number_of_days, values=None):
def request_leave(self, user_id, request_date_from, number_of_days, values=None):
values = dict(values or {}, **{
'date_from': date_from,
'request_date_from': date_from,
'date_to': date_from + relativedelta(days=number_of_days),
'request_date_to': date_from + relativedelta(days=number_of_days),
'number_of_days': number_of_days,
'request_date_from': request_date_from,
'request_date_to': request_date_from + relativedelta(days=number_of_days - 1),
})
return self.env['hr.leave'].with_user(user_id).create(values)
@tests.tagged('access_rights', 'access_rights_states')
class TestAcessRightsStates(TestHrHolidaysAccessRightsCommon):
# ******************************************************
# Action draft
# ******************************************************
def test_draft_status(self):
"""
We should only be able to draft a leave that is
in confirm or refuse state
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Ranoi',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.action_draft()
values = {
'name': 'Ranoi',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=20 + i), 1, values)
# the state has to be set to draft in a write because it is initialized to confirm if it has validation
leave.write({'state': 'draft'})
with self.assertRaises(UserError):
leave.action_draft()
def test_base_user_draft_his_leave(self):
"""
Should be able to draft his own leave
whatever the holiday_status_id
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_employee.id).action_draft()
def test_base_user_draft_other_employee_leave(self):
"""
Should not be able to draft the leave of someone else
whatever the holiday_status_id
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
with self.assertRaises(UserError):
leave.with_user(self.user_employee.id).action_draft()
def test_base_user_draft_other_employee_leave_and_is_leave_manager_id(self):
"""
Should not be able to draft the leave of someone else
even when being the leave manager id for this person
whatever the holiday_status_id
"""
self.employee_hruser.write({'leave_manager_id': self.user_employee.id})
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
with self.assertRaises(UserError):
leave.with_user(self.user_employee.id).action_draft()
def test_base_user_draft_self_and_is_leave_manager_id(self):
"""
Should be able to draft his own leave
even when being leave manager id
whatever the holiday_status_id
"""
self.employee_emp.write({'leave_manager_id': self.user_employee.id})
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_employee.id).action_draft()
def test_base_user_draft_refused_leave(self):
"""
Should not be able to draft a refused leave
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.action_refuse()
with self.assertRaises(UserError):
leave.with_user(self.user_employee.id).action_draft()
def test_base_user_draft_current_leave(self):
"""
Should not be able to draft a passed leave
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=-20 + i), 1, values)
with self.assertRaises(UserError):
leave.with_user(self.user_employee.id).action_draft()
def test_holiday_user_draft_his_leave(self):
"""
Should be able to draft his own leave
whatever the holiday_status_id
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_hruser.id).action_draft()
def test_holiday_user_draft_other_employee_leave(self):
"""
Should not be able to draft other employee leave
whatever the holiday_status_id
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
with self.assertRaises(UserError):
leave.with_user(self.user_hruser.id).action_draft()
def test_holiday_user_draft_other_employee_leave_and_is_leave_manager_id(self):
"""
Should not be able to draft other employee leave
even if he is the leave manager id
whatever the holiday_status_id
"""
self.employee_emp.write({'leave_manager_id': self.user_hruser.id})
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
with self.assertRaises(UserError):
leave.with_user(self.user_hruser.id).action_draft()
def test_holiday_user_draft_self_and_is_manager_id(self):
"""
Should be able to draft his own leave
even if he is leave manager id
whatever the holiday_status_id
"""
self.employee_hruser.write({'leave_manager_id': self.user_hruser.id})
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_hruser.id).action_draft()
def test_holiday_user_draft_refused_leave(self):
"""
Should not be able to draft a refused leave
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.action_refuse()
with self.assertRaises(UserError):
leave.with_user(self.user_hruser.id).action_draft()
def test_holiday_user_draft_current_leave(self):
"""
Should not be able to draft a passed leave
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=-20 + i), 1, values)
with self.assertRaises(UserError):
leave.with_user(self.user_hruser.id).action_draft()
def test_holiday_manager_draft_his_leave(self):
"""
The holiday manager should be able to do everything
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hrmanager.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_hrmanager.id).action_draft()
def test_holiday_manager_draft_other_employee_leave(self):
"""
The holiday manager should be able to do everything
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_hrmanager.id).action_draft()
def test_holiday_manager_draft_other_employee_leave_and_is_leave_manager_id(self):
"""
The holiday manager should be able to do everything
"""
self.employee_hruser.write({'leave_manager_id': self.user_hrmanager.id})
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_hrmanager.id).action_draft()
def test_holiday_manager_draft_self_and_is_manager_id(self):
"""
The holiday manager should be able to do everything
"""
self.employee_hrmanager.write({'leave_manager_id': self.user_hrmanager.id})
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hrmanager.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.with_user(self.user_hrmanager.id).action_draft()
def test_holiday_manager_draft_refused_leave(self):
"""
The holiday manager should be able to do everything
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values)
leave.action_refuse()
leave.with_user(self.user_hrmanager.id).action_draft()
def test_holiday_manager_draft_current_leave(self):
"""
The holiday manager should be able to do everything
"""
for i, status in enumerate(self.draft_status):
values = {
'name': 'Random Leave',
'employee_id': self.employee_hruser.id,
'holiday_status_id': status.id,
}
leave = self.request_leave(1, datetime.today() + relativedelta(days=-20 + i), 1, values)
leave.with_user(self.user_hrmanager.id).action_draft()
@tests.tagged('access_rights', 'access_rights_create')
class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon):
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -390,7 +91,7 @@ class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon):
'employee_id': self.employee_emp_id,
'holiday_status_id': self.leave_type.id,
}
self.request_leave(self.user_employee_id, datetime.today() + relativedelta(days=5), 1, values)
self.request_leave(self.user_employee_id, date.today() + relativedelta(days=5), 1, values)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_base_user_create_other(self):
@ -401,21 +102,9 @@ class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon):
'holiday_status_id': self.leave_type.id,
}
with self.assertRaises(AccessError):
self.request_leave(self.user_employee_id, datetime.today() + relativedelta(days=5), 1, values)
self.request_leave(self.user_employee_id, date.today() + relativedelta(days=5), 1, values)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_base_user_create_batch(self):
""" A simple user cannot create a leave in bacth mode (by company, by department, by tag)"""
values = {
'name': 'Hol10',
'holiday_status_id': self.leave_type.id,
'holiday_type': 'company',
'mode_company_id': 1,
}
with self.assertRaises(AccessError):
self.request_leave(self.user_employee_id, datetime.today() + relativedelta(days=5), 1, values)
# hr_holidays.group_hr_holidays_user
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -426,7 +115,7 @@ class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon):
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.leave_type.id,
}
self.request_leave(self.user_hruser_id, datetime.today() + relativedelta(days=5), 1, values)
self.request_leave(self.user_hruser_id, date.today() + relativedelta(days=5), 1, values)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_holidays_user_create_other(self):
@ -436,7 +125,7 @@ class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon):
'employee_id': self.employee_emp_id,
'holiday_status_id': self.leave_type.id,
}
self.request_leave(self.user_hruser_id, datetime.today() + relativedelta(days=5), 1, values)
self.request_leave(self.user_hruser_id, date.today() + relativedelta(days=5), 1, values)
# hr_holidays.group_hr_holidays_manager
@ -448,7 +137,7 @@ class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon):
'employee_id': self.employee_hrmanager_id,
'holiday_status_id': self.leave_type.id,
}
self.request_leave(self.user_hrmanager_id, datetime.today() + relativedelta(days=5), 1, values)
self.request_leave(self.user_hrmanager_id, date.today() + relativedelta(days=5), 1, values)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_holidays_manager_create_other(self):
@ -458,18 +147,7 @@ class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon):
'employee_id': self.employee_emp_id,
'holiday_status_id': self.leave_type.id,
}
self.request_leave(self.user_hrmanager_id, datetime.today() + relativedelta(days=5), 1, values)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_holidays_manager_create_batch(self):
""" A holidays manager can create a leave in bacth mode (by company, by department, by tag)"""
values = {
'name': 'Hol10',
'holiday_status_id': self.leave_type.id,
'holiday_type': 'company',
'mode_company_id': 1,
}
self.request_leave(self.user_hrmanager_id, datetime.today() + relativedelta(days=5), 1, values)
self.request_leave(self.user_hrmanager_id, date.today() + relativedelta(days=5), 1, values)
@tests.tagged('access_rights', 'access_rights_read')
@ -484,11 +162,10 @@ class TestAccessRightsRead(TestHrHolidaysAccessRightsCommon):
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days': 1,
'request_date_from': date.today(),
'request_date_to': date.today() + relativedelta(days=1),
})
with self.assertRaises(AccessError), self.cr.savepoint():
with self.assertRaises(AccessError):
res = other_leave.with_user(self.user_employee_id).read(['number_of_days', 'state', 'name'])
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -499,11 +176,10 @@ class TestAccessRightsRead(TestHrHolidaysAccessRightsCommon):
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days': 1,
'request_date_from': date.today(),
'request_date_to': date.today() + relativedelta(days=1),
})
with self.assertRaises(AccessError), self.cr.savepoint():
with self.assertRaises(AccessError):
other_leave.invalidate_model(['name'])
name = other_leave.with_user(self.user_employee_id).name
@ -531,9 +207,8 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days': 1,
'request_date_from': date.today(),
'request_date_to': date.today() + relativedelta(days=1),
})
with self.assertRaises(AccessError):
other_leave.with_user(self.user_employee_id).write({'name': 'Crocodile Dundee is my man'})
@ -547,9 +222,8 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
'name': 'Hol10',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.leave_type.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
'request_date_from': (date.today() - relativedelta(days=1)),
'request_date_to': date.today(),
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -598,13 +272,13 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_hr_to_validate_by_manager(self):
""" Manager validate its own leaves """
leave_start = date_utils.start_of(date.today() + relativedelta(days=15), 'week')
manager_leave = self.env['hr.leave'].with_user(self.user_hrmanager_id).create({
'name': 'Hol manager',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_hrmanager_id,
'date_from': (datetime.today() + relativedelta(days=15)),
'date_to': (datetime.today() + relativedelta(days=16)),
'number_of_days': 1,
'request_date_from': leave_start,
'request_date_to': leave_start + relativedelta(days=1),
})
self.assertEqual(manager_leave.state, 'confirm')
manager_leave.action_approve()
@ -626,8 +300,8 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
'holiday_status_id': self.leave_type.id,
'state': 'confirm',
}
hr_leave = self.request_leave(self.user_hruser_id, datetime.now() + relativedelta(days=2), 1, values)
with self.assertRaises(AccessError):
hr_leave = self.request_leave(self.user_hruser_id, date_utils.start_of(date.today() + relativedelta(days=7), 'week'), 1, values)
with self.assertRaises(UserError):
hr_leave.with_user(self.user_employee_id).action_approve()
self.employee_hruser.write({'leave_manager_id': self.user_employee_id})
hr_leave.with_user(self.user_employee_id).action_approve()
@ -644,7 +318,7 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
'holiday_status_id': self.leave_type.id,
'state': 'confirm',
}
hr_leave = self.request_leave(self.user_hruser_id, datetime.now() + relativedelta(days=2), 1, values)
hr_leave = self.request_leave(self.user_hruser_id, date_utils.start_of(date.today() + relativedelta(days=7), 'week'), 1, values)
hr_leave.with_user(self.user_hruser_id).action_approve()
# ----------------------------------------
@ -661,18 +335,18 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
'state': 'confirm',
}
self.employee_hrmanager.leave_manager_id = self.env['res.users'].browse(1)
hr_leave = self.request_leave(self.user_hruser_id, datetime.now() + relativedelta(days=6), 1, values)
leave_date = date_utils.start_of(date.today() + relativedelta(days=7), 'week')
hr_leave = self.request_leave(self.user_hruser_id, leave_date, 1, values)
with self.assertRaises(AccessError):
with self.assertRaises(UserError):
hr_leave.with_user(self.user_employee_id).action_approve()
self.employee_hrmanager.leave_manager_id = self.user_responsible
hr_leave.with_user(self.user_responsible_id).action_approve()
self.employee_hrmanager.leave_manager_id = self.user_hruser
with self.assertRaises(UserError):
hr_leave.with_user(self.user_employee_id).action_approve()
hr_leave.with_user(self.user_hruser_id).action_approve()
with self.assertRaises(AccessError):
hr_leave.with_user(self.user_employee_id).action_validate()
hr_leave.with_user(self.user_hruser_id).action_validate()
# hr_holidays.group_hr_holidays_manager
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -684,9 +358,9 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
'holiday_status_id': self.leave_type.id,
'state': 'confirm',
}
hr_leave = self.request_leave(self.user_hrmanager_id, datetime.now() + relativedelta(days=4), 1, values).with_user(self.user_hrmanager_id)
leave_start = date_utils.start_of(date.today() + relativedelta(days=15), 'week')
hr_leave = self.request_leave(self.user_hrmanager_id, leave_start, 1, values).with_user(self.user_hrmanager_id)
hr_leave.action_approve()
hr_leave.action_validate()
# ----------------------------------------
# State = Refuse
@ -702,6 +376,32 @@ class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon):
# TODO Can refuse
# hr_holidays.group_hr_holidays_responsible
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@freeze_time('2026-01-23 10:00:00')
def test_holiday_responsible_refuse_leave(self):
"""
The holiday responsible should be able to accept and refuse correct type leaves of users they are responsible for
"""
respo_user = self.user_responsible
self.employee_emp.leave_manager_id = respo_user
for validatation_type in ['manager', 'both']:
self.leave_type.write({'leave_validation_type': validatation_type})
values = {
'name': 'Random Time Off',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'state': 'confirm',
}
leave = self.request_leave(self.user_employee, date.today(), 1, values)
leave.with_user(respo_user).action_refuse()
# Check that refusing after first approval also works
leave = self.request_leave(self.user_employee, date.today(), 1, values)
leave.with_user(respo_user).action_approve()
leave.with_user(respo_user).action_refuse()
# ----------------------------------------
# State = Cancel
# ----------------------------------------
@ -722,54 +422,40 @@ class TestAccessRightsUnlink(TestHrHolidaysAccessRightsCommon):
# base.group_user
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_unlink_draft_by_user(self):
""" A simple user may delete its leave in draft state in the future"""
values = {
'name': 'Random Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'state': 'draft',
}
leave = self.request_leave(self.user_employee_id, datetime.now() + relativedelta(days=5), 1, values)
leave.with_user(self.user_employee.id).unlink()
def test_leave_unlink_confirm_by_user(self):
""" A simple user may delete its leave in confirm state in the future"""
values = {
'name': 'Random Leave',
'name': 'Random Time Off',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'state': 'confirm',
}
leave = self.request_leave(self.user_employee_id, datetime.now() + relativedelta(days=5), 1, values)
leave = self.request_leave(self.user_employee_id, date.today() + relativedelta(days=6), 1, values)
leave.with_user(self.user_employee.id).unlink()
def test_leave_unlink_confirm_in_past_by_user(self):
""" A simple user cannot delete past leaves, but can delete today's leave"""
""" A simple user cannot delete its leave in the past"""
values = {
'name': 'Random Leave',
'name': 'Random Time Off',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'state': 'confirm',
}
with freeze_time('2024-5-23 13:00:00'):
other_leave = self.request_leave(self.user_employee_id, datetime.now() + relativedelta(hours=-4), 1, values)
other_leave.with_user(self.user_employee.id).unlink()
leave = self.request_leave(self.user_employee_id, datetime.now() + relativedelta(days=-4), 1, values)
with self.assertRaises(UserError), self.cr.savepoint():
leave.with_user(self.user_employee.id).unlink()
leave = self.request_leave(self.user_employee_id, date.today() + relativedelta(days=-4), 1, values)
with self.assertRaises(UserError):
leave.with_user(self.user_employee.id).unlink()
def test_leave_unlink_validate_by_user(self):
""" A simple user cannot delete its leave in validate state"""
values = {
'name': 'Random Leave',
'name': 'Random Time Off',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
}
leave = self.request_leave(self.user_employee_id, datetime.now() + relativedelta(days=5), 1, values)
leave = self.request_leave(self.user_employee_id, date.today() + relativedelta(days=6), 1, values)
leave.with_user(self.user_hrmanager_id).write({'state': 'validate'})
with self.assertRaises(UserError), self.cr.savepoint():
with self.assertRaises(UserError):
leave.with_user(self.user_employee.id).unlink()
class TestMultiCompany(TestHrHolidaysCommon):
@ -784,8 +470,9 @@ class TestMultiCompany(TestHrHolidaysCommon):
'name': 'Unlimited - Company New',
'company_id': cls.new_company.id,
'leave_validation_type': 'hr',
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.employee_emp.company_id = cls.new_company
cls.rd_dept.manager_id = False
cls.hr_dept.manager_id = False
@ -794,9 +481,8 @@ class TestMultiCompany(TestHrHolidaysCommon):
'holiday_status_id': cls.leave_type.id,
'department_id': cls.employee_emp.department_id.id,
'employee_id': cls.employee_emp.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days': 1,
'request_date_from': date.today(),
'request_date_to': date.today() + relativedelta(days=1),
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -806,7 +492,7 @@ class TestMultiCompany(TestHrHolidaysCommon):
with self.assertRaises(AccessError):
employee_leave.name
with self.assertRaises(AccessError):
with self.assertRaises(UserError):
employee_leave.action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -816,7 +502,7 @@ class TestMultiCompany(TestHrHolidaysCommon):
with self.assertRaises(AccessError):
employee_leave_hruser.name
with self.assertRaises(AccessError):
with self.assertRaises(UserError):
employee_leave_hruser.action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@ -826,11 +512,12 @@ class TestMultiCompany(TestHrHolidaysCommon):
with self.assertRaises(AccessError):
employee_leave_hrmanager.name
with self.assertRaises(AccessError):
with self.assertRaises(UserError):
employee_leave_hrmanager.action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_no_company_user(self):
self.employee_emp.company_id = self.user_employee.company_id
self.leave_type.write({'company_id': False})
employee_leave = self.employee_leave.with_user(self.user_employee)
@ -839,8 +526,10 @@ class TestMultiCompany(TestHrHolidaysCommon):
employee_leave.action_approve()
self.assertEqual(employee_leave.state, 'confirm')
@unittest.skip
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_no_company_officer(self):
self.employee_emp.company_id = self.user_employee.company_id
self.leave_type.write({'company_id': False})
employee_leave_hruser = self.employee_leave.with_user(self.user_hruser)
@ -848,8 +537,10 @@ class TestMultiCompany(TestHrHolidaysCommon):
employee_leave_hruser.action_approve()
self.assertEqual(employee_leave_hruser.state, 'validate')
@unittest.skip
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_no_company_manager(self):
self.employee_emp.company_id = self.user_employee.company_id
self.leave_type.write({'company_id': False})
employee_leave_hrmanager = self.employee_leave.with_user(self.user_hrmanager)

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import tests
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.exceptions import AccessError, UserError
import time
@ -17,25 +16,25 @@ class TestAllocationRights(TestHrHolidaysCommon):
cls.employee_emp.parent_id = False
cls.employee_emp.leave_manager_id = False
cls.lt_no_allocation = cls.env['hr.leave.type'].create({
cls.lt_validation_hr = cls.env['hr.leave.type'].create({
'name': 'Validation = HR',
'allocation_validation_type': 'officer',
'requires_allocation': 'no',
'employee_requests': 'yes',
'allocation_validation_type': 'hr',
'requires_allocation': True,
'employee_requests': True,
})
cls.lt_validation_manager = cls.env['hr.leave.type'].create({
'name': 'Validation = manager',
'allocation_validation_type': 'officer',
'requires_allocation': 'yes',
'employee_requests': 'yes',
'allocation_validation_type': 'manager',
'requires_allocation': True,
'employee_requests': True,
})
cls.lt_allocation_no_validation = cls.env['hr.leave.type'].create({
'name': 'Validation = user',
'allocation_validation_type': 'no',
'requires_allocation': 'yes',
'employee_requests': 'yes',
'allocation_validation_type': 'no_validation',
'requires_allocation': True,
'employee_requests': True,
})
def request_allocation(self, user, values={}):
@ -58,7 +57,7 @@ class TestAccessRightsSimpleUser(TestAllocationRights):
}
allocation = self.request_allocation(self.user_employee.id, values)
with self.assertRaises(UserError):
allocation.action_validate()
allocation.action_approve()
def test_simple_user_request_allocation_no_validation(self):
""" A simple user can request and automatically validate an allocation with no validation """
@ -85,11 +84,7 @@ class TestAccessRightsSimpleUser(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_employee.id, values)
self.assertEqual(allocation.state, 'draft')
allocation.action_confirm()
self.assertEqual(allocation.state, 'confirm', "It should be confirmed")
allocation.action_draft()
self.assertEqual(allocation.state, 'draft', "It should have been reset to draft")
self.assertEqual(allocation.state, 'confirm', "The allocation should be in 'confirm' state")
class TestAccessRightsEmployeeManager(TestAllocationRights):
@ -118,8 +113,7 @@ class TestAccessRightsEmployeeManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_employee.id, values)
allocation.action_confirm()
allocation.action_validate()
allocation.action_approve()
self.assertEqual(allocation.state, 'validate', "The allocation should be validated")
def test_manager_refuse_request_allocation(self):
@ -129,20 +123,9 @@ class TestAccessRightsEmployeeManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_employee.id, values)
allocation.action_confirm()
allocation.action_refuse()
self.assertEqual(allocation.state, 'refuse', "The allocation should be validated")
def test_manager_batch_allocation(self):
""" A manager cannot create batch allocation """
values = {
'holiday_status_id': self.lt_validation_manager.id,
'holiday_type': 'company',
'mode_company_id': self.user_employee.company_id.id,
}
with self.assertRaises(AccessError):
self.request_allocation(self.user_employee.id, values)
def test_manager_approve_own(self):
""" A manager cannot approve his own allocation """
values = {
@ -151,31 +134,21 @@ class TestAccessRightsEmployeeManager(TestAllocationRights):
}
allocation = self.request_allocation(self.user_employee.id, values)
with self.assertRaises(UserError):
allocation.action_validate()
allocation.action_approve()
class TestAccessRightsHolidayUser(TestAllocationRights):
def test_holiday_user_request_allocation(self):
""" A holiday user can request and approve an allocation for any employee """
""" A holiday user can request and approve an allocation for any internal employee """
values = {
'employee_id': self.employee_emp.id,
'holiday_status_id': self.lt_validation_manager.id,
'holiday_status_id': self.lt_validation_hr.id,
}
allocation = self.request_allocation(self.user_hruser.id, values)
allocation.action_confirm()
allocation.action_validate()
allocation.action_approve()
self.assertEqual(allocation.state, 'validate', "It should have been validated")
def test_holiday_user_batch_allocation(self):
""" A holiday user cannot create a batch allocation """
values = {
'holiday_status_id': self.lt_validation_manager.id,
'holiday_type': 'company',
'mode_company_id': self.user_employee.company_id.id,
}
with self.assertRaises(AccessError):
self.request_allocation(self.user_hruser.id, values)
def test_holiday_user_cannot_approve_own(self):
""" A holiday user cannot approve his own allocation """
values = {
@ -183,9 +156,27 @@ class TestAccessRightsHolidayUser(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_hruser.id, values)
allocation.action_confirm()
with self.assertRaises(UserError):
allocation.action_validate()
allocation.action_approve()
def test_holiday_user_cannot_approve_external_company(self):
"""A holidy user can validate but not approve allocations for employees in external company"""
self.user_hruser.write({
'company_ids': [(6, 0, [self.company.id, self.external_company.id])],
})
values = {
'employee_id': self.employee_external.id,
'holiday_status_id': self.lt_validation_hr.id,
}
allocation = self.request_allocation(self.user_hruser.id, values).with_company(self.external_company.id)
self.assertEqual(allocation.can_validate, True)
self.assertEqual(allocation.can_approve, False)
self.assertEqual(allocation.state, 'confirm')
allocation.action_approve()
self.assertEqual(allocation.state, 'validate')
class TestAccessRightsHolidayManager(TestAllocationRights):
@ -197,8 +188,7 @@ class TestAccessRightsHolidayManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_hrmanager.id, values)
allocation.action_confirm()
allocation.action_validate()
allocation.action_approve()
self.assertEqual(allocation.state, 'validate', "It should have been validated")
def test_holiday_manager_refuse_validated(self):
@ -208,8 +198,7 @@ class TestAccessRightsHolidayManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_hrmanager.id, values)
allocation.action_confirm()
allocation.action_validate()
allocation.action_approve()
self.assertEqual(allocation.state, 'validate', "It should have been validated")
allocation.action_refuse()
self.assertEqual(allocation.state, 'refuse', "It should have been refused")

View file

@ -0,0 +1,656 @@
from datetime import date, timedelta
from freezegun import freeze_time
from odoo.exceptions import ValidationError
from odoo.fields import Date, Datetime
from odoo.tests import Form, tagged, users
from odoo.tools import format_date
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('allocation')
class TestAllocations(TestHrHolidaysCommon):
@classmethod
def setUpClass(cls):
super(TestAllocations, cls).setUpClass()
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Time Off with no validation for approval',
'time_type': 'leave',
'requires_allocation': True,
'allocation_validation_type': 'no_validation',
})
cls.department = cls.env['hr.department'].create({
'name': 'Test Department',
})
cls.category_tag = cls.env['hr.employee.category'].create({
'name': 'Test category'
})
cls.employee = cls.env['hr.employee'].create({
'name': 'My Employee',
'company_id': cls.company.id,
'department_id': cls.department.id,
'category_ids': [(4, cls.category_tag.id)],
})
cls.leave_type_paid = cls.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'requires_allocation': True,
'allocation_validation_type': 'no_validation',
})
cls.calendar_35h = cls.env['resource.calendar'].create({
'name': 'Calendar - 35H',
'company_id': cls.company.id,
'attendance_ids': [(5, 0, 0),
(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': 16, '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': 16, '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': 16, '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': 16, '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': 16, 'day_period': 'afternoon'})
]
})
def test_allocation_whole_company(self):
company_allocation = self.env['hr.leave.allocation.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'allocation_mode': 'company',
'company_id': self.company.id,
'holiday_status_id': self.leave_type.id,
'duration': 2,
'allocation_type': 'regular',
})
company_allocation.action_generate_allocations()
num_of_allocations = self.env['hr.leave.allocation'].search_count([('employee_id', '=', self.employee.id)])
self.assertEqual(num_of_allocations, 1)
def test_allocation_multi_employee(self):
employee_allocation = self.env['hr.leave.allocation.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'allocation_mode': 'employee',
'employee_ids': [(4, self.employee.id), (4, self.employee_emp.id)],
'holiday_status_id': self.leave_type.id,
'duration': 2,
'allocation_type': 'regular',
})
employee_allocation.action_generate_allocations()
num_of_allocations = self.env['hr.leave.allocation'].search_count([('employee_id', '=', self.employee.id)])
self.assertEqual(num_of_allocations, 1)
def test_allocation_department(self):
department_allocation = self.env['hr.leave.allocation.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'allocation_mode': 'department',
'department_id': self.department.id,
'holiday_status_id': self.leave_type.id,
'duration': 2,
'allocation_type': 'regular',
})
department_allocation.action_generate_allocations()
num_of_allocations = self.env['hr.leave.allocation'].search_count([('employee_id', '=', self.employee.id)])
self.assertEqual(num_of_allocations, 1)
@users('Titus')
def test_create_group_allocation_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,
},
])
allocation_wizard = self.env['hr.leave.allocation.generate.multi.wizard'].create({
'holiday_status_id': self.leave_type.id,
'date_from': date(2019, 5, 6),
'date_to': date(2019, 5, 6),
'employee_ids': (employee_1 + employee_2).ids,
'duration': 2,
'allocation_type': 'regular',
})
allocation_wizard.action_generate_allocations()
def test_allocation_category(self):
category_allocation = self.env['hr.leave.allocation.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'allocation_mode': 'category',
'category_id': self.category_tag.id,
'holiday_status_id': self.leave_type.id,
'duration': 2,
'allocation_type': 'regular',
})
category_allocation.action_generate_allocations()
num_of_allocations = self.env['hr.leave.allocation'].search_count([('employee_id', '=', self.employee.id)])
self.assertEqual(num_of_allocations, 1)
def test_allocation_request_day(self):
self.leave_type.write({
'name': 'Custom Time Off Test',
'allocation_validation_type': 'hr'
})
employee_allocation = self.env['hr.leave.allocation'].create({
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'allocation_type': 'regular',
})
with Form(employee_allocation.with_context(is_employee_allocation=True), 'hr_holidays.hr_leave_allocation_view_form_dashboard') as allocation:
allocation.number_of_days_display = 10
employee_allocation = allocation.save()
self.assertEqual(employee_allocation.name, "Custom Time Off Test (10.0 day(s))")
def test_allocation_request_half_days(self):
self.leave_type.write({
'name': 'Custom Time Off Test',
'allocation_validation_type': 'hr'
})
employee_allocation = self.env['hr.leave.allocation'].create({
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'allocation_type': 'regular',
'type_request_unit': 'half_day',
})
with Form(employee_allocation.with_context(is_employee_allocation=True), 'hr_holidays.hr_leave_allocation_view_form_dashboard') as allocation:
allocation.number_of_days_display = 10
employee_allocation = allocation.save()
self.assertEqual(employee_allocation.name, "Custom Time Off Test (10.0 day(s))")
def change_allocation_type_day(self):
self.leave_type.write({
'name': 'Custom Time Off Test',
'allocation_validation_type': 'hr'
})
employee_allocation = self.env['hr.leave.allocation'].create({
'holiday_type': 'employee',
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'allocation_type': 'regular',
})
with Form(employee_allocation.with_context(is_employee_allocation=True), 'hr_holidays.hr_leave_allocation_view_form_dashboard') as allocation:
allocation.allocation_type = 'extra'
allocation.allocation_type = 'regular'
employee_allocation = allocation.save()
self.assertEqual(employee_allocation.number_of_days, 1.0)
def test_allocation_type_hours_with_resource_calendar(self):
self.leave_type.request_unit = 'hour'
self.employee.resource_calendar_id = self.calendar_35h
hour_type_allocation = self.env['hr.leave.allocation.generate.multi.wizard'].create({
'name': 'Hours Allocation',
'allocation_mode': 'employee',
'employee_ids': [(4, self.employee.id), (4, self.employee_emp.id)],
'holiday_status_id': self.leave_type.id,
'duration': 10,
'allocation_type': 'regular',
})
self.assertEqual(self.employee.resource_calendar_id.hours_per_day, 7.0)
self.assertEqual(self.employee_emp.resource_calendar_id.hours_per_day, 8.0)
hour_type_allocation.action_generate_allocations()
# Find allocations created for individual employees
employee_allocation = self.env['hr.leave.allocation'].search([
('employee_id', '=', self.employee.id),
])
employee_emp_allocation = self.env['hr.leave.allocation'].search([
('employee_id', '=', self.employee_emp.id),
])
self.assertEqual(employee_allocation.number_of_hours_display, 10)
self.assertEqual(employee_emp_allocation.number_of_hours_display, 10)
def change_allocation_type_hours(self):
self.leave_type.write({
'name': 'Custom Time Off Test',
'allocation_validation_type': 'hr'
})
employee_allocation = self.env['hr.leave.allocation'].create({
'holiday_type': 'employee',
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'allocation_type': 'regular',
'type_request_unit': 'hour',
})
with Form(employee_allocation.with_context(is_employee_allocation=True), 'hr_holidays.hr_leave_allocation_view_form_dashboard') as allocation:
allocation.allocation_type = 'extra'
allocation.allocation_type = 'regular'
employee_allocation = allocation.save()
self.assertEqual(employee_allocation.number_of_days, 1.0)
def test_allowed_change_allocation(self):
allocation = self.env['hr.leave.allocation'].create({
'name': 'Initial Allocation',
'holiday_status_id': self.leave_type_paid.id,
'number_of_days': 20,
'employee_id': self.employee.id,
'date_from': date(2024, 1, 1),
})
allocation.action_approve()
leave_request = self.env['hr.leave'].create({
'name': 'Leave Request',
'holiday_status_id': self.leave_type_paid.id,
'request_date_from': date(2024, 1, 5),
'request_date_to': date(2024, 1, 10),
'employee_id': self.employee.id,
})
leave_request.action_approve()
allocation.write({'number_of_days_display': 14, 'number_of_days': 14})
self.assertEqual(allocation.number_of_days_display, 14)
with self.assertRaises(ValidationError):
allocation.write({'number_of_days_display': 2, 'number_of_days': 2})
def test_disallowed_change_allocation_with_overlapping_allocations(self):
# Creating the first allocation
allocation_one = self.env['hr.leave.allocation'].create({
'name': 'First Allocation',
'holiday_status_id': self.leave_type_paid.id,
'number_of_days': 5,
'employee_id': self.employee.id,
'date_from': date(2024, 1, 1),
'date_to': date(2024, 1, 30),
})
allocation_one.action_approve()
# Creating the second overlapping allocation
allocation_two = self.env['hr.leave.allocation'].create({
'name': 'Second Half Allocation',
'holiday_status_id': self.leave_type_paid.id,
'number_of_days': 5,
'employee_id': self.employee.id,
'date_from': date(2024, 1, 20),
'date_to': date(2024, 2, 20),
})
allocation_two.action_approve()
# Creating a leave request consuming days from both allocations
leave_request = self.env['hr.leave'].create({
'name': 'Leave Request Spanning Allocations',
'holiday_status_id': self.leave_type_paid.id,
'request_date_from': date(2024, 1, 25),
'request_date_to': date(2024, 2, 5),
'employee_id': self.employee.id,
})
leave_request.action_approve()
with self.assertRaises(ValidationError):
allocation_one.write({'number_of_days_display': 2, 'number_of_days': 2})
allocation_one.write({'number_of_days_display': 3, 'number_of_days': 3})
@users('admin')
@freeze_time('2024-03-25')
def test_allocation_dropdown_after_period(self):
"""
Test when having two allocations of the same type with different
time range and submitting a request will the allocations be
shown correctly in the dropdown menu or not
:return:
"""
leave_type = self.env.ref('hr_holidays.leave_type_compensatory_days')
allocation = self.env['hr.leave.allocation'].sudo().create({
'name': 'Alloc',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'number_of_days': 3,
'allocation_type': 'regular',
'date_from': date(2024, 1, 1),
'date_to': date(2024, 4, 30)
})
allocation.action_approve()
second_allocation = self.env['hr.leave.allocation'].sudo().create({
'name': 'Alloc2',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'number_of_days': 9,
'allocation_type': 'regular',
'date_from': date(2024, 5, 1),
'date_to': date(2024, 12, 31)
})
second_allocation.action_approve()
# _compute_leaves depends on the context that is getting cleared
self.env['hr.leave.type'].invalidate_model(['max_leaves', 'leaves_taken', 'virtual_remaining_leaves'])
result = self.env['hr.leave.type'].with_context(
employee_id=self.employee.id,
leave_date_from='2024-08-18 06:00:00', # for _compute_leaves
default_date_from='2024-08-18 06:00:00',
default_date_to='2024-08-18 15:00:00'
).name_search(domain=[['id', '=', leave_type.id]])
self.assertEqual(result[0][1], 'Compensatory Days (9 remaining out of 9 days)')
def test_allocation_hourly_leave_type(self):
"""
Make sure that the number of hours is correctly set on the allocation for an hourly leave type
for an employee who works some other schedule than the default 8 hours per day.
"""
employee = self.env['hr.employee'].create({
'name': 'My Employee',
'company_id': self.company.id,
'resource_calendar_id': self.calendar_35h.id,
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Hourly Leave Type',
'time_type': 'leave',
'requires_allocation': True,
'allocation_validation_type': 'no_validation',
'request_unit': 'hour',
})
with Form(self.env['hr.leave.allocation'].with_user(self.user_hrmanager)) as allocation_form:
allocation_form.allocation_type = 'regular'
allocation_form.employee_id = employee
allocation_form.holiday_status_id = leave_type
allocation_form.number_of_hours_display = 10
allocation = allocation_form.save()
self.assertEqual(allocation.number_of_hours_display, 10.0)
def test_automatic_allocation_type(self):
"""
Make sure that an allocation with an accrual plan imported will automatically set the allocation_type to 'accrual'
"""
leave_type = self.env['hr.leave.type'].create({
'name': 'Hourly Leave Type',
'time_type': 'leave',
'requires_allocation': 'yes',
'allocation_validation_type': 'no_validation',
'request_unit': 'hour',
})
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
})
allocation = self.env['hr.leave.allocation'].create({
'name': 'Alloc with accrual plan',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'accrual_plan_id': accrual_plan.id,
})
self.assertEqual(allocation.allocation_type, 'accrual')
allocation.update({
'accrual_plan_id': False,
})
self.assertEqual(allocation.allocation_type, 'regular')
def test_create_allocation_from_company_with_no_employee_for_current_user(self):
"""
This test makes sure that the allocation can be created if the current company doesn't have an employee
linked to the loggedIn user.
"""
self.user_hrmanager.employee_id = False
allocation_form = Form(self.env['hr.leave.allocation'].with_user(self.user_hrmanager))
self.assertFalse(allocation_form.employee_id)
allocation_form.employee_id = self.employee
allocation_form.holiday_status_id = self.leave_type
allocation = allocation_form.save()
self.assertTrue(allocation)
def test_hr_leave_allocation_balance(self):
"""
This test makes sure that the time off balance showed on the time off management kanban card is correct
"""
leave_type = self.env.ref('hr_holidays.leave_type_compensatory_days')
invalid_allocation = self.env['hr.leave.allocation'].sudo().create({
'name': 'Alloc',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'number_of_days': 5,
'allocation_type': 'regular',
'date_from': date(2024, 1, 1),
'date_to': date(2024, 4, 30)
})
invalid_allocation.action_approve()
first_valid_allocation = self.env['hr.leave.allocation'].sudo().create({
'name': 'Alloc',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'number_of_days': 10,
'allocation_type': 'regular',
'date_from': date(2024, 1, 1),
'date_to': False
})
first_valid_allocation.action_approve()
second_valid_allocation = self.env['hr.leave.allocation'].sudo().create({
'name': 'Alloc',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'number_of_days': 12,
'allocation_type': 'regular',
'date_from': date(2025, 1, 1),
'date_to': date.today()
})
second_valid_allocation.action_approve()
leave = self.env['hr.leave'].create({
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': date(2025, 1, 1),
'request_date_to': date(2025, 1, 10)
})
leave._action_validate()
self.assertEqual(leave.max_leaves, 22)
self.assertEqual(leave.virtual_remaining_leaves, 14)
def test_allocation_request_with_date_from(self):
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager)
allocation_view = 'hr_holidays.hr_leave_allocation_view_form'
with self.assertRaises(AssertionError):
with Form(allocation, allocation_view) as allocation_form:
allocation_form.holiday_status_id = self.leave_type
allocation_form.date_from = False
with Form(allocation, allocation_view) as allocation_form:
date_from = Date.today()
allocation_form.holiday_status_id = self.leave_type
allocation_form.date_from = date_from
self.assertEqual(allocation_form.date_from, date_from)
self.assertEqual(
allocation_form.name_validity,
"%(allocation_name)s (from %(date_from)s to No Limit)" % {
'allocation_name': allocation_form.name,
'date_from': format_date(allocation.env, Date.context_today(allocation, Datetime.to_datetime(allocation_form.date_from))),
},
"The name_validity field was not set correctly."
)
def test_leave_allocation_by_removing_employee(self):
"""
Test that creating a leave allocation and then removing the employee will
not raise an error
"""
self.leave_type.request_unit = "hour"
with self.assertRaises(AssertionError): # AssertionError raised by Form as employee is required
with Form(self.env['hr.leave.allocation']) as allocation_form:
allocation_form.allocation_type = "regular"
allocation_form.holiday_status_id = self.leave_type
allocation_form.number_of_hours_display = 10
allocation_form.employee_id = self.env["hr.employee"]
allocation_form.save()
def test_employee_holidays_archived_display(self):
admin_user = self.env.ref('base.user_admin')
employee = self.env['hr.employee'].create({
'name': 'test_employee',
})
leave_type = self.env['hr.leave.type'].with_user(admin_user)
holidays_type_1 = leave_type.create({
'name': 'archived_holidays',
'allocation_validation_type': 'no_validation',
})
self.env['hr.leave.allocation'].create({
'name': 'archived_holidays_allocation',
'employee_id': employee.id,
'holiday_status_id': holidays_type_1.id,
'number_of_days': 10,
'state': 'confirm',
'date_from': '2022-01-01',
})
self.assertEqual(employee.allocation_display, '10')
holidays_type_1.active = False
employee._compute_allocation_remaining_display()
self.assertEqual(employee.allocation_display, '0')
def test_refuse_validated_allocation_with_leaves(self):
"""
Test that an allocation can be refused after being validated only if the existing leave's taken days can be
handled by the other allocations
"""
today = date.today()
start_of_week = today - timedelta(days=today.weekday())
leave_employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'user_id': self.env.uid,
})
def _create_allocation(days):
return self.env['hr.leave.allocation'].create({
'name': f'{days} days Allocation',
'holiday_status_id': self.leave_type_paid.id,
'number_of_days': days,
'employee_id': leave_employee.id,
'date_from': start_of_week,
})
allocation_5_days = _create_allocation(days=5)
allocation_5_days.action_approve()
self.assertEqual(allocation_5_days.state, 'validate')
# 4 Days leave - Can be only on the 5 days allocation
leave_request = self.env['hr.leave'].create({
'name': 'Leave Request',
'holiday_status_id': self.leave_type_paid.id,
'request_date_from': start_of_week,
'request_date_to': start_of_week + timedelta(days=3),
'employee_id': leave_employee.id,
})
leave_request.action_approve()
allocation_3_days = _create_allocation(days=3)
allocation_3_days.date_to = start_of_week + timedelta(days=5)
allocation_3_days.action_approve()
self.assertEqual(allocation_3_days.state, 'validate')
# Can't Refuse 5 days allocation
with self.assertRaises(ValidationError):
allocation_5_days.action_refuse()
self.assertEqual(allocation_5_days.state, 'validate')
# But can Refuse 3 days one
allocation_3_days.action_refuse()
self.assertEqual(allocation_3_days.state, 'refuse')
allocation_3_days.state = 'confirm'
allocation_3_days.action_approve()
self.assertEqual(allocation_3_days.state, 'validate')
# 2 Days leave - Both allocations can be refused / but not at the same time
leave_request.state = 'confirm'
leave_request.request_date_to = start_of_week + timedelta(days=1)
leave_request.action_approve()
allocation_5_days.action_refuse()
self.assertEqual(allocation_5_days.state, 'refuse')
with self.assertRaises(ValidationError):
allocation_3_days.action_refuse()
self.assertEqual(allocation_3_days.state, 'validate')
allocation_5_days.state = 'confirm'
allocation_5_days.action_approve()
self.assertEqual(allocation_5_days.state, 'validate')
allocation_3_days.action_refuse()
self.assertEqual(allocation_3_days.state, 'refuse')
def test_time_off_hours_start_date_attendance(self):
"""
When we set a date_from and/or a date_to on an attendance, it doesn't appear in global attendances anymore,
causing the hours of this attendance to not be taken into account. If all attendances have a date_from and/or
a date_to, the total hours_per_day will reach zero, which causes a division per zero when setting a time
off based on hours. This test makes sure that we don't divide ever by zero, even in that case.
"""
calendar = self.env['resource.calendar'].create({
'name': 'Standard Calendar',
'two_weeks_calendar': False,
})
self.env['resource.calendar.attendance'].create({
'name': 'Monday',
'calendar_id': calendar.id,
'dayofweek': '0', # Monday
'hour_from': 8,
'hour_to': 16,
})
self.leave_type.write({'request_unit': 'hour'})
with Form(self.env['hr.leave.allocation'].with_user(self.user_hrmanager)) as allocation_form:
allocation_form.allocation_type = 'regular'
allocation_form.employee_id = self.employee
allocation_form.holiday_status_id = self.leave_type
allocation_form.number_of_hours_display = 7.2
allocation = allocation_form.save()
self.assertEqual(allocation.duration_display, '7.2 hours')
@freeze_time('2024-03-25')
def test_allocation_count_date_previous_year(self):
"""Checks that the allocation count is calculated correctly when an employee has an allocation starting during
the prevous year, but which hasn't expired yet."""
self.env['hr.leave.allocation'].create({
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'allocation_type': 'regular',
'date_from': '2023-12-25'
})
self.assertEqual(1, self.leave_type.allocation_count)

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
from datetime import date, datetime
from odoo.tests.common import Form
from odoo.fields import Command
from odoo.tests import Form
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.exceptions import ValidationError
class TestAutomaticLeaveDates(TestHrHolidaysCommon):
@ -14,8 +13,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Automatic Test',
'time_type': 'leave',
'requires_allocation': 'no',
# Required for `request_unit_half` to be visible in the view
'requires_allocation': False,
'request_unit': 'half_day',
})
@ -30,12 +28,11 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form:
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = date(2019, 9, 2)
leave_form.request_date_to = date(2019, 9, 2)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
self.assertEqual(leave_form.number_of_days_display, 0)
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
leave = leave_form.record
self.assertEqual(leave.number_of_days, 0)
self.assertEqual(leave.number_of_hours, 0)
def test_single_attendance_on_morning_and_afternoon(self):
calendar = self.env['resource.calendar'].create({
@ -48,6 +45,13 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'day_period': 'morning',
'dayofweek': '0',
}),
(0, 0, {
'name': 'monday lunch',
'hour_from': 12,
'hour_to': 13,
'day_period': 'lunch',
'dayofweek': '0',
}),
(0, 0, {
'name': 'monday afternoon',
'hour_from': 13,
@ -64,16 +68,19 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = date(2019, 9, 2)
leave_form.request_date_to = date(2019, 9, 2)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, .5)
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, .5)
self.assertEqual(leave_form.record.number_of_hours, 4)
leave_form.request_date_from_period = 'pm'
leave_form.request_date_to_period = 'pm'
self.assertEqual(leave_form.number_of_days_display, .5)
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, .5)
self.assertEqual(leave_form.record.number_of_hours, 4)
def test_multiple_attendance_on_morning(self):
calendar = self.env['resource.calendar'].create({
@ -85,6 +92,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'hour_to': 10,
'day_period': 'morning',
'dayofweek': '0',
'duration_days': 0.25,
}),
(0, 0, {
'name': 'monday morning 2',
@ -92,6 +100,14 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'hour_to': 12.25,
'day_period': 'morning',
'dayofweek': '0',
'duration_days': 0.25,
}),
(0, 0, {
'name': 'monday lunch',
'hour_from': 12.25,
'hour_to': 13,
'day_period': 'lunch',
'dayofweek': '0',
}),
(0, 0, {
'name': 'monday afternoon',
@ -99,6 +115,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'hour_to': 17,
'day_period': 'afternoon',
'dayofweek': '0',
'duration_days': 0.5,
})]
})
employee = self.employee_emp
@ -108,16 +125,19 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = date(2019, 9, 2)
leave_form.request_date_to = date(2019, 9, 2)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, .5)
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, .5)
self.assertEqual(leave_form.record.number_of_hours, 4)
leave_form.request_date_from_period = 'pm'
leave_form.request_date_to_period = 'pm'
self.assertEqual(leave_form.number_of_days_display, .5)
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, .5)
self.assertEqual(leave_form.record.number_of_hours, 4)
def test_attendance_on_morning(self):
calendar = self.env['resource.calendar'].create({
@ -137,18 +157,160 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = date(2019, 9, 2)
leave_form.request_date_to = date(2019, 9, 2)
leave_form.request_unit_half = True
# Ask for morning
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, 0.5)
self.assertEqual(leave_form.number_of_hours_text, '8 Hours')
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 1)
self.assertEqual(leave_form.record.number_of_hours, 8)
# Ask for afternoon
leave_form.request_date_from_period = 'pm'
leave_form.request_date_to_period = 'pm'
self.assertEqual(leave_form.number_of_days_display, 0.5)
self.assertEqual(leave_form.number_of_hours_text, '8 Hours')
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 0)
self.assertEqual(leave_form.record.number_of_hours, 0)
def test_attendance_full_day(self):
calendar = self.env["resource.calendar"].create({
"name": "Full Days",
"attendance_ids": [
Command.clear(),
Command.create({
"name": "Monday",
"hour_from": 8,
"hour_to": 16,
"day_period": "full_day",
"dayofweek": "0",
}),
],
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
with Form(
self.env["hr.leave"].with_context(default_employee_id=employee.id)
) as leave_form:
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = date(2019, 9, 2) # Monday
leave_form.request_date_to = date(2019, 9, 2) # Monday
# Ask for morning
leave_form.request_date_from_period = "am"
leave_form.request_date_to_period = "am"
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 0.5)
self.assertEqual(leave_form.record.number_of_hours, 4)
# dates are checked in UTC that why -2
self.assertEqual(leave_form.record.date_from, datetime(2019, 9, 2, 6, 0, 0))
self.assertEqual(leave_form.record.date_to, datetime(2019, 9, 2, 10, 0, 0))
# Ask for afternoon
leave_form.request_date_from_period = "pm"
leave_form.request_date_to_period = "pm"
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 0.5)
self.assertEqual(leave_form.record.number_of_hours, 4)
# dates are checked in UTC that why -2
self.assertEqual(leave_form.record.date_from, datetime(2019, 9, 2, 10, 0, 0))
self.assertEqual(leave_form.record.date_to, datetime(2019, 9, 2, 14, 0, 0))
def test_attendance_based_on_duration(self):
calendar = self.env["resource.calendar"].create({
"name": "Full Days",
"duration_based": True,
"attendance_ids": [
Command.clear(),
Command.create({
"name": "Monday Morning",
"duration_hours": 5, # hour_from: 7, hour_to: 12
"day_period": "morning",
"dayofweek": "0"}),
Command.create({
"name": "Monday Afternoon",
"duration_hours": 3, # hour_from: 12, hour_to: 15
"day_period": "afternoon",
"dayofweek": "0"}),
],
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
with Form(
self.env["hr.leave"].with_context(default_employee_id=employee.id)
) as leave_form:
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = date(2019, 9, 2) # Monday
leave_form.request_date_to = date(2019, 9, 2) # Monday
# Ask for morning
leave_form.request_date_from_period = "am"
leave_form.request_date_to_period = "am"
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 0.5)
self.assertEqual(leave_form.record.number_of_hours, 5)
# dates are checked in UTC that why -2
self.assertEqual(leave_form.record.date_from, datetime(2019, 9, 2, 5, 0, 0))
self.assertEqual(leave_form.record.date_to, datetime(2019, 9, 2, 10, 0, 0))
# Ask for afternoon
leave_form.request_date_from_period = "pm"
leave_form.request_date_to_period = "pm"
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 0.5)
self.assertEqual(leave_form.record.number_of_hours, 3)
# dates are checked in UTC that why -2
self.assertEqual(leave_form.record.date_from, datetime(2019, 9, 2, 10, 0, 0))
self.assertEqual(leave_form.record.date_to, datetime(2019, 9, 2, 13, 0, 0))
def test_attendance_based_on_duration_full_day(self):
calendar = self.env["resource.calendar"].create({
"name": "Full Days",
"duration_based": True,
"attendance_ids": [
Command.clear(),
Command.create({
"name": "Monday",
"duration_hours": 6, # hour_from: 9, hour_to: 15
"day_period": "full_day",
"dayofweek": "0",
}),
],
})
employee = self.employee_emp
employee.resource_calendar_id = calendar
with Form(
self.env["hr.leave"].with_context(default_employee_id=employee.id)
) as leave_form:
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = date(2019, 9, 2) # Monday
leave_form.request_date_to = date(2019, 9, 2) # Monday
# Ask for morning
leave_form.request_date_from_period = "am"
leave_form.request_date_to_period = "am"
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 0.5)
self.assertEqual(leave_form.record.number_of_hours, 3)
# dates are checked in UTC that why -2
self.assertEqual(leave_form.record.date_from, datetime(2019, 9, 2, 7, 0, 0))
self.assertEqual(leave_form.record.date_to, datetime(2019, 9, 2, 10, 0, 0))
# Ask for afternoon
leave_form.request_date_from_period = "pm"
leave_form.request_date_to_period = "pm"
leave_form.save() # need to be saved to have access to record
self.assertEqual(leave_form.record.number_of_days, 0.5)
self.assertEqual(leave_form.record.number_of_hours, 3)
# dates are checked in UTC that why -2
self.assertEqual(leave_form.record.date_from, datetime(2019, 9, 2, 10, 0, 0))
self.assertEqual(leave_form.record.date_to, datetime(2019, 9, 2, 13, 0, 0))
def test_attendance_next_day(self):
self.env.user.tz = 'Europe/Brussels'
@ -171,14 +333,14 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
# does not work on mondays
leave_form.request_date_from = date(2019, 9, 2)
leave_form.request_date_to = date(2019, 9, 2)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, 0)
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0))
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
leave = leave_form.record
self.assertEqual(leave.number_of_days, 0)
self.assertEqual(leave.number_of_hours, 0)
self.assertEqual(leave.date_from, datetime(2019, 9, 2, 6, 0, 0))
self.assertEqual(leave.date_to, datetime(2019, 9, 2, 10, 0, 0))
def test_attendance_previous_day(self):
self.env.user.tz = 'Europe/Brussels'
@ -201,14 +363,14 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
# does not work on tuesdays
leave_form.request_date_from = date(2019, 9, 3)
leave_form.request_date_to = date(2019, 9, 3)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, 0)
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0))
self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0))
leave = leave_form.record
self.assertEqual(leave.number_of_days, 0)
self.assertEqual(leave.number_of_hours, 0)
self.assertEqual(leave.date_from, datetime(2019, 9, 3, 6, 0, 0))
self.assertEqual(leave.date_to, datetime(2019, 9, 3, 10, 0, 0))
def test_2weeks_calendar(self):
self.env.user.tz = 'Europe/Brussels'
@ -223,6 +385,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'day_period': 'morning',
'dayofweek': '0',
'week_type': '0',
'duration_days': 0.5,
}),
(0, 0, {
'name': 'monday morning even week',
@ -231,6 +394,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'day_period': 'morning',
'dayofweek': '0',
'week_type': '1',
'duration_days': 0.25
})]
})
employee = self.employee_emp
@ -241,26 +405,28 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
# even week, works 2 hours
leave_form.request_date_from = date(2019, 9, 2)
leave_form.request_date_to = date(2019, 9, 2)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, 0.5)
self.assertEqual(leave_form.number_of_hours_text, '2 Hours')
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 8, 0, 0))
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
leave = leave_form.record
self.assertEqual(leave.number_of_days, 0.25)
self.assertEqual(leave.number_of_hours, 2)
self.assertEqual(leave.date_from, datetime(2019, 9, 2, 8, 0, 0))
self.assertEqual(leave.date_to, datetime(2019, 9, 2, 10, 0, 0))
with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form:
leave_form.holiday_status_id = self.leave_type
# odd week, works 4 hours
leave_form.request_date_from = date(2019, 9, 9)
leave_form.request_date_to = date(2019, 9, 9)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, 0.5)
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
self.assertEqual(leave_form.date_from, datetime(2019, 9, 9, 6, 0, 0))
self.assertEqual(leave_form.date_to, datetime(2019, 9, 9, 10, 0, 0))
leave = leave_form.record
self.assertEqual(leave.number_of_days, 0.5)
self.assertEqual(leave.number_of_hours, 4)
self.assertEqual(leave.date_from, datetime(2019, 9, 9, 6, 0, 0))
self.assertEqual(leave.date_to, datetime(2019, 9, 9, 10, 0, 0))
def test_2weeks_calendar_next_week(self):
self.env.user.tz = 'Europe/Brussels'
@ -285,10 +451,11 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
# even week, does not work
leave_form.request_date_from = date(2019, 9, 2)
leave_form.request_date_to = date(2019, 9, 2)
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
leave_form.request_date_to_period = 'am'
self.assertEqual(leave_form.number_of_days_display, 0)
self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0))
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
leave = leave_form.record
self.assertEqual(leave.number_of_days, 0)
self.assertEqual(leave.number_of_hours, 0)
self.assertEqual(leave.date_from, datetime(2019, 9, 2, 6, 0, 0))
self.assertEqual(leave.date_to, datetime(2019, 9, 2, 10, 0, 0))

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from datetime import date
from dateutil.relativedelta import relativedelta, MO, FR
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@ -14,7 +13,7 @@ class TestChangeDepartment(TestHrHolidaysCommon):
HolidayStatusManagerGroup = self.env['hr.leave.type'].with_user(self.user_hrmanager_id)
self.holidays_status_1 = HolidayStatusManagerGroup.create({
'name': 'NotLimitedHR',
'requires_allocation': 'no',
'requires_allocation': False,
})
def create_holiday(name, start, end):
@ -22,45 +21,51 @@ class TestChangeDepartment(TestHrHolidaysCommon):
'name': name,
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_1.id,
'date_from': (datetime.today() + relativedelta(days=start)).strftime('%Y-%m-%d %H:%M'),
'date_to': datetime.today() + relativedelta(days=end),
'number_of_days': end-start,
'request_date_from': date.today() + relativedelta(weekday=(MO(2) if start > 0 else FR(-1))) + relativedelta(days=start),
'request_date_to': date.today() + relativedelta(weekday=(MO(2) if start > 0 else FR(-1))) + relativedelta(days=end),
})
# Non approved leave request change department
self.employee_emp.department_id = self.rd_dept
hol1_employee_group = create_holiday("hol1", 1, 2)
hol1_employee_group = create_holiday("hol1", 1, 1)
self.employee_emp.department_id = self.hr_dept
self.assertEqual(hol1_employee_group.department_id, self.hr_dept, 'hr_holidays: non approved leave request should change department if employee change department')
# flushing is needed after approving because the hr.leave.department_id
# is not yet recomputed
# Approved passed leave request change department
self.employee_emp.department_id = self.hr_dept
hol2_employee_group = create_holiday("hol2", -4, -3)
hol2_employee_group = create_holiday("hol2", -1, -1)
hol2_user_group = hol2_employee_group.with_user(self.user_hruser_id)
hol2_user_group.action_approve()
self.env.flush_all()
self.employee_emp.department_id = self.rd_dept
self.assertEqual(hol2_employee_group.department_id, self.hr_dept, 'hr_holidays: approved passed leave request should stay in previous department if employee change department')
# Approved futur leave request change department
# Approved future leave request change department
self.employee_emp.department_id = self.hr_dept
hol22_employee_group = create_holiday("hol22", 3, 4)
hol22_employee_group = create_holiday("hol22", 2, 2)
hol22_user_group = hol22_employee_group.with_user(self.user_hruser_id)
hol22_user_group.action_approve()
self.env.flush_all()
self.employee_emp.department_id = self.rd_dept
self.assertEqual(hol22_employee_group.department_id, self.rd_dept, 'hr_holidays: approved futur leave request should change department if employee change department')
self.assertEqual(hol22_employee_group.department_id, self.rd_dept, 'hr_holidays: approved future leave request should change department if employee change department')
# Refused passed leave request change department
self.employee_emp.department_id = self.rd_dept
hol3_employee_group = create_holiday("hol3", -6, -5)
hol3_employee_group = create_holiday("hol3", -2, -2)
hol3_user_group = hol3_employee_group.with_user(self.user_hruser_id)
hol3_user_group.action_refuse()
self.env.flush_all()
self.employee_emp.department_id = self.hr_dept # Change department
self.assertEqual(hol3_employee_group.department_id, self.rd_dept, 'hr_holidays: refused passed leave request should stay in previous department if employee change department')
self.assertEqual(hol3_user_group.department_id, self.rd_dept, 'hr_holidays: refused passed leave request should stay in previous department if employee change department')
# Refused futur leave request change department
# Refused future leave request change department
self.employee_emp.department_id = self.rd_dept
hol32_employee_group = create_holiday("hol32", 5, 6)
hol32_employee_group = create_holiday("hol32", 10, 10)
hol32_user_group = hol32_employee_group.with_user(self.user_hruser_id)
hol32_user_group.action_refuse()
self.env.flush_all()
self.employee_emp.department_id = self.hr_dept # Change department
self.assertEqual(hol32_employee_group.department_id, self.hr_dept, 'hr_holidays: refused futur leave request should change department if employee change department')
self.assertEqual(hol32_employee_group.department_id, self.hr_dept, 'hr_holidays: refused future leave request should change department if employee change department')

View file

@ -3,6 +3,7 @@
from datetime import date, datetime
from odoo import Command
from odoo.tests import tagged
from odoo.tests.common import TransactionCase, warmup
@ -20,9 +21,9 @@ class TestCompanyLeave(TransactionCase):
cls.bank_holiday = cls.env['hr.leave.type'].create({
'name': 'Bank Holiday',
'responsible_id': cls.env.user.id,
'responsible_ids': [Command.link(cls.env.ref('base.user_admin').id)],
'company_id': cls.company.id,
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.paid_time_off = cls.env['hr.leave.type'].create({
@ -30,7 +31,7 @@ class TestCompanyLeave(TransactionCase):
'request_unit': 'day',
'leave_validation_type': 'both',
'company_id': cls.company.id,
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.employee = cls.env['hr.employee'].create({
@ -39,16 +40,7 @@ class TestCompanyLeave(TransactionCase):
'tz': "Europe/Brussels",
})
cls.paid_time_off_hours = cls.env['hr.leave.type'].create({
'name': 'Paid Time Off in Hours',
'request_unit': 'hour',
'leave_validation_type': 'no_validation',
'company_id': cls.company.id,
'time_type': 'other',
'requires_allocation': 'yes',
})
def test_leave_whole_company_01(self):
def test_01_leave_whole_company(self):
# TEST CASE 1: Leaves taken in days. Take a 3 days leave
# Add a company leave on the second day.
# Check that leave is split into 2.
@ -58,50 +50,38 @@ class TestCompanyLeave(TransactionCase):
'employee_id': self.employee.id,
'holiday_status_id': self.paid_time_off.id,
'request_date_from': date(2020, 1, 7),
'date_from': date(2020, 1, 7),
'request_date_to': date(2020, 1, 9),
'date_to': date(2020, 1, 9),
'number_of_days': 3,
})
leave._compute_date_from_to()
company_leave = self.env['hr.leave'].create({
company_leave = self.env['hr.leave.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'allocation_mode': 'company',
'company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 8),
'request_date_from': date(2020, 1, 8),
'date_to': date(2020, 1, 8),
'request_date_to': date(2020, 1, 8),
'number_of_days': 1,
})
company_leave._compute_date_from_to()
company_leave.action_validate()
company_leave.action_generate_time_off()
all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id')
self.assertEqual(len(all_leaves), 4)
# Original Leave
self.assertEqual(leave.state, 'refuse')
# before leave
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 7, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 7, 16, 0))
self.assertEqual(len(all_leaves), 3)
# Before Time Off
self.assertEqual(all_leaves[0].date_from, datetime(2020, 1, 7, 7, 0))
self.assertEqual(all_leaves[0].date_to, datetime(2020, 1, 7, 16, 0))
self.assertEqual(all_leaves[0].number_of_days, 1)
self.assertEqual(all_leaves[0].state, 'confirm')
# After Time Off
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 9, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 9, 16, 0))
self.assertEqual(all_leaves[1].number_of_days, 1)
self.assertEqual(all_leaves[1].state, 'confirm')
# After leave
self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 9, 7, 0))
self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 9, 16, 0))
# Company Time Off
self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 8, 7, 0))
self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 8, 16, 0))
self.assertEqual(all_leaves[2].number_of_days, 1)
self.assertEqual(all_leaves[2].state, 'confirm')
# Company Leave
self.assertEqual(all_leaves[3].date_from, datetime(2020, 1, 8, 7, 0))
self.assertEqual(all_leaves[3].date_to, datetime(2020, 1, 8, 16, 0))
self.assertEqual(all_leaves[3].number_of_days, 1)
self.assertEqual(all_leaves[3].state, 'validate')
self.assertEqual(all_leaves[2].state, 'validate')
def test_leave_whole_company_02(self):
def test_02_leave_whole_company(self):
# TEST CASE 2: Leaves taken in half-days. Take a 3 days leave
# Add a company leave on the second day
# Check that leave is split into 2
@ -112,50 +92,41 @@ class TestCompanyLeave(TransactionCase):
'employee_id': self.employee.id,
'holiday_status_id': self.paid_time_off.id,
'request_date_from': date(2020, 1, 7),
'date_from': date(2020, 1, 7),
'request_date_to': date(2020, 1, 9),
'date_to': date(2020, 1, 9),
'number_of_days': 3,
})
leave._compute_date_from_to()
company_leave = self.env['hr.leave'].create({
company_leave = self.env['hr.leave.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'allocation_mode': 'company',
'company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 8),
'request_date_from': date(2020, 1, 8),
'date_to': date(2020, 1, 8),
'request_date_to': date(2020, 1, 8),
'number_of_days': 1,
})
company_leave._compute_date_from_to()
company_leave.action_validate()
company_leave.action_generate_time_off()
all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id')
self.assertEqual(len(all_leaves), 4)
# Original Leave
self.assertEqual(leave.state, 'refuse')
# before leave
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 7, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 7, 16, 0))
self.assertEqual(len(all_leaves), 3)
# Before Time Off
self.assertEqual(all_leaves[0].date_from, datetime(2020, 1, 7, 7, 0))
self.assertEqual(all_leaves[0].date_to, datetime(2020, 1, 7, 16, 0))
self.assertEqual(all_leaves[0].number_of_days, 1)
self.assertEqual(all_leaves[0].state, 'confirm')
# After Time Off
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 9, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 9, 16, 0))
self.assertEqual(all_leaves[1].number_of_days, 1)
self.assertEqual(all_leaves[1].state, 'confirm')
# After leave
self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 9, 7, 0))
self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 9, 16, 0))
# Company Time Off
self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 8, 7, 0))
self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 8, 16, 0))
self.assertEqual(all_leaves[2].number_of_days, 1)
self.assertEqual(all_leaves[2].state, 'confirm')
# Company Leave
self.assertEqual(all_leaves[3].date_from, datetime(2020, 1, 8, 7, 0))
self.assertEqual(all_leaves[3].date_to, datetime(2020, 1, 8, 16, 0))
self.assertEqual(all_leaves[3].number_of_days, 1)
self.assertEqual(all_leaves[3].state, 'validate')
self.assertEqual(all_leaves[2].state, 'validate')
def test_leave_whole_company_03(self):
# TEST CASE 3: Leaves taken in half-days. Take a 0.5 days leave
def test_03_leave_whole_company(self):
# TEST CASE 3: Time Off taken in half-days. Take a 0.5 days leave
# Add a company leave on the same day
# Check that leave refused
self.paid_time_off.request_unit = 'half_day'
@ -164,43 +135,34 @@ class TestCompanyLeave(TransactionCase):
'name': 'Hol11',
'employee_id': self.employee.id,
'holiday_status_id': self.paid_time_off.id,
'date_from': date(2020, 1, 7),
'request_date_from': date(2020, 1, 7),
'date_to': date(2020, 1, 7),
'request_date_to': date(2020, 1, 7),
'number_of_days': 0.5,
'request_unit_half': True,
'request_date_from_period': 'am',
})
leave._compute_date_from_to()
company_leave = self.env['hr.leave'].create({
company_leave = self.env['hr.leave.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'allocation_mode': 'company',
'company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 7),
'request_date_from': date(2020, 1, 7),
'date_to': date(2020, 1, 7),
'request_date_to': date(2020, 1, 7),
'number_of_days': 1,
})
company_leave._compute_date_from_to()
company_leave.action_validate()
company_leave.action_generate_time_off()
all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id')
self.assertEqual(len(all_leaves), 2)
# Original Leave
# Original Time Off
self.assertEqual(leave.state, 'refuse')
# Company Leave
# Company Time Off
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 7, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 7, 16, 0))
self.assertEqual(all_leaves[1].number_of_days, 1)
self.assertEqual(all_leaves[1].state, 'validate')
def test_leave_whole_company_04(self):
def test_04_leave_whole_company(self):
# TEST CASE 4: Leaves taken in days. Take a 1 days leave
# Add a company leave on the same day
# Check that leave is refused
@ -210,41 +172,34 @@ class TestCompanyLeave(TransactionCase):
'name': 'Hol11',
'employee_id': self.employee.id,
'holiday_status_id': self.paid_time_off.id,
'date_from': datetime.now(),
'request_date_from': date(2020, 1, 9),
'date_to': datetime.now(),
'request_date_to': date(2020, 1, 9),
'number_of_days': 1,
})
leave._compute_date_from_to()
company_leave = self.env['hr.leave'].create({
company_leave = self.env['hr.leave.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'allocation_mode': 'company',
'company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 9),
'request_date_from': date(2020, 1, 9),
'date_to': date(2020, 1, 9),
'request_date_to': date(2020, 1, 9),
'number_of_days': 1,
})
company_leave._compute_date_from_to()
company_leave.action_validate()
company_leave.action_generate_time_off()
all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id')
self.assertEqual(len(all_leaves), 2)
# Original Leave
# Original Time Off
self.assertEqual(leave.state, 'refuse')
# Company Leave
# Company Time Off
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 9, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 9, 16, 0))
self.assertEqual(all_leaves[1].number_of_days, 1)
self.assertEqual(all_leaves[1].state, 'validate')
def test_leave_whole_company_06(self):
def test_06_leave_whole_company(self):
# Test case 6: Leaves taken in days. But the employee
# only works on Monday, Wednesday and Friday
# Takes a time off for all the week (3 days), should be split
@ -252,10 +207,13 @@ class TestCompanyLeave(TransactionCase):
self.employee.resource_calendar_id.write({'attendance_ids': [
(5, 0, 0),
(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': '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': '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'})
]})
@ -263,45 +221,36 @@ class TestCompanyLeave(TransactionCase):
'name': 'Hol11',
'employee_id': self.employee.id,
'holiday_status_id': self.paid_time_off.id,
'date_from': date(2020, 1, 6),
'request_date_from': date(2020, 1, 6),
'date_to': date(2020, 1, 10),
'request_date_to': date(2020, 1, 10),
'number_of_days': 3,
})
leave._compute_date_from_to()
company_leave = self.env['hr.leave'].create({
company_leave = self.env['hr.leave.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'allocation_mode': 'company',
'company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 10),
'request_date_from': date(2020, 1, 10),
'date_to': date(2020, 1, 10),
'request_date_to': date(2020, 1, 10),
'number_of_days': 1,
})
company_leave._compute_date_from_to()
company_leave.action_validate()
company_leave.action_generate_time_off()
all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id')
self.assertEqual(len(all_leaves), 3)
# Original Leave
self.assertEqual(leave.state, 'refuse')
# before leave
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 6, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 9, 16, 0))
self.assertEqual(all_leaves[1].number_of_days, 2)
self.assertEqual(all_leaves[1].state, 'confirm')
# Company Leave
self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 10, 7, 0))
self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 10, 16, 0))
self.assertEqual(all_leaves[2].number_of_days, 1)
self.assertEqual(all_leaves[2].state, 'validate')
self.assertEqual(len(all_leaves), 2)
# Before Time Off
self.assertEqual(all_leaves[0].date_from, datetime(2020, 1, 6, 7, 0))
self.assertEqual(all_leaves[0].date_to, datetime(2020, 1, 9, 16, 0))
self.assertEqual(all_leaves[0].number_of_days, 2)
self.assertEqual(all_leaves[0].state, 'confirm')
# Company Time Off
self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 10, 7, 0))
self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 10, 16, 0))
self.assertEqual(all_leaves[1].number_of_days, 1)
self.assertEqual(all_leaves[1].state, 'validate')
@warmup
def test_leave_whole_company_07(self):
def test_07_leave_whole_company(self):
# Test Case 7: Try to create a bank holidays for a lot of
# employees, and check the performances
# 100 employees - 15 already on holidays that day
@ -316,199 +265,23 @@ class TestCompanyLeave(TransactionCase):
'employee_id': employee.id,
'holiday_status_id': self.paid_time_off.id,
'request_date_from': date(2020, 3, 29),
'date_from': datetime(2020, 3, 29, 7, 0, 0),
'request_date_to': date(2020, 4, 1),
'date_to': datetime(2020, 4, 1, 19, 0, 0),
'number_of_days': 3,
} for employee in employees[0:15]])
leaves._compute_date_from_to()
company_leave = self.env['hr.leave'].create({
company_leave = self.env['hr.leave.generate.multi.wizard'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'allocation_mode': 'company',
'company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 4, 1),
'request_date_from': date(2020, 4, 1),
'date_to': date(2020, 4, 1),
'request_date_to': date(2020, 4, 1),
'number_of_days': 1,
'date_from': date(2020, 4, 2),
'date_to': date(2020, 4, 2),
})
company_leave._compute_date_from_to()
with self.assertQueryCount(__system__=830): # 770 community
with self.assertQueryCount(__system__=1856): # 770 community
# Original query count: 1987
# Without tracking/activity context keys: 5154
company_leave.action_validate()
company_leave.action_generate_time_off()
leaves = self.env['hr.leave'].search([('holiday_status_id', '=', self.bank_holiday.id)])
self.assertEqual(len(leaves), 102)
def test_leave_whole_company_08(self):
"""
Give a company leave with employees on different schedules.
"""
# employee on different schedule
calendar = self.env['resource.calendar'].create({
'name': 'Different schedule',
'attendance_ids': [(5, 0, 0),
(0, 0, {
'name': 'monday morning, earlier start',
'hour_from': 7.5,
'hour_to': 9.75,
'day_period': 'morning',
'dayofweek': '0',
}),
(0, 0, {
'name': 'monday morning, second attendance',
'hour_from': 10,
'hour_to': 12,
'day_period': 'morning',
'dayofweek': '0',
}),
(0, 0, {
'name': 'monday afternoon',
'hour_from': 13,
'hour_to': 17,
'day_period': 'afternoon',
'dayofweek': '0',
}),
]
})
self.employee.resource_calendar_id = calendar
# employee on default schedule
employee2 = self.env['hr.employee'].create({
'name': 'Employee2',
'company_id': self.company.id,
'tz': "Europe/Brussels",
})
company_leave = self.env['hr.leave'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 6),
'request_date_from': date(2020, 1, 6),
'date_to': date(2020, 1, 6),
'request_date_to': date(2020, 1, 6),
'number_of_days': 1,
})
company_leave._compute_date_from_to()
company_leave.action_validate()
half_day_company_leave = self.env['hr.leave'].create({
'name': 'Bank Holiday',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 13),
'request_date_from': date(2020, 1, 13),
'date_to': date(2020, 1, 13),
'request_date_to': date(2020, 1, 13),
'number_of_days': 0.5,
'request_unit_half': True,
'request_date_from_period': 'am',
})
half_day_company_leave._compute_date_from_to()
half_day_company_leave.action_validate()
employee_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id')
self.assertEqual(employee_leaves[0].date_from, datetime(2020, 1, 6, 6, 30))
self.assertEqual(employee_leaves[0].date_to, datetime(2020, 1, 6, 16, 0))
self.assertEqual(employee_leaves[0].number_of_days, 1)
self.assertEqual(employee_leaves[0].number_of_hours_display, 8.25)
self.assertEqual(employee_leaves[1].date_from, datetime(2020, 1, 13, 6, 30))
self.assertEqual(employee_leaves[1].date_to, datetime(2020, 1, 13, 11, 0))
self.assertEqual(employee_leaves[1].number_of_days, 0.5)
self.assertEqual(employee_leaves[1].number_of_hours_display, 4.25)
employee2_leaves = self.env['hr.leave'].search([('employee_id', '=', employee2.id)], order='id')
self.assertEqual(employee2_leaves[0].date_from, datetime(2020, 1, 6, 7, 0))
self.assertEqual(employee2_leaves[0].number_of_days, 1)
self.assertEqual(employee2_leaves[1].date_from, datetime(2020, 1, 13, 7, 0))
self.assertEqual(employee2_leaves[1].date_to, datetime(2020, 1, 13, 11, 0))
self.assertEqual(employee2_leaves[1].number_of_days, 0.5)
def test_leave_whole_company_09(self):
"""
Check leaves given in half days and in hours for a company.
"""
half_day_leave = self.env['hr.leave'].create({
'name': 'Bank Holiday (full day)',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 6),
'request_date_from': date(2020, 1, 6),
'date_to': date(2020, 1, 6),
'request_date_to': date(2020, 1, 6),
'number_of_days': 0.5,
'request_unit_half': True,
'request_date_from_period': 'am',
})
hours_leave = self.env['hr.leave'].create({
'name': 'Bank Holiday (half day)',
'holiday_type': 'company',
'mode_company_id': self.company.id,
'holiday_status_id': self.bank_holiday.id,
'date_from': date(2020, 1, 7),
'request_date_from': date(2020, 1, 7),
'date_to': date(2020, 1, 7),
'request_date_to': date(2020, 1, 7),
'request_unit_hours': True,
'request_hour_from': '5.5',
'request_hour_to': '9',
})
half_day_leave._compute_date_from_to()
half_day_leave.action_validate()
hours_leave._compute_date_from_to()
hours_leave.action_validate()
employee_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id')
# half days leave
self.assertEqual(employee_leaves[0].date_from, datetime(2020, 1, 6, 7, 0))
self.assertEqual(employee_leaves[0].date_to, datetime(2020, 1, 6, 11, 0))
self.assertEqual(employee_leaves[0].number_of_days, 0.5)
# leave given in hours
self.assertEqual(employee_leaves[1].date_from, datetime(2020, 1, 7, 3, 30))
self.assertEqual(employee_leaves[1].date_to, datetime(2020, 1, 7, 7, 0))
self.assertEqual(employee_leaves[1].number_of_hours_display, 1.0)
def test_leave_whole_company_10(self):
"""
Check leaves given in hours for a company,
Making sure no leaves are given for 0 Hours / Week employee(i.e. Contractors billed for hours).
"""
employee_0_test_10, employee_1_test_10, employee_2_test_10 = self.env['hr.employee'].create([{
'name': 'My Employee 0',
'company_id': self.company.id,
'tz': "Europe/Brussels",
},{
'name': 'My Employee 1',
'company_id': self.company.id,
'tz': "Europe/Brussels",
},{
'name': 'My Employee 2',
'company_id': self.company.id,
'tz': "Europe/Brussels",
}])
zero_hours_working_schedule = self.env['resource.calendar'].create({
'name': 'Standard - Hours/Week',
'hours_per_day': 0,
'tz': "Europe/Brussels",
})
employee_0_test_10.resource_calendar_id = zero_hours_working_schedule
self.env['hr.leave.allocation'].create({
'name': 'Holiday (8 Hours)',
'holiday_status_id': self.paid_time_off_hours.id,
'holiday_type': 'company',
'mode_company_id': self.company.id,
'number_of_days': 1,
})
employee_leaves = self.env['hr.leave.allocation'].search([
('name', '=', 'Holiday (8 Hours)'),
('employee_id', 'in', [employee_0_test_10.id, employee_1_test_10.id, employee_2_test_10.id])])
self.assertEqual(len(employee_leaves), 2)
self.assertEqual(len(leaves), 101)

View file

@ -1,16 +1,17 @@
from datetime import date, datetime
from freezegun import freeze_time
from datetime import datetime
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
class TestDashboard(TestHrHolidaysCommon):
def test_dashboard_special_days(self):
self.env.user = self.user_hrmanager
self.uid = self.user_hrmanager.id
employee = self.env.user.employee_id
other_calendar = employee.company_id.resource_calendar_ids[1]
other_calendar = self.env['resource.calendar'].sudo().create({
'name': 'Other calendar',
})
stress_day_vals = [
mandatory_day_vals = [
{
'name': 'Super Event (employee schedule)',
'company_id': employee.company_id.id,
@ -32,7 +33,7 @@ class TestDashboard(TestHrHolidaysCommon):
'resource_calendar_id': other_calendar.id,
}
]
self.env['hr.leave.stress.day'].create(stress_day_vals)
self.env['hr.leave.mandatory.day'].create(mandatory_day_vals)
public_holiday_vals = [
{
@ -55,38 +56,7 @@ class TestDashboard(TestHrHolidaysCommon):
]
self.env['resource.calendar.leaves'].create(public_holiday_vals)
dashboard_data = self.env['hr.employee'].get_special_days_data("2021/06/01", "2021/07/01")
dashboard_data = self.env['hr.employee'].get_special_days_data("2021-06-01", "2021-07-01")
self.assertEqual({d["title"] for d in dashboard_data["stressDays"]}, {'Super Event (employee schedule)', 'Super Event (no schedule)'})
self.assertEqual({d["title"] for d in dashboard_data["mandatoryDays"]}, {'Super Event (employee schedule)', 'Super Event (no schedule)'})
self.assertEqual({d["title"] for d in dashboard_data["bankHolidays"]}, {'Public holiday (employee schedule)', 'Public holiday (no schedule)'})
def test_dashboard_max_near_accrual_validity_end(self):
emp_id = self.employee_emp_id
leave_type = self.env['hr.leave.type'].create({
'name': 'Test Time Off',
'requires_allocation': 'yes',
'employee_requests': 'no',
'allocation_validation_type': 'no',
'leave_validation_type': 'both',
'responsible_id': self.user_hrmanager_id,
})
self.env['hr.leave.allocation'].create([{
'employee_id': emp_id,
'name': '10 days allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 10,
'date_from': date(2024, 1, 1),
'date_to': date(2024, 12, 30),
}, {
'employee_id': emp_id,
'name': '2 days allocation starting later',
'holiday_status_id': leave_type.id,
'number_of_days': 2,
'date_from': date(2024, 2, 1),
'date_to': date(2024, 12, 30),
}])
with freeze_time('2024-12-27'):
employee_max_leaves = leave_type.get_employees_days([emp_id])[emp_id][leave_type.id]['max_leaves']
self.assertEqual(employee_max_leaves, 12, "All 12 leaves should be seen from the dashboard")

View file

@ -0,0 +1,812 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.addons.base.tests.common import HttpCase
from odoo.tests.common import tagged
from odoo.tests.common import users
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('post_install', '-at_install', 'carryover_expiring_leaves')
class TestExpiringLeaves(HttpCase, TestHrHolidaysCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Test',
'time_type': 'leave',
'requires_allocation': True,
'allocation_validation_type': 'no_validation',
})
cls.accrual_plan_with_accrual_validity = cls.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan With Accrual Validity',
'carryover_date': 'other',
'carryover_day': 1,
'carryover_month': '4',
'level_ids': [
(0, 0, {
'start_count': 0,
'start_type': 'day',
'added_value': 3,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'all',
'carryover_options': 'limited',
'postpone_max_days': 5,
'accrual_validity': True,
'accrual_validity_count': 3,
'accrual_validity_type': 'month',
})
],
})
@users('enguerran')
def test_no_carried_over_leaves(self):
"""
The accrual plan:
- Accrue at the end of period.
- Carryover date : 31/12 (end of the year).
Milestones:
Milestone 1:
- Start immediately.
- Accrue 10 days.
- Accrue days on 01/01 (start of the year).
- Unused accruals are lost (no leaves are carried over).
Create an accrual allocation with this plan and allocate it to the logged-in user.
The employee will be accrued 10 days. The employee will use some of them. The carryover policy is set
to None, so no leaves will be carriedover. The remaining days of the allocation will expire.
"""
number_of_accrued_days = 10
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'other',
'carryover_day': 31,
'carryover_month': '12',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': number_of_accrued_days,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'lost'
})
],
})
logged_in_emp = self.env.user.employee_id
allocation = self.env['hr.leave.allocation'].sudo().create({
'date_from': date(date.today().year, 1, 1),
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
target_date = date(date.today().year + 1, 12, 30)
leave = self.env['hr.leave'].create({
'employee_id': logged_in_emp.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': target_date + relativedelta(month=12, day=1),
'request_date_to': target_date + relativedelta(month=12, day=7)
})
allocation_data = self.leave_type.get_allocation_data(
allocation.employee_id, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
allocation._get_carryover_date(target_date).strftime('%m/%d/%Y'),
"The expiration date should match the carryover date")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'],
number_of_accrued_days - leave.number_of_days,
"All the remaining days of the allocation will expire")
@users('enguerran')
def test_carried_over_leaves_with_maximum(self):
"""
The accrual plan:
- Accrue at the end of period.
- Carryover date : 31/12 (end of the year).
Milestones:
Milestone 1:
- Start immediately.
- Accrue 20 days.
- Accrue days on 01/01 (start of the year).
- Unused accruals are carried over with a maximum of 10.
Create an accrual allocation with this plan and allocate it to the logged-in user.
The employee will be accrued 20 days. The employee will use some of them. The carryover
policy is set to carryover with a maximum of 10, so at max 10 leaves will be carriedover.
The remaining days of the allocation will expire.
"""
number_of_accrued_days = 20
carryover_limit = 10
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'other',
'carryover_day': 31,
'carryover_month': '12',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': number_of_accrued_days,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'all',
'carryover_options': 'limited',
'postpone_max_days': carryover_limit,
})
],
})
logged_in_emp = self.env.user.employee_id
allocation = self.env['hr.leave.allocation'].sudo().create({
'date_from': date(date.today().year, 1, 1),
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
target_date = date(date.today().year + 1, 12, 30)
leave = self.env['hr.leave'].create({
'employee_id': logged_in_emp.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': target_date + relativedelta(month=12, day=1),
'request_date_to': target_date + relativedelta(month=12, day=7)
})
allocation_data = self.leave_type.get_allocation_data(
allocation.employee_id, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
allocation._get_carryover_date(target_date).strftime('%m/%d/%Y'),
"The expiration date should match the carryover date")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'],
number_of_accrued_days - leave.number_of_days - carryover_limit,
"All the remaining days of the allocation will expire")
@users('enguerran')
def test_allocation_with_max_carryover_and_expiring_allocation(self):
"""
The accrual plan:
- Accrue at the end of period.
- Carryover date : 31/12 (end of the year).
Milestones:
Milestone 1:
- Start immediately.
- Accrue 20 days.
- Accrue days on 01/01 (start of the year).
- Unused accruals are carried over with a maximum of 10.
Create an accrual allocation with this plan:
- Employee: logged-in user.
- Start date: 01/01/2024
- Carryover Policy: carryover with a maximum of 10
On 01/01/2025 The employee will be accrued 20 days.
The employee will take 5 leaves using this allocation.
- The carryover policy is set to carryover with a maximum of 10, so at max 10 days will be carriedover.
- 5 days will expire.
Create a second accrual allocation:
- The configuration is the same as the previous one except that carryover date is the start of the year
and all unused accruals will be carriedover.
- This allocation will expire on 31/12 next year. 20 days will expire when the allocation expires.
Number of expiring leaves = number of not carriedover days from the first allocation +
number of expiring leaves due to expiration of the second allocation
= 5 + 20 = 25 days
"""
number_of_accrued_days = 20
carryover_limit = 10
accrual_plan_1 = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'other',
'carryover_day': 31,
'carryover_month': '12',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': number_of_accrued_days,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'all',
'carryover_options': 'limited',
'postpone_max_days': carryover_limit,
})
],
})
accrual_plan_2 = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan With All Leaves Carried Over',
'can_be_carryover': True,
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': number_of_accrued_days,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'all',
})
],
})
logged_in_emp = self.env.user.employee_id
with freeze_time("2024-01-01"):
allocation_with_carryover = self.env['hr.leave.allocation'].sudo().create({
'date_from': '2024-01-01',
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan_1.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
leave = self.env['hr.leave'].create({
'employee_id': logged_in_emp.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': '2025-12-01',
'request_date_to': '2025-12-05'
})
# The expiring allocation
self.env['hr.leave.allocation'].sudo().create({
'date_from': '2024-01-01',
'date_to': '2025-12-31',
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan_2.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
target_date = date(2025, 12, 30)
allocation_data = self.leave_type.get_allocation_data(logged_in_emp, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
allocation_with_carryover._get_carryover_date(target_date).strftime('%m/%d/%Y'),
"The expiration date should match the carryover date")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'],
(number_of_accrued_days - leave.number_of_days - carryover_limit) + number_of_accrued_days,
"All the remaining days of the allocation will expire")
@users('enguerran')
def test_expiring_allocation_without_carried_over_leaves(self):
"""
The accrual plan:
- Accrue at the end of period.
- Carryover date : 31/12 (end of the year).
Milestones:
Milestone 1:
- Start immediately.
- Accrue 20 days.
- Accrue days on 01/01 (start of the year).
- Unused accruals are lost (no leaves are carried over).
Create an accrual allocation with this plan and allocate it to the logged-in user. This
allocation will expire on 31/12 next year.
The employee will be accrued 10 days. The carryover policy is set to None, so no leaves
will be carriedover. The remaining days of the allocation will expire.
The number of expiring leaves should be 10 and not 20 (10 for the expiration of the allocation
and 10 for the leaves being not carried over).
"""
number_of_accrued_days = 10
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'other',
'carryover_day': 31,
'carryover_month': '12',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': number_of_accrued_days,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'lost',
})
],
})
logged_in_emp = self.env.user.employee_id
allocation = self.env['hr.leave.allocation'].sudo().create({
'date_from': date(date.today().year, 1, 1),
'date_to': date(date.today().year + 1, 12, 31),
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
target_date = date(date.today().year + 1, 12, 30)
allocation_data = self.leave_type.get_allocation_data(
allocation.employee_id, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
allocation._get_carryover_date(target_date).strftime('%m/%d/%Y'),
"The expiration date should match the carryover date")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'],
number_of_accrued_days,
"All the remaining days of the allocation will expire")
@users('enguerran')
def test_expiration_date(self):
"""
The accrual plan:
- Accrue at the end of period.
- Carryover date : 01/01.
Milestones:
Milestone 1:
- Start immediately.
- Accrue 10 days.
- Accrue days on 01/01 (start of the year).
- Unused accruals are carried over with a maximum of 5.
Create an accrual allocation with this plan and allocate it to the logged-in user.
The employee will be accrued 10 days. The carryover policy is set to carryover with
a maximum of 5, so only 5 leaves will be carriedover. The remaining days of the allocation
will expire.
If the target date is 01/01/2025, then the expiration date should be 01/01/2026 because 5 of the days
accrued on 01/01/2025 will expire on 01/01/2026.
"""
with freeze_time('2024-01-01'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'year_start',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': 10,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'all',
'carryover_options': 'limited',
'postpone_max_days': 5,
})
],
})
logged_in_emp = self.env.user.employee_id
allocation = self.env['hr.leave.allocation'].sudo().create({
'date_from': date(2024, 1, 1),
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
target_date = date(2025, 1, 1)
allocation_data = self.leave_type.get_allocation_data(allocation.employee_id, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
(target_date + relativedelta(years=1)).strftime('%m/%d/%Y'),
"The expiration date should be the carryover date of the year that follows the target date's year")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'], 5)
@users('enguerran')
def test_expiration_date_2(self):
"""
- Define an accrual plan:
- Carryover date : 1st of September.
- Carryover with a maximum of 5 days.
- Define 2 allocations:
- Both allocations will start on 01/01/2023.
- Both allocations use the accrual plan defined above.
- Both allocations accrue 3 days yearly.
- The first allocation expires on the 1st of October.
- The second allocation doesn't expire.
- On 01/01/2024, both allocations will accrue 3 days for the employee.
- The expiration date should be 01/10/2024 because on 01/09/2024, 3 days will carryover for both allocations
and no days will expire.
"""
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'other',
'carryover_day': 1,
'carryover_month': '9',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': 3,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'all',
'carryover_options': 'limited',
'postpone_max_days': 5,
})
],
})
logged_in_emp = self.env.user.employee_id
with freeze_time("2023-01-01"):
# Allocation 1
self.env['hr.leave.allocation'].sudo().create({
'date_from': '2023-01-01',
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
# Allocation 2
self.env['hr.leave.allocation'].sudo().create({
'date_from': '2023-01-01',
'date_to': '2024-10-01',
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
with freeze_time("2024-01-01"):
self.env['hr.leave.allocation'].with_user(self.user_hruser)._update_accrual()
target_date = date(2024, 1, 1)
allocation_data = self.leave_type.get_allocation_data(logged_in_emp, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
(target_date + relativedelta(month=10)).strftime('%m/%d/%Y'),
"The expiration date should be the expiration date of the second allocation because no days will expire on carryover date")
@users('enguerran')
def test_no_carried_over_leaves_for_flexible_resource(self):
"""
Identical test to test_no_carried_over_leaves but with a flexible resource calendar. The test aims to verify that
the expiration date is correctly calculated even if attendance is not taken into account for the flexible resource.
The accrual plan:
- Accrue at the end of period.
- Carryover date : 31/12 (end of the year).
Milestones:
Milestone 1:
- Start immediately.
- Accrue 10 days.
- Accrue days on 01/01 (start of the year).
- Unused accruals are lost (no leaves are carried over).
Create an accrual allocation with this plan and allocate it to the logged-in user.
The employee will be accrued 10 days. The employee will use some of them. The carryover policy is set
to None, so no leaves will be carriedover. The remaining days of the allocation will expire.
"""
number_of_accrued_days = 10
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'other',
'carryover_day': 31,
'carryover_month': '12',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': number_of_accrued_days,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'lost'
})
],
})
self.flex_40h_calendar = self.env['resource.calendar'].sudo().create({
'name': 'Flexible 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'hours_per_week': 80.0,
'full_time_required_hours': 80.0,
'flexible_hours': True,
})
logged_in_emp = self.env.user.employee_id
logged_in_emp.resource_calendar_id = self.flex_40h_calendar
allocation = self.env['hr.leave.allocation'].sudo().create({
'date_from': date(date.today().year, 1, 1),
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
target_date = date(date.today().year + 1, 12, 30)
leave = self.env['hr.leave'].create({
'employee_id': logged_in_emp.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': target_date + relativedelta(month=12, day=1),
'request_date_to': target_date + relativedelta(month=12, day=7)
})
allocation_data = self.leave_type.get_allocation_data(
allocation.employee_id, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
allocation._get_carryover_date(target_date).strftime('%m/%d/%Y'),
"The expiration date should match the carryover date")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'],
number_of_accrued_days - leave.number_of_days,
"All the remaining days of the allocation will expire")
@users('enguerran')
def test_no_carried_over_leaves_for_fully_flexible_resource(self):
"""
/!\\ Fully Flexible Resource should not take leaves. However the test aims to verify that the expiration date
is correctly calculated for the fully flexible resource.
The accrual plan:
- Accrue at the end of period.
- Carryover date : 31/12 (end of the year).
Milestones:
Milestone 1:
- Start immediately.
- Accrue 10 days.
- Accrue days on 01/01 (start of the year).
- Unused accruals are lost (no leaves are carried over).
Create an accrual allocation with this plan and allocate it to the logged-in user.
The employee will be accrued 10 days. The employee will use some of them. The carryover policy is set
to None, so no leaves will be carriedover. The remaining days of the allocation will expire.
"""
number_of_accrued_days = 10
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'can_be_carryover': True,
'carryover_date': 'other',
'carryover_day': 31,
'carryover_month': '12',
'level_ids': [
(0, 0, {
'milestone_date': 'creation',
'start_type': 'day',
'added_value': number_of_accrued_days,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'lost'
})
],
})
logged_in_emp = self.env.user.employee_id
logged_in_emp.resource_calendar_id = None # Set as Fully flexible resource
allocation = self.env['hr.leave.allocation'].sudo().create({
'date_from': date(date.today().year, 1, 1),
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
target_date = date(date.today().year + 1, 12, 30)
leave = self.env['hr.leave'].create({
'employee_id': logged_in_emp.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': target_date + relativedelta(month=12, day=1),
'request_date_to': target_date + relativedelta(month=12, day=7)
})
allocation_data = self.leave_type.get_allocation_data(
allocation.employee_id, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
allocation._get_carryover_date(target_date).strftime('%m/%d/%Y'),
"The expiration date should match the carryover date")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'],
number_of_accrued_days - leave.number_of_days,
"All the remaining days of the allocation will expire")
# Days between the target date and the expiration date (accrual_plan's carryover date)
working_days_equivalent_needed = (allocation._get_carryover_date(target_date) - target_date).days + 1
# Assert the closest allocation duration (number of working days equivalent (8 hours/day) remaining before the allocation expires)
self.assertEqual(round(allocation_data[logged_in_emp][0][1]['closest_allocation_duration']), working_days_equivalent_needed,
"The closest allocation duration should be the number of working days equivalent (24 hours/day) remaining before the allocation expires")
@users('enguerran')
def test_carried_over_days_expiration_date(self):
"""
This test case aims to assert that carried_over_days_expiration_date is taken into account when the
expiration date is computed.
- First accrual plan:
- Carryover date : 1st of April.
- Has 1 level:
- Accrues 3 days yearly on the 1st of January.
- Carryover with a maximum of 5 days.
- Second accrual plan:
Has the same definition as the one above except that carried over days are valid for 3 months.
- Note: the following dates are in format dd/mm/YYYY
- Define 2 allocations:
- Both allocations will start on 01/01/2023.
- One allocation uses the first accrual plan and the other uses the second accrual plan.
- Both allocations accrue 3 days yearly.
- The first allocation expires on the 1st of October.
- The second allocation doesn't expire.
- On 01/01/2024, both allocations will accrue 3 days for the employee.
- If target date is 01/04/2024, then the expiration date should be 01/7/2024 because on 01/04/2024, 3 days will carryover for
the second allocation and these 3 days will expire in 3 months.
"""
accrual_plan_without_accrual_validity = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).sudo().create({
'name': 'Test Accrual Plan',
'carryover_date': 'other',
'carryover_day': 1,
'carryover_month': '4',
'level_ids': [
(0, 0, {
'start_count': 0,
'start_type': 'day',
'added_value': 3,
'added_value_type': 'day',
'frequency': 'yearly',
'yearly_day': 1,
'yearly_month': '1',
'cap_accrued_time': False,
'action_with_unused_accruals': 'all',
'carryover_options': 'limited',
'postpone_max_days': 5,
})
],
})
logged_in_emp = self.env.user.employee_id
with freeze_time("2023-01-01"):
# Allocation 1
self.env['hr.leave.allocation'].sudo().create({
'date_from': '2023-01-01',
'date_to': '2024-10-01',
'allocation_type': 'accrual',
'accrual_plan_id': accrual_plan_without_accrual_validity.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
# Allocation 2
self.env['hr.leave.allocation'].sudo().create({
'date_from': '2023-01-01',
'allocation_type': 'accrual',
'accrual_plan_id': self.accrual_plan_with_accrual_validity.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
with freeze_time("2024-04-01"):
self.env['hr.leave.allocation'].with_user(self.user_hruser)._update_accrual()
target_date = date(2024, 4, 1)
allocation_data = self.leave_type.get_allocation_data(logged_in_emp, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
(target_date + relativedelta(month=7)).strftime('%m/%d/%Y'),
"The expiration date should be the carried over days expiration date of allocation 3")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'], 3)
@users('enguerran')
def test_carried_over_days_expiration_date_2(self):
"""
This tess case aims to assert that the number of expiring leaves on carried_over_days_expiration_date is
computed properly
- Define an accrual plan:
- Carryover date : 1st of April.
- Has 1 level:
- Accrues 3 days yearly on the 1st of January.
- Carryover with a maximum of 5 days.
- Carried over days are valid for 3 months.
- Note: the following dates are in format dd/mm/YYYY
- Define an allocation:
- The allocation will start on 01/01/2023.
- The allocation uses the accrual plan defined above.
- The allocation expires on the 1st of October.
- On 01/01/2024, 3 days are accrued.
- If target date is 01/05/2024, then the expiration date should be 01/7/2024.
- On 01/04/2024, 3 days will carryover.
- The employee taked 2 days as time off.
- The number of expiring days on 01/07/2024 is 1 day.
"""
logged_in_emp = self.env.user.employee_id
with freeze_time("2023-01-01"):
self.env['hr.leave.allocation'].sudo().create({
'date_from': '2023-01-01',
'allocation_type': 'accrual',
'accrual_plan_id': self.accrual_plan_with_accrual_validity.id,
'holiday_status_id': self.leave_type.id,
'employee_id': logged_in_emp.id,
'number_of_days': 0,
})
with freeze_time("2024-04-01"):
self.env['hr.leave.allocation'].with_user(self.user_hruser)._update_accrual()
leave = self.env['hr.leave'].create({
'name': 'leave',
'employee_id': logged_in_emp.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': '2024-04-03',
'request_date_to': '2024-04-04',
})
leave.sudo().action_approve()
target_date = date(2024, 5, 1)
allocation_data = self.leave_type.get_allocation_data(logged_in_emp, target_date)
# Assert the date of expiration
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_expire'],
(target_date + relativedelta(month=7)).strftime('%m/%d/%Y'),
"The expiration date should be the carried over days expiration date of allocation 3")
# Assert the number of expiring leaves
self.assertEqual(allocation_data[logged_in_emp][0][1]['closest_allocation_remaining'], 1)

View file

@ -0,0 +1,150 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime, date
from odoo.tests.common import TransactionCase
UTC = pytz.timezone('UTC')
class TestFlexibleResourceCalendar(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.calendar_40h_flex = cls.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'flexible_hours': True,
})
cls.flex_resource, cls.fully_flex_resource = cls.env['resource.resource'].create([{
'name': 'Flex',
'tz': 'UTC',
'calendar_id': cls.calendar_40h_flex.id,
}, {
'name': 'fully flex',
'tz': 'UTC',
'calendar_id': False,
}])
cls.flex_employee, cls.fully_flex_employee = cls.env['hr.employee'].create([{
'name': "flexible employee",
'date_version': date(2025, 1, 1),
'contract_date_start': date(2025, 1, 1),
'wage': 10,
'resource_calendar_id': cls.calendar_40h_flex.id,
'resource_id': cls.flex_resource.id,
}, {
'name': "fully flexible employee",
'date_version': date(2025, 1, 1),
'contract_date_start': date(2025, 1, 1),
'wage': 10,
'resource_calendar_id': False,
'resource_id': cls.fully_flex_resource.id,
}])
def test_flexible_resource_work_intervals_with_leaves(self):
self.env['resource.calendar.leaves'].create([{
'resource_id': self.flex_resource.id,
'date_from': datetime(2025, 7, 31, 8),
'date_to': datetime(2025, 8, 1, 17),
}, {
'resource_id': self.fully_flex_resource.id,
'date_from': datetime(2025, 7, 31, 8),
'date_to': datetime(2025, 8, 1, 17),
}])
custom_leave, half_day_leave = self.env['hr.leave.type'].create([{
'name': 'Custom Leave',
'requires_allocation': False,
'request_unit': 'hour',
}, {
'name': 'Half day',
'requires_allocation': False,
'request_unit': 'half_day',
}])
self.env['hr.leave'].with_context(mail_create_nolog=True, mail_notrack=True).create([{
'name': 'Half 1',
'holiday_status_id': half_day_leave.id,
'employee_id': self.flex_employee.id,
'request_date_from': date(2025, 7, 28),
'request_date_to': date(2025, 7, 28),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
}, {
'name': 'Half 2',
'holiday_status_id': half_day_leave.id,
'employee_id': self.flex_employee.id,
'request_date_from': date(2025, 7, 30),
'request_date_to': date(2025, 7, 30),
'request_date_from_period': 'pm',
'request_date_to_period': 'pm',
}, {
'name': 'Custom',
'holiday_status_id': custom_leave.id,
'employee_id': self.flex_employee.id,
'request_date_from': date(2025, 7, 29),
'request_date_to': date(2025, 7, 29),
'request_hour_from': 11.0,
'request_hour_to': 16.0,
}, {
'name': 'Half 1',
'holiday_status_id': half_day_leave.id,
'employee_id': self.fully_flex_employee.id,
'request_date_from': date(2025, 7, 28),
'request_date_to': date(2025, 7, 28),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
}, {
'name': 'Half 2',
'holiday_status_id': half_day_leave.id,
'employee_id': self.fully_flex_employee.id,
'request_date_from': date(2025, 7, 30),
'request_date_to': date(2025, 7, 30),
'request_date_from_period': 'pm',
'request_date_to_period': 'pm',
}, {
'name': 'Custom',
'holiday_status_id': custom_leave.id,
'employee_id': self.fully_flex_employee.id,
'request_date_from': date(2025, 7, 29),
'request_date_to': date(2025, 7, 29),
'request_hour_from': 11.0,
'request_hour_to': 16.0,
}]).action_approve()
start_dt = datetime(2025, 7, 28).astimezone(UTC)
end_dt = datetime(2025, 8, 3, 17).astimezone(UTC)
resources = self.flex_resource | self.fully_flex_resource
work_intervals, hours_per_day, hours_per_week = resources._get_flexible_resource_valid_work_intervals(start_dt, end_dt)
self.maxDiff = None
for resource in resources:
self.assertEqual(work_intervals[resource.id]._items, [
(datetime(2025, 7, 28, 12, 0, tzinfo=UTC), datetime(2025, 7, 28, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 29, 0, 0, tzinfo=UTC), datetime(2025, 7, 29, 11, 0, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 29, 16, 0, tzinfo=UTC), datetime(2025, 7, 29, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 30, 0, 0, tzinfo=UTC), datetime(2025, 7, 30, 12, 0, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 2, 0, 0, tzinfo=UTC), datetime(2025, 8, 2, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 3, 0, 0, tzinfo=UTC), datetime(2025, 8, 3, 17, 0, tzinfo=UTC), self.env['resource.calendar.attendance']),
], "resource not available on 29, 31, 01, 28 morning and 30 afternoon. for other days, resource can do his hours at any moment of the day (from 00:00:00 to 23:59:59)")
self.assertDictEqual(hours_per_day[self.flex_resource.id], {
date(2025, 7, 28): 4.0,
date(2025, 7, 29): 3.0,
date(2025, 7, 30): 4.0,
date(2025, 7, 31): 0.0,
date(2025, 8, 1): 0.0,
date(2025, 8, 2): 8.0,
date(2025, 8, 3): 8.0,
}, "hours_per_day/2 for half days off, and hours_per_day - number_of_hoursfor custom time off")
self.assertTrue(self.fully_flex_resource.id not in hours_per_day)
self.assertDictEqual(hours_per_week[self.flex_resource.id], {
(2025, 31): 11.0,
(2025, 32): 40.0,
}, "week 31 (27/07 -> 02/08): 2 days off 31 & 01 (-16 hours), half day on 28 and 30 (-8 hours), 5 hours off on day 29 / hours = 40-(16+8+5) = 11 hours, no timeoff on week 32")
self.assertTrue(self.fully_flex_resource.id not in hours_per_week)

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from datetime import date, datetime, timedelta
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.exceptions import ValidationError
from freezegun import freeze_time
@ -21,14 +22,19 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
'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'})
]
})
@ -47,13 +53,13 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
})
cls.global_leave = cls.env['resource.calendar.leaves'].create({
'name': 'Global Leave',
'name': 'Global Time Off',
'date_from': date(2022, 3, 7),
'date_to': date(2022, 3, 7),
})
cls.calendar_leave = cls.env['resource.calendar.leaves'].create({
'name': 'Global Leave',
'name': 'Global Time Off',
'date_from': date(2022, 3, 8),
'date_to': date(2022, 3, 8),
'calendar_id': cls.calendar_1.id,
@ -62,7 +68,7 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
def test_leave_on_global_leave(self):
with self.assertRaises(ValidationError):
self.env['resource.calendar.leaves'].create({
'name': 'Wrong Leave',
'name': 'Wrong Time Off',
'date_from': date(2022, 3, 7),
'date_to': date(2022, 3, 7),
'calendar_id': self.calendar_1.id,
@ -70,72 +76,14 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
with self.assertRaises(ValidationError):
self.env['resource.calendar.leaves'].create({
'name': 'Wrong Leave',
'name': 'Wrong Time Off',
'date_from': date(2022, 3, 7),
'date_to': date(2022, 3, 7),
})
def test_leave_on_deleted_global_leave(self):
public_leave = self.env['resource.calendar.leaves'].create({
'name': 'Public Time Off',
'date_from': datetime(2024, 2, 20, 0, 0),
'date_to': datetime(2024, 2, 22, 23, 59),
'company_id': self.employee_emp.company_id.id,
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'requires_allocation': 'yes',
'employee_requests': 'no',
'allocation_validation_type': 'no',
'leave_validation_type': 'both',
'responsible_id': self.user_hrmanager_id,
})
allocation = self.env['hr.leave.allocation'].create({
'employee_id': self.employee_emp_id,
'name': '2 days allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 2,
'state': 'confirm',
'date_from': date(2024, 2, 1),
'date_to': date(2024, 2, 29),
})
allocation.action_validate()
covered_leave_1 = self.env['hr.leave'].create({
'name': 'Covered Leave',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type.id,
'date_from': datetime(2024, 2, 19, 7, 0),
'date_to': datetime(2024, 2, 20, 18, 0),
})
self.assertEqual(covered_leave_1.number_of_days, 1, 'The leave should have a duration of 1 day.')
covered_leave_2 = self.env['hr.leave'].create({
'name': 'Covered Leave',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type.id,
'date_from': datetime(2024, 2, 21, 7, 0),
'date_to': datetime(2024, 2, 21, 18, 0),
})
self.assertEqual(covered_leave_2.number_of_days, 0, 'The leave should have a duration of 0 days.')
covered_leave_3 = self.env['hr.leave'].create({
'name': 'Covered Leave',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type.id,
'date_from': datetime(2024, 2, 22, 7, 0),
'date_to': datetime(2024, 2, 23, 18, 0),
})
self.assertEqual(covered_leave_3.number_of_days, 1, 'The leave should have a duration of 1 day.')
public_leave.unlink()
self.assertEqual(covered_leave_1.active, True, 'The partially covered leave should still be active.')
self.assertEqual(covered_leave_1.number_of_days, 1, 'The leave should have a duration of 1 day.')
self.assertEqual(covered_leave_2.active, False, 'The covered leave should be archived.')
self.assertEqual(covered_leave_3.active, True, 'The partially covered leave should still be active.')
self.assertEqual(covered_leave_3.number_of_days, 1, 'The leave should have a duration of 1 day.')
def test_leave_on_calendar_leave(self):
self.env['resource.calendar.leaves'].create({
'name': 'Correct Leave',
'name': 'Correct Time Off',
'date_from': date(2022, 3, 8),
'date_to': date(2022, 3, 8),
'calendar_id': self.calendar_2.id,
@ -143,14 +91,14 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
with self.assertRaises(ValidationError):
self.env['resource.calendar.leaves'].create({
'name': 'Wrong Leave',
'name': 'Wrong Time Off',
'date_from': date(2022, 3, 8),
'date_to': date(2022, 3, 8),
})
with self.assertRaises(ValidationError):
self.env['resource.calendar.leaves'].create({
'name': 'Wrong Leave',
'name': 'Wrong Time Off',
'date_from': date(2022, 3, 8),
'date_to': date(2022, 3, 8),
'calendar_id': self.calendar_1.id,
@ -184,6 +132,84 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
# The user in Europe/Brussels timezone see 4:30 and not 2:30 because he is in UTC +02:00.
# The user in Asia/Kolkata timezone (determined via the browser) see 8:00 because he is in UTC +05:30
def test_global_leave_working_schedule_without_company(self):
"""
Check public holidays for a company apply to employees of this company
when using a working schedule without a company.
"""
calendar_no_company = self.env['resource.calendar'].create({
'name': 'Schedule without company',
'company_id': False,
})
self.employee_emp.resource_calendar_id = calendar_no_company
self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday',
'date_from': datetime(2024, 1, 3, 0, 0),
'date_to': datetime(2024, 1, 3, 23, 59),
'calendar_id': calendar_no_company.id,
'company_id': self.employee_emp.company_id.id,
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'time_type': 'leave',
'requires_allocation': False,
})
leave = self.env['hr.leave'].create({
'name': 'Time Off',
'employee_id': self.employee_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': date(2024, 1, 2),
'request_date_to': date(2024, 1, 4),
})
self.assertEqual(leave.number_of_days, 2, "Public holiday duration should not be included")
def test_global_leave_number_of_days_with_new(self):
"""
Check that leaves stored in memory (and not in the database)
take into account global leaves.
"""
global_leave = self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off',
'date_from': datetime(2024, 1, 3, 6, 0, 0),
'date_to': datetime(2024, 1, 3, 19, 0, 0),
'calendar_id': self.calendar_1.id,
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'time_type': 'leave',
'requires_allocation': False,
})
self.employee_emp.resource_calendar_id = self.calendar_1.id
leave = self.env['hr.leave'].create({
'name': 'Test new leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': global_leave.date_from,
'request_date_to': global_leave.date_to,
})
self.assertEqual(leave.number_of_days, 0, 'It is a global leave')
leave = self.env['hr.leave'].new({
'name': 'Test new leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': global_leave.date_from,
'request_date_to': global_leave.date_to,
})
self.assertEqual(leave.number_of_days, 0, 'It is a global leave')
leave = self.env['hr.leave'].new({
'name': 'Test new leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': global_leave.date_from - timedelta(days=1),
'request_date_to': global_leave.date_to + timedelta(days=1),
})
self.assertEqual(leave.number_of_days, 2, 'There is a global leave')
@freeze_time('2024-12-01')
def test_global_leave_keeps_employee_resource_leave(self):
"""
@ -191,37 +217,35 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
if the employee's leave is not fully covered by the global leave, the employee's leave
should still have resource leaves linked to it.
"""
employee = self.employee_emp
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'requires_allocation': 'yes',
'employee_requests': 'no',
'allocation_validation_type': 'no',
'request_unit': 'hour',
'leave_validation_type': 'both',
'responsible_id': self.user_hrmanager_id,
})
allocation = self.env['hr.leave.allocation'].create({
'employee_id': self.employee_emp_id,
'name': '5 days allocation',
self.env['hr.leave.allocation'].create({
'name': '20 days allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 5,
'number_of_days': 20,
'employee_id': employee.id,
'state': 'confirm',
'date_from': date(2024, 12, 1),
'date_to': date(2024, 12, 30),
})
allocation.action_validate()
}).action_approve()
partially_covered_leave = self.env['hr.leave'].create({
'name': 'Covered Leave',
'employee_id': self.employee_emp_id,
'name': 'Holiday 1 week',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': datetime(2024, 12, 3, 7, 0),
'date_to': datetime(2024, 12, 5, 18, 0),
'request_date_from': datetime(2024, 12, 3, 7, 0),
'request_date_to': datetime(2024, 12, 5, 18, 0),
})
partially_covered_leave.action_validate()
partially_covered_leave.action_approve()
global_leave = self.env['resource.calendar.leaves'].with_user(self.env.user).create({
'name': 'Public holiday',
'date_from': "2024-12-4 06:00:00",
'date_to': "2024-12-4 23:00:00",
'date_from': "2024-12-04 06:00:00",
'date_to': "2024-12-04 23:00:00",
'calendar_id': self.calendar_1.id,
})
@ -230,3 +254,156 @@ class TestGlobalLeaves(TestHrHolidaysCommon):
('holiday_id', '=', partially_covered_leave.id)
])
self.assertTrue(resource_leaves, 'Resource leaves linked to the employee leave should exist.')
@freeze_time('2025-05-11')
def test_employee_leave_with_global_leave(self):
"""
When an employee's leave is created, if there are any public holidays within the leave period,
the number of leave days is reduced accordingly.
eg,.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Leave Requested | Leave State | Public Holiday days | # days leave remains |
|---------------------------------------------------------------------------------|
| 5 Days | confirm | 1 Days | 4 Days |
|---------------------------------------------------------------------------------|
| 4 Days | validate1 | 1 Days | 3 Days |
|---------------------------------------------------------------------------------|
| 3 Days | validate | 1 Days | 2 Days |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
user_david = mail_new_test_user(self.env, login='david', groups='base.group_user')
user_timeoff_officer_david = mail_new_test_user(self.env, login='timeoff_officer', groups='base.group_user')
employee_david = self.env['hr.employee'].create({
'name': 'David Employee',
'user_id': user_david.id,
'leave_manager_id': user_timeoff_officer_david.id,
'parent_id': self.employee_hruser.id,
'department_id': self.rd_dept.id,
'resource_calendar_id': self.calendar_1.id,
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Sick Time Off',
'time_type': 'leave',
'requires_allocation': False,
'leave_validation_type': 'both',
})
employee_leave = self.env['hr.leave'].create({
'name': 'Holiday 5 days',
'employee_id': employee_david.id,
'holiday_status_id': leave_type.id,
'request_date_from': datetime(2025, 5, 12),
'request_date_to': datetime(2025, 5, 16),
})
self.env['resource.calendar.leaves'].with_user(self.user_hrmanager).create({
'name': 'Public holiday day 1',
'date_from': datetime(2025, 5, 13),
'date_to': datetime(2025, 5, 13, 23, 59),
'calendar_id': employee_david.resource_calendar_id.id,
})
self.assertEqual(employee_leave.number_of_days, 4, 'Leave duration should be reduced because of public holiday day 1')
employee_leave.with_user(user_timeoff_officer_david).action_approve()
self.env['resource.calendar.leaves'].with_user(self.user_hrmanager).create({
'name': 'Public holiday day 2',
'date_from': datetime(2025, 5, 14),
'date_to': datetime(2025, 5, 14, 23, 59),
'calendar_id': employee_david.resource_calendar_id.id,
})
self.assertEqual(employee_leave.number_of_days, 3, 'Leave duration should be reduced because of public holiday day 2')
employee_leave.with_user(self.user_hruser).action_approve()
self.env['resource.calendar.leaves'].with_user(self.user_hrmanager).create({
'name': 'Public holiday day 3',
'date_from': datetime(2025, 5, 15),
'date_to': datetime(2025, 5, 15, 23, 59),
'calendar_id': employee_david.resource_calendar_id.id,
})
self.assertEqual(employee_leave.number_of_days, 2, 'Leave duration should be reduced because of public holiday day 3')
def test_multi_day_public_holidays_for_flexible_schedule(self):
"""
Test that _get_unusual_days return correct value for
multi-day holidays in flexible schedules
"""
flex_cal = self.env['resource.calendar'].create({
'name': 'Flexible', 'tz': 'UTC', 'flexible_hours': True, 'hours_per_day': 8.0
})
# tuesday to thursday
self.env['resource.calendar.leaves'].create({
'name': '3 day holiday', 'calendar_id': flex_cal.id,
'date_from': datetime(2024, 3, 5), 'date_to': date(2024, 3, 7)
})
# monday to saturday
start = datetime(2024, 3, 4)
end = datetime(2024, 3, 10)
flex_days = flex_cal._get_unusual_days(start, end)
expected = {
'2024-03-04': False,
'2024-03-05': True,
'2024-03-06': True,
'2024-03-07': True,
'2024-03-08': False,
'2024-03-09': False,
'2024-03-10': False,
}
for day, value in expected.items():
self.assertEqual(flex_days.get(day), value, f"Day {day} should be {'unusual' if value else 'normal'}")
def test_public_holidays_for_consecutive_allocations(self):
employee = self.employee_emp
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'time_type': 'leave',
'requires_allocation': 'yes',
})
self.env['hr.leave.allocation'].create([
{
'name': '2025 allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 20,
'employee_id': employee.id,
'state': 'confirm',
'date_from': date(2025, 1, 1),
'date_to': date(2025, 12, 31),
},
{
'name': '2026 allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 20,
'employee_id': employee.id,
'state': 'confirm',
'date_from': date(2026, 1, 1),
'date_to': date(2026, 12, 31),
}
]).action_approve()
leave = self.env['hr.leave'].create({
'name': 'Holiday 1 week',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': datetime(2025, 12, 8, 7, 0),
'request_date_to': datetime(2026, 1, 3, 18, 0),
})
leave.action_approve()
self.assertEqual(leave.number_of_days, 20, "Number of days should be 20")
public_holiday = self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off',
'date_from': datetime(2025, 12, 31, 23, 0, 0),
'date_to': datetime(2026, 1, 1, 22, 59, 59),
})
self.assertTrue(public_holiday)
self.assertEqual(leave.number_of_days, 19, "Number of days should be 19 as one day has been granted back to the"
"the employee for the public holiday")

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, timedelta
from odoo.addons.base.tests.common import HttpCase
from odoo.tests.common import tagged
@ -8,23 +8,96 @@ from odoo.tests.common import users
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('post_install', '-at_install')
@tagged('post_install', '-at_install', 'holiday_calendar')
class TestHolidaysCalendar(HttpCase, TestHrHolidaysCommon):
@users('admin')
@users('enguerran')
def test_hours_time_off_request_calendar_view(self):
"""
Testing the flow of clicking on a day, save the leave request directly
and verify that the start/end time are correctly set
and verify that the start/end time are correctly set.
"""
self.env.user.tz = 'UTC'
calendar = self.env.user.employee_id.resource_calendar_id.attendance_ids
expected_start_thursday = calendar[6].hour_from
expected_end_thursday = calendar[7].hour_to
first_day_of_year = date(date.today().year, 1, 1)
days_to_thursday = (3 - first_day_of_year.weekday()) % 7
first_thursday_of_year = first_day_of_year + timedelta(days=days_to_thursday)
self.start_tour('/', 'time_off_request_calendar_view', login='admin')
leave = self.env['hr.leave'].new({
'name': 'Reference Holiday',
'employee_id': self.employee_emp.id,
'request_date_from': first_thursday_of_year,
'request_date_to': first_thursday_of_year,
})
leave._compute_date_from_to()
expected_leave_start = leave.date_from.hour
expected_leave_end = leave.date_to.hour
last_leave = self.env['hr.leave'].search([('employee_id.id', '=', self.env.user.employee_id.id)]).sorted(lambda leave: leave.create_date)[-1]
# Tour that takes a leave on the first thursday of the year.
self.start_tour('/', 'time_off_request_calendar_view', login='enguerran')
last_leave = self.env['hr.leave'].search([('employee_id.id', '=', self.employee_emp.id)]).sorted(lambda leave: leave.create_date)[-1]
self.assertEqual(last_leave.date_from.weekday(), 3, "It should be Thursday")
self.assertEqual(last_leave.date_from.hour, expected_start_thursday, "Wrong start of the day")
self.assertEqual(last_leave.date_to.hour, expected_end_thursday, "Wrong end of the day")
self.assertEqual(last_leave.date_from.hour, expected_leave_start, "Wrong start of the day")
self.assertEqual(last_leave.date_to.hour, expected_leave_end, "Wrong end of the day")
def test_timezone_calendar_event_single_day(self):
"""
Test that single-day time off requests have a single day display in calendar
"""
leave_type, leave_type_half = self.env['hr.leave.type'].create([
{
'name': 'Test Leave Type',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
'create_calendar_meeting': True,
},
{
'name': 'Test Leave Type Half Day',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
'create_calendar_meeting': True,
'request_unit': 'half_day',
},
])
# case 1: full day in Los/Angeles tz
test_date = date(2025, 4, 22)
self.employee_emp.user_id.tz = 'America/Los_Angeles'
self.employee_emp.resource_calendar_id.tz = 'America/Los_Angeles'
leave = self.env['hr.leave'].create({
'name': 'Single Day Leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': test_date,
'request_date_to': test_date,
})
leave.action_approve()
self.assertEqual(leave.meeting_id.allday, True)
self.assertEqual(leave.meeting_id.start_date, test_date,
f"Meeting start date should be {test_date}")
self.assertEqual(leave.meeting_id.stop_date, test_date,
f"Meeting end date should be {test_date}")
# case 2: half day in Los/Angeles tz
test_date_half = date(2025, 4, 23)
leave_half = self.env['hr.leave'].create({
'name': 'Half Day Leave LA',
'employee_id': self.employee_emp.id,
'holiday_status_id': leave_type_half.id,
'request_date_from': test_date_half,
'request_date_to': test_date_half,
'request_date_from_period': 'pm',
'request_date_to_period': 'pm',
})
leave_half.action_approve()
self.assertEqual(leave_half.meeting_id.allday, False)
self.assertEqual(leave_half.meeting_id.start, leave_half.date_from)
self.assertEqual(leave_half.meeting_id.stop, leave_half.date_to)

View file

@ -2,14 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from datetime import datetime
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from psycopg2 import IntegrityError
from odoo import fields
from odoo.exceptions import AccessError, ValidationError, UserError
from odoo.tools import mute_logger, test_reports
from odoo import Command
from odoo.tools import date_utils, mute_logger, test_reports
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@ -26,29 +25,29 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
HolidayStatusManagerGroup = HolidaysStatus.with_user(self.user_hrmanager_id)
HolidayStatusManagerGroup.create({
'name': 'WithMeetingType',
'requires_allocation': 'no',
'requires_allocation': False,
})
self.holidays_status_hr = HolidayStatusManagerGroup.create({
'name': 'NotLimitedHR',
'requires_allocation': 'no',
'requires_allocation': False,
'leave_validation_type': 'hr',
})
self.holidays_status_manager = HolidayStatusManagerGroup.create({
'name': 'NotLimitedManager',
'requires_allocation': 'no',
'requires_allocation': False,
'leave_validation_type': 'manager',
})
HolidaysEmployeeGroup = Requests.with_user(self.user_employee_id)
# Employee creates a leave request in a no-limit category hr manager only
leave_date = date_utils.start_of((date.today() - relativedelta(days=1)), 'week')
hol1_employee_group = HolidaysEmployeeGroup.create({
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_hr.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
'request_date_from': leave_date,
'request_date_to': leave_date,
})
hol1_user_group = hol1_employee_group.with_user(self.user_hruser_id)
hol1_manager_group = hol1_employee_group.with_user(self.user_hrmanager_id)
@ -59,13 +58,13 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
self.assertEqual(hol1_manager_group.state, 'validate', 'hr_holidays: validated leave request should be in validate state')
# Employee creates a leave request in a no-limit category department manager only
leave_date = date_utils.start_of(date.today() + relativedelta(days=11), 'week')
hol12_employee_group = HolidaysEmployeeGroup.create({
'name': 'Hol12',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_manager.id,
'date_from': (datetime.today() + relativedelta(days=12)),
'date_to': (datetime.today() + relativedelta(days=13)),
'number_of_days': 1,
'request_date_from': leave_date,
'request_date_to': leave_date,
})
hol12_user_group = hol12_employee_group.with_user(self.user_hruser_id)
hol12_manager_group = hol12_employee_group.with_user(self.user_hrmanager_id)
@ -75,7 +74,6 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
hol12_manager_group.action_approve()
self.assertEqual(hol1_user_group.state, 'validate', 'hr_holidays: validates leave request should be in validate state')
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_01_leave_request_flow_limited(self):
""" Testing leave request flow: limited type of leave request """
@ -84,13 +82,15 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
Allocations = self.env['hr.leave.allocation']
HolidaysStatus = self.env['hr.leave.type']
self.env.ref('hr.employee_admin').tz = "Europe/Brussels"
holiday_status_paid_time_off = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'requires_allocation': 'yes',
'employee_requests': 'no',
'allocation_validation_type': 'officer',
'requires_allocation': True,
'employee_requests': False,
'allocation_validation_type': 'hr',
'leave_validation_type': 'both',
'responsible_id': self.env.ref('base.user_admin').id,
'responsible_ids': [Command.link(self.env.ref('base.user_admin').id)],
})
self.env['hr.leave.allocation'].create([
@ -102,38 +102,40 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
'state': 'confirm',
'date_from': time.strftime('%Y-%m-01'),
}, {
'name': 'Paid Time off for David',
'name': 'Paid Time off for Admin',
'holiday_status_id': holiday_status_paid_time_off.id,
'number_of_days': 20,
'employee_id': self.ref('hr.employee_admin'),
'state': 'confirm',
'date_from': time.strftime('%Y-%m-01'),
}
]).action_validate()
]).action_approve()
def _check_holidays_status(holiday_status, ml, lt, rl, vrl):
self.assertEqual(holiday_status.max_leaves, ml,
def _check_holidays_status(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(holiday_status.leaves_taken, lt,
self.assertEqual(result['leaves_taken'], lt,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.remaining_leaves, rl,
self.assertEqual(result['remaining_leaves'], rl,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.virtual_remaining_leaves, vrl,
self.assertEqual(result['virtual_remaining_leaves'], vrl,
'hr_holidays: wrong type days computation')
# HrManager creates some holiday statuses
HolidayStatusManagerGroup = HolidaysStatus.with_user(self.user_hrmanager_id)
HolidayStatusManagerGroup.create({
'name': 'WithMeetingType',
'requires_allocation': 'no',
'requires_allocation': False,
})
self.holidays_status_limited = HolidayStatusManagerGroup.create({
'name': 'Limited',
'requires_allocation': 'yes',
'employee_requests': 'no',
'allocation_validation_type': 'officer',
'requires_allocation': True,
'employee_requests': False,
'allocation_validation_type': 'hr',
'leave_validation_type': 'both',
'responsible_ids': [Command.link(self.env.ref('base.user_admin').id)]
})
HolidaysEmployeeGroup = Requests.with_user(self.user_employee_id)
@ -147,33 +149,35 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
'date_from': time.strftime('%Y-%m-01'),
})
# HrUser validates the first step
self.env.flush_all()
# HrManager validates the second step
aloc1_user_group.with_user(self.user_hrmanager_id).action_validate()
aloc1_user_group.with_user(self.user_hrmanager_id).action_approve()
# Checks Employee has effectively some days left
hol_status_2_employee_group = self.holidays_status_limited.with_user(self.user_employee_id)
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0)
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 0.0, 2.0, 2.0)
# Employee creates a leave request in the limited category, now that he has some days left
hol2 = HolidaysEmployeeGroup.create({
'name': 'Hol22',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_limited.id,
'date_from': (datetime.today() + relativedelta(days=2)).strftime('%Y-%m-%d %H:%M'),
'date_to': (datetime.today() + relativedelta(days=3)),
'number_of_days': 1,
'request_date_from': (date.today() + relativedelta(days=2)),
'request_date_to': (date.today() + relativedelta(days=2)),
})
self.env.flush_all()
hol2_user_group = hol2.with_user(self.user_hruser_id)
# Check left days: - 1 virtual remaining day
hol_status_2_employee_group.invalidate_model()
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 1.0)
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 0.0, 2.0, 1.0)
# HrManager validates the second step
hol2_user_group.with_user(self.user_hrmanager_id).action_validate()
hol2_user_group.with_user(self.user_hrmanager_id).action_approve()
self.assertEqual(hol2.state, 'validate',
'hr_holidays: second validation should lead to validate state')
# Check left days: - 1 day taken
_check_holidays_status(hol_status_2_employee_group, 2.0, 1.0, 1.0, 1.0)
hol_status_2_employee_group.invalidate_model(['max_leaves', 'leaves_taken'])
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 1.0, 1.0, 1.0)
# HrManager finds an error: he refuses the leave request
hol2.with_user(self.user_hrmanager_id).action_refuse()
@ -182,17 +186,11 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
# Check left days: 2 days left again
hol_status_2_employee_group.invalidate_model(['max_leaves'])
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0)
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 0.0, 2.0, 2.0)
self.assertEqual(hol2.state, 'refuse',
'hr_holidays: hr_user should not be able to reset a refused leave request')
# HrManager resets the request
hol2_manager_group = hol2.with_user(self.user_hrmanager_id)
hol2_manager_group.action_draft()
self.assertEqual(hol2.state, 'draft',
'hr_holidays: resetting should lead to draft state')
employee_id = self.ref('hr.employee_admin')
# cl can be of maximum 20 days for employee_admin
hol3_status = holiday_status_paid_time_off.with_context(employee_id=employee_id)
@ -200,24 +198,19 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
hol3 = Requests.create({
'name': 'Sick Time Off',
'holiday_status_id': hol3_status.id,
'date_from': datetime.today().strftime('%Y-%m-10 10:00:00'),
'date_to': datetime.today().strftime('%Y-%m-11 19:00:00'),
'request_date_from': date.today() + relativedelta(day=10),
'request_date_to': date.today() + relativedelta(day=10),
'employee_id': employee_id,
'number_of_days': 1,
})
# I find a small mistake on my leave request to I click on "Refuse" button to correct a mistake.
hol3.action_refuse()
self.assertEqual(hol3.state, 'refuse', 'hr_holidays: refuse should lead to refuse state')
# I again set to draft and then confirm.
hol3.action_draft()
self.assertEqual(hol3.state, 'draft', 'hr_holidays: resetting should lead to draft state')
hol3.action_confirm()
self.assertEqual(hol3.state, 'confirm', 'hr_holidays: confirming should lead to confirm state')
# I validate the holiday request by clicking on "To Approve" button.
hol3.action_validate()
# Validate it again
hol3.action_approve()
self.assertEqual(hol3.state, 'validate', 'hr_holidays: validation should lead to validate state')
# Check left days for casual leave: 19 days left
_check_holidays_status(hol3_status, 20.0, 1.0, 19.0, 19.0)
_check_holidays_status(hol3_status, self.env['hr.employee'].browse(employee_id), 20.0, 1.0, 19.0, 19.0)
def test_10_leave_summary_reports(self):
# Print the HR Holidays(Summary Employee) Report through the wizard
@ -240,11 +233,11 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
holiday_status_paid_time_off = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'requires_allocation': 'yes',
'employee_requests': 'no',
'allocation_validation_type': 'officer',
'requires_allocation': True,
'employee_requests': False,
'allocation_validation_type': 'hr',
'leave_validation_type': 'both',
'responsible_id': self.env.ref('base.user_admin').id,
'responsible_ids': [Command.link(self.env.ref('base.user_admin').id)],
})
self.env['hr.leave.allocation'].create({
@ -255,34 +248,29 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
'state': 'confirm',
'date_from': time.strftime('%Y-%m-01'),
'date_to': time.strftime('%Y-12-31'),
}).action_validate()
}).action_approve()
leave_vals = {
'name': 'Sick Time Off',
'holiday_status_id': holiday_status_paid_time_off.id,
'date_from': datetime.today().strftime('%Y-%m-11 19:00:00'),
'date_to': datetime.today().strftime('%Y-%m-10 10:00:00'),
'request_date_from': date.today() + relativedelta(day=11),
'request_date_to': date.today() + relativedelta(day=10),
'employee_id': self.ref('hr.employee_admin'),
'number_of_days': 1,
}
with mute_logger('odoo.sql_db'):
with self.assertRaises(IntegrityError):
with self.cr.savepoint():
self.env['hr.leave'].create(leave_vals)
with mute_logger('odoo.sql_db'), self.assertRaises(IntegrityError):
self.env['hr.leave'].create(leave_vals)
leave_vals = {
'name': 'Sick Time Off',
'holiday_status_id': holiday_status_paid_time_off.id,
'date_from': datetime.today().strftime('%Y-%m-10 10:00:00'),
'date_to': datetime.today().strftime('%Y-%m-11 19:00:00'),
'request_date_from': date.today() + relativedelta(day=10),
'request_date_to': date.today() + relativedelta(day=11),
'employee_id': self.ref('hr.employee_admin'),
'number_of_days': 1,
}
leave = self.env['hr.leave'].create(leave_vals)
with mute_logger('odoo.sql_db'):
with self.assertRaises(IntegrityError): # No ValidationError
with self.cr.savepoint():
leave.write({
'date_from': datetime.today().strftime('%Y-%m-11 19:00:00'),
'date_to': datetime.today().strftime('%Y-%m-10 10:00:00'),
})
with mute_logger('odoo.sql_db'), self.assertRaises(IntegrityError):
leave.write({
'request_date_from': date.today() + relativedelta(day=11),
'request_date_to': date.today() + relativedelta(day=10),
})

View file

@ -0,0 +1,68 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from datetime import date
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import Command
from odoo.tools import mute_logger
from .common import TestHrHolidaysCommon
from odoo.addons.mail.tests.common import MailCase
class TestHolidaysMail(TestHrHolidaysCommon, MailCase):
"""Test that mails are correctly sent when a timeoff is taken"""
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_email_sent_when_approved(self):
""" Testing leave request flow: limited type of leave request """
with freeze_time('2022-01-15'):
self.env.ref('hr.employee_admin').tz = "Europe/Brussels"
holiday_status_paid_time_off = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'requires_allocation': True,
'employee_requests': False,
'allocation_validation_type': 'hr',
'leave_validation_type': 'both',
'responsible_ids': [Command.link(self.env.ref('base.user_admin').id)],
})
self.env['hr.leave.allocation'].create([
{
'name': 'Paid Time off for David',
'holiday_status_id': holiday_status_paid_time_off.id,
'number_of_days': 20,
'employee_id': self.employee_emp_id,
'state': 'confirm',
'date_from': time.strftime('%Y-%m-01'),
}
]).action_approve()
self.env['hr.leave.allocation'].create([
{
'name': 'Paid Time off for Mitchell',
'holiday_status_id': holiday_status_paid_time_off.id,
'number_of_days': 20,
'employee_id': self.ref('hr.employee_admin'),
'state': 'confirm',
'date_from': time.strftime('%Y-%m-01'),
},
]).action_approve()
leave_vals = {
'name': 'Sick Time Off',
'holiday_status_id': holiday_status_paid_time_off.id,
'request_date_from': date.today() + relativedelta(day=2),
'request_date_to': date.today() + relativedelta(day=3),
'employee_id': self.ref('hr.employee_admin'),
}
leave = self.env['hr.leave'].create(leave_vals)
leave.action_approve()
with self.mock_mail_gateway():
leave.action_approve()
admin_emails = self._new_mails.filtered(lambda x: x.partner_ids.employee_ids.id == self.ref('hr.employee_admin'))
self.assertEqual(len(admin_emails), 1, "Mitchell Admin should receive an email")
self.assertTrue("has been accepted" in admin_emails.preview)

View file

@ -0,0 +1,144 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, timedelta
from odoo import Command
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
class TestHolidaysFlow(TestHrHolidaysCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.employee = cls.env['hr.employee'].create({
'name': 'Sky'
})
cls.departure_date = date.today()
departure_reason = cls.env['hr.departure.reason'].create({'name': "Fired"})
cls.departure_wizard = cls.env['hr.departure.wizard'].create({
'departure_reason_id': departure_reason.id,
'departure_date': cls.departure_date,
'employee_ids': [Command.link(cls.employee.id)],
})
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'time_type': 'leave',
'requires_allocation': False,
})
def test_departure_without_leave_and_allocation_employee(self):
self._check_action_departure()
def test_departure_leave_before_departure_date(self):
leave = self.env['hr.leave'].with_context(leave_fast_create=True).create({
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': self.departure_date + timedelta(days=-6),
'request_date_to': self.departure_date,
})
leave.state = 'validate'
self._check_action_departure()
def test_departure_leave_after_departure_date(self):
leave = self.env['hr.leave'].with_context(leave_fast_create=True).create({
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': self.departure_date + timedelta(days=6),
'request_date_to': self.departure_date + timedelta(days=8),
})
leave.state = 'validate'
self._check_action_departure()
def test_departure_leave_with_departure_date(self):
leave = self.env['hr.leave'].with_context(leave_fast_create=True).create({
'employee_id': self.employee.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': self.departure_date + timedelta(days=-6),
'request_date_to': self.departure_date + timedelta(days=8),
})
leave.state = 'validate'
self._check_action_departure()
message = "<p>End date has been updated because the employee will leave the company on %(departure_date)s.</p>" % {
'departure_date': self.departure_date}
self.assertTrue(message in leave.message_ids.mapped('body'))
cancel_message = "<p>The time off request has been cancelled for the following reason:</p><p>The employee will leave the company on %(departure_date)s.</p>" % {
'departure_date': self.departure_date
}
self.assertTrue(cancel_message in self.env['hr.leave'].search([
('request_date_from', '=', self.departure_date + timedelta(days=1)),
('request_date_to', '=', self.departure_date + timedelta(days=8)),
("employee_id", "=", self.employee.id)
]).message_ids.mapped('body'))
def test_departure_allocation_before_departure_date(self):
self.env['hr.leave.allocation'].create([{
'name': 'allocation',
'holiday_status_id': self.leave_type.id,
'number_of_days': 15,
'employee_id': self.employee.id,
'state': 'confirm',
'date_from': self.departure_date + timedelta(days=-10),
'date_to': self.departure_date,
}]).action_approve()
self._check_action_departure()
def test_departure_allocation_after_departure_date(self):
self.env['hr.leave.allocation'].create([{
'name': 'allocation',
'holiday_status_id': self.leave_type.id,
'number_of_days': 15,
'employee_id': self.employee.id,
'state': 'confirm',
'date_from': self.departure_date + timedelta(days=1),
'date_to': self.departure_date + timedelta(days=10),
}]).action_approve()
self._check_action_departure()
def test_departure_allocation_with_departure_date(self):
allocation = self.env['hr.leave.allocation'].create([{
'name': 'allocation',
'holiday_status_id': self.leave_type.id,
'number_of_days': 15,
'employee_id': self.employee.id,
'state': 'confirm',
'date_from': self.departure_date + timedelta(days=-10),
'date_to': self.departure_date + timedelta(days=10),
}])
allocation.action_approve()
self._check_action_departure()
allocation_msg = '<p>Validity End date has been updated because the employee will leave the company on %(departure_date)s.</p>' % {
'departure_date': self.departure_date
}
self.assertTrue(allocation_msg in allocation.message_ids.mapped('body'))
def _check_action_departure(self):
self.departure_wizard.action_register_departure()
self._check_employee_allocation()
self._check_employee_leave()
def _check_employee_leave(self):
leaves_after_departure_date = self.env['hr.leave'].search([
('employee_id', '=', self.employee.id),
('date_from', '>', self.departure_date),
('state', '!=', 'cancel')
])
self.assertFalse(leaves_after_departure_date)
leaves_before_departure_date = self.env['hr.leave'].search([
('employee_id', '=', self.employee.id),
('date_from', '<=', self.departure_date),
])
self.assertFalse(any(leave.date_to.date() > self.departure_date for leave in leaves_before_departure_date))
def _check_employee_allocation(self):
allocations = self.env['hr.leave.allocation'].search([
('employee_id', '=', self.employee.id),
'|',
('date_from', '>', self.departure_date),
('date_to', '>', self.departure_date),
])
self.assertFalse(allocations)

View file

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from datetime import date
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import ValidationError, UserError
from .common import TestHrHolidaysCommon
@ -15,22 +14,21 @@ class TestHrHolidaysCancelLeave(TestHrHolidaysCommon):
def setUpClass(cls):
super().setUpClass()
leave_start_datetime = datetime(2018, 2, 5, 7, 0, 0, 0) # this is monday
leave_end_datetime = leave_start_datetime + relativedelta(days=3)
leave_start_date = date(2018, 2, 5) # this is monday
leave_end_date = leave_start_date + relativedelta(days=2)
cls.hr_leave_type = cls.env['hr.leave.type'].with_user(cls.user_hrmanager).create({
'name': 'Leave Type',
'requires_allocation': 'no',
'name': 'Time Off Type',
'requires_allocation': False,
})
cls.holiday = cls.env['hr.leave'].with_context(mail_create_nolog=True, mail_notrack=True).with_user(cls.user_employee).create({
'name': 'Leave 1',
'name': 'Time Off 1',
'employee_id': cls.employee_emp.id,
'holiday_status_id': cls.hr_leave_type.id,
'date_from': leave_start_datetime,
'date_to': leave_end_datetime,
'number_of_days': (leave_end_datetime - leave_start_datetime).days,
'request_date_from': leave_start_date,
'request_date_to': leave_end_date,
})
cls.holiday.with_user(cls.user_hrmanager).action_validate()
cls.holiday.with_user(cls.user_hrmanager).action_approve()
@freeze_time('2018-02-05') # useful to be able to cancel the validated time off
def test_action_cancel_leave(self):
@ -38,7 +36,7 @@ class TestHrHolidaysCancelLeave(TestHrHolidaysCommon):
self.env['hr.holidays.cancel.leave'].with_user(self.user_employee).with_context(default_leave_id=self.holiday.id) \
.new({'reason': 'Test remove holiday'}) \
.action_cancel_leave()
self.assertFalse(self.holiday.active, 'The validated leave should be canceled, that is archived.')
self.assertEqual(self.holiday.state, 'cancel', 'The validated leave should be canceled.')
def test_action_cancel_leave_in_past(self):
""" Test if the user may cancel a validated leave in the past. """
@ -61,5 +59,5 @@ class TestHrHolidaysCancelLeave(TestHrHolidaysCommon):
self.env['hr.holidays.cancel.leave'].with_user(self.user_employee).with_context(default_leave_id=self.holiday.id) \
.new({'reason': 'Test remove holiday'}) \
.action_cancel_leave()
with self.assertRaises(UserError, msg='The user should not be able to manually unarchive the leave.'):
self.holiday.with_user(self.user_employee).write({'active': False})
with self.assertRaises(UserError, msg='Only a manager can modify a canceled leave.'):
self.holiday.with_user(self.user_employee).write({'state': 'cancel'})

View file

@ -1,5 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo.tests import HttpCase
from odoo.tests.common import tagged
@ -8,8 +10,12 @@ from datetime import date
@tagged('post_install', '-at_install')
class TestHrHolidaysTour(HttpCase):
@freeze_time('01/17/2022')
def test_hr_holidays_tour(self):
admin_user = self.env.ref('base.user_admin')
admin_user.write({
'email': 'mitchell.admin@example.com',
})
admin_employee = admin_user.employee_id
HRLeave = self.env['hr.leave']
date_from = date(2022, 1, 17)
@ -25,11 +31,11 @@ class TestHrHolidaysTour(HttpCase):
holidays_type_1 = LeaveType.create({
'name': 'NotLimitedHR',
'requires_allocation': 'no',
'requires_allocation': False,
'leave_validation_type': 'hr',
})
# add allocation
allocation = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': admin_employee.id,
'holiday_status_id': holidays_type_1.id,
@ -38,6 +44,5 @@ class TestHrHolidaysTour(HttpCase):
'date_from': '2022-01-01',
'date_to': '2022-12-31',
})
allocation.action_validate()
self.start_tour('/web', 'hr_holidays_tour', login="admin")
self.start_tour('/odoo', 'hr_holidays_tour', login="admin")

View file

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.exceptions import AccessError
from odoo.exceptions import AccessError, ValidationError
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@ -12,19 +10,58 @@ from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
class TestHrLeaveType(TestHrHolidaysCommon):
def test_time_type(self):
employee = self.env['hr.employee'].create({'name': 'Test Employee'})
leave_type = self.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'time_type': 'leave',
'requires_allocation': 'no',
'requires_allocation': False,
})
with self.assertRaises(ValidationError):
leave_type.allow_request_on_top = True
worked_leave_type = self.env['hr.leave.type'].create({
'name': 'Worked Time',
'time_type': 'other',
'requires_allocation': False,
})
with self.assertRaises(ValidationError):
worked_leave_type.elligible_for_accrual_rate = False
leave_0 = self.env['hr.leave'].create({
'name': 'Remote Work',
'employee_id': employee.id,
'holiday_status_id': worked_leave_type.id,
'request_date_from': '2025-09-01', # Monday
'request_date_to': '2025-09-05',
})
leave_0.action_approve()
self.assertEqual(
self.env['resource.calendar.leaves'].search([('holiday_id', '=', leave_0.id)]).time_type,
'other',
)
with freeze_time('2025-09-03 13:00:00'):
employee._compute_leave_status()
self.assertFalse(employee.is_absent)
with self.assertRaises(ValidationError):
leave_1 = self.env['hr.leave'].create({
'name': 'Doctor Appointment',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': '2025-09-03',
'request_date_to': '2025-09-03',
})
worked_leave_type.allow_request_on_top = True
leave_1 = self.env['hr.leave'].create({
'name': 'Doctor Appointment',
'employee_id': self.employee_hruser_id,
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days': 1,
'request_date_from': '2025-09-03',
'request_date_to': '2025-09-03',
})
leave_1.action_approve()
@ -32,13 +69,16 @@ class TestHrLeaveType(TestHrHolidaysCommon):
self.env['resource.calendar.leaves'].search([('holiday_id', '=', leave_1.id)]).time_type,
'leave'
)
with freeze_time('2025-09-03 13:00:00'):
employee._compute_leave_status()
self.assertTrue(employee.is_absent)
def test_type_creation_right(self):
# HrUser creates some holiday statuses -> crash because only HrManagers should do this
with self.assertRaises(AccessError):
self.env['hr.leave.type'].with_user(self.user_hruser_id).create({
'name': 'UserCheats',
'requires_allocation': 'no',
'requires_allocation': False,
})
def test_users_tz_shift_back(self):
@ -61,7 +101,7 @@ class TestHrLeaveType(TestHrHolidaysCommon):
'employee_id': employee.id,
'date_from': '2024-08-19',
'date_to': '2024-08-20',
}).action_validate()
}).action_approve()
leave_types = self.env['hr.leave.type'].with_context(
default_date_from='2024-08-20 21:00:00',

View file

@ -0,0 +1,74 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo import Command
from odoo.tests import HttpCase
from odoo.tests.common import tagged
from datetime import date
@tagged('post_install', '-at_install')
class TestHrLeaveTypeTour(HttpCase):
@freeze_time('01/17/2022')
def test_hr_leave_type_tour(self):
"""
Test Time Off multi company rule defined in hr_holidays_security for hr_leave_type.
The available leave types are the ones whose:
- Company is one of the selected companies.
- Company is false but whose country is one the countries of the selected companies.
- Company is false and country is false
Define:
- 2 Companies: company_1 and company_2
- 3 Leave Types:
* leave_type_1 whose country is set to the country of company_1.
* leave_type_2 whose company is set to company_2.
* leave_type_3 whose country and company are both False.
leave_type_1 will be available if company_1 is one of the selected companies.
leave_type_2 will be available if company_2 is one of the selected companies.
leave_type_3 will always be available.
"""
admin_user = self.env.ref('base.user_admin')
admin_user.write({
'email': 'mitchell.admin@example.com',
})
admin_employee = admin_user.employee_id
HRLeave = self.env['hr.leave']
date_from = date(2022, 1, 17)
date_to = date(2022, 1, 18)
leaves_on_freeze_date = HRLeave.search([
('date_from', '>=', date_from),
('date_to', "<=", date_to),
('employee_id', '=', admin_employee.id)
])
leaves_on_freeze_date.sudo().unlink()
company_1 = self.env.company
company_1.name = 'company_1'
company_2 = self.env['res.company'].create({'name': 'company_2'})
self.env["res.users"].browse(2).write({
"company_ids": [Command.clear(), Command.link(company_1.id), Command.link(company_2.id)]
})
leave_type = self.env['hr.leave.type'].with_user(admin_user)
leave_type.create({
'name': 'leave_type_1',
'requires_allocation': False,
'leave_validation_type': 'hr',
'country_id': company_1.country_id.id
})
leave_type.create({
'name': 'leave_type_2',
'requires_allocation': False,
'leave_validation_type': 'hr',
'company_id': company_2.id
})
leave_type.create({
'name': 'leave_type_3',
'requires_allocation': False,
'leave_validation_type': 'hr'
})
self.start_tour('/web', 'hr_leave_type_tour', login="admin")

View file

@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import ValidationError
from odoo.release import version_info
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('-at_install', 'post_install', 'post_install_l10n')
class TestLeaveTypeData(TransactionCase):
def test_ensure_hr_leave_type_definition(self):
# Make sure leave types are defined in hr_holidays in master (and not in other modules)
# In the case this tests breaks during a forward port, move the time off type definition
# to hr_work_entry and make a upgrade script accordingly.
if version_info[3] != 'alpha':
return
leave_types_xmlids = self.env['hr.leave.type'].search([])._get_external_ids()
invalid_xmlids = []
for xmlids in leave_types_xmlids.values():
for xmlid in xmlids:
module = xmlid.split('.')[0]
if module not in ['hr_holidays', '__export__', '__custom__'] and not module.startswith('test_'):
invalid_xmlids.append(xmlid)
if invalid_xmlids:
raise ValidationError("Some time off types are defined outside of module hr_holidays.\n%s" % '\n'.join(invalid_xmlids))

View file

@ -0,0 +1,387 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from freezegun import freeze_time
from odoo import tests
from odoo.tests import Form, new_test_user, TransactionCase
from odoo.exceptions import ValidationError
@tests.tagged('access_rights', 'post_install', '-at_install')
class TestHrLeaveMandatoryDays(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.default_calendar = cls.env['resource.calendar'].create({
'name': 'moon calendar',
})
cls.company = cls.env['res.company'].create({
'name': 'super company',
'resource_calendar_id': cls.default_calendar.id,
})
cls.employee_user = new_test_user(cls.env, login='user', groups='base.group_user', company_ids=[(6, 0, cls.company.ids)], company_id=cls.company.id)
cls.manager_user = new_test_user(cls.env, login='manager', groups='base.group_user,hr_holidays.group_hr_holidays_manager', company_ids=[(6, 0, cls.company.ids)], company_id=cls.company.id)
cls.employee_emp = cls.env['hr.employee'].create({
'name': 'Toto Employee',
'company_id': cls.company.id,
'user_id': cls.employee_user.id,
'resource_calendar_id': cls.default_calendar.id,
})
cls.manager_emp = cls.env['hr.employee'].create({
'name': 'Toto Mananger',
'company_id': cls.company.id,
'user_id': cls.manager_user.id,
})
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Unlimited',
'leave_validation_type': 'hr',
'requires_allocation': False,
'company_id': cls.company.id,
})
cls.mandatory_day = cls.env['hr.leave.mandatory.day'].create({
'name': 'Super Event',
'company_id': cls.company.id,
'start_date': datetime(2021, 11, 2),
'end_date': datetime(2021, 11, 2),
'color': 1,
'resource_calendar_id': cls.default_calendar.id,
})
cls.mandatory_week = cls.env['hr.leave.mandatory.day'].create({
'name': 'Super Event End Of Week',
'company_id': cls.company.id,
'start_date': datetime(2021, 11, 8),
'end_date': datetime(2021, 11, 12),
'color': 2,
'resource_calendar_id': cls.default_calendar.id,
})
@freeze_time('2021-10-15')
def test_request_mandatory_days(self):
# An employee can request time off outside mandatory days
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 3),
'request_date_to': datetime(2021, 11, 3),
})
# Taking a time off during a Mandatory Day is not allowed for a simple employee...
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 3),
'request_date_to': datetime(2021, 11, 17),
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 9),
'request_date_to': datetime(2021, 11, 9),
})
# ... but is allowed for a Time Off Officer
self.env['hr.leave'].with_user(self.manager_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 2),
'request_date_to': datetime(2021, 11, 2),
})
@freeze_time('2021-10-15')
def test_get_mandatory_days(self):
mandatory_days = self.employee_emp.get_mandatory_days('2021-11-01', '2021-11-30')
# Mandatory Days spanning multiple days should be split in single days
expected_data = {'2021-11-02': 1, '2021-11-08': 2, '2021-11-09': 2, '2021-11-10': 2, '2021-11-11': 2, '2021-11-12': 2}
self.assertEqual(len(mandatory_days), len(expected_data))
for day, color in expected_data.items():
self.assertTrue(day in mandatory_days)
self.assertEqual(color, mandatory_days[day])
with Form(self.env['hr.leave'].with_user(self.employee_user.id).with_context(default_employee_id=self.employee_emp.id)) as leave_form:
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = datetime(2021, 11, 1)
leave_form.request_date_to = datetime(2021, 11, 1)
leave_form.save() # need to be saved to have access to record
self.assertFalse(leave_form.record.has_mandatory_day)
leave_form.request_date_to = datetime(2021, 11, 5)
leave_form.save() # need to be saved to have access to record
self.assertTrue(leave_form.record.has_mandatory_day)
@freeze_time('2021-10-15')
def test_department_mandatory_days(self):
production_department = self.env['hr.department'].create({
'name': 'Production Department',
'company_id': self.company.id,
})
post_production_department = self.env['hr.department'].create({
'name': 'Post-Production Department',
'company_id': self.company.id,
'parent_id': production_department.id,
})
deployment_department = self.env['hr.department'].create({
'name': 'Deployment Department',
'company_id': self.company.id,
'parent_id': production_department.id,
})
self.employee_emp.write({
'department_id': post_production_department.id
})
# Create one mandatory day for each department
self.env['hr.leave.mandatory.day'].create({
'name': 'Last Rush Before Launch (production)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 3),
'end_date': datetime(2021, 11, 3),
'color': 1,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [production_department.id],
})
self.env['hr.leave.mandatory.day'].create({
'name': 'Last Rush Before Launch (post-production)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 4),
'end_date': datetime(2021, 11, 4),
'color': 1,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [post_production_department.id],
})
self.env['hr.leave.mandatory.day'].create({
'name': 'Last Rush Before Launch (deployment)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 5),
'end_date': datetime(2021, 11, 5),
'color': 1,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [deployment_department.id],
})
# The employee should only be able to create a time off on mandatory days
# that do not include his department
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'have been given the black spot',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 3),
'request_date_to': datetime(2021, 11, 3),
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'have been given the black spot',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 4),
'request_date_to': datetime(2021, 11, 4),
})
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'have been given the black spot',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 5),
'request_date_to': datetime(2021, 11, 5),
})
@freeze_time('2021-10-15')
def test_job_position_mandatory_days(self):
"""
Test mandatory leave restrictions based on job positions and departments.
This test ensures that employees cannot request time off on mandatory leave days
that are assigned to their specific job position or department. The logic includes:
- Creating a production department and job positions.
- Assigning an employee to a department and job position.
- Defining mandatory leave days for specific jobs and departments.
- Validating that the employee cannot take leave on restricted days.
- Allowing leave requests on days without conflicts.
Expected behavior:
- Raises a ValidationError if the employee's department or job position is linked to a mandatory leave day.
- Allows leave requests on days that do not conflict with their assigned job or department.
"""
production_department = self.env['hr.department'].create({
'name': 'Production Department',
'company_id': self.company.id,
})
# Create job positions
production_manager, post_production_manager = self.env['hr.job'].create([{
'name': 'Production Manager',
'company_id': self.company.id,
},{
'name': 'Post-Production Manager',
'company_id': self.company.id,
}])
self.employee_emp.write({
'department_id': production_department.id,
'job_id': production_manager.id,
})
# Create mandatory leave days for job positions and departments
self.env['hr.leave.mandatory.day'].create([{
'name': 'Production Deadline',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 3),
'end_date': datetime(2021, 11, 3),
'color': 1,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [production_department.id],
'job_ids': [production_manager.id],
},{
'name': 'Post-Production Deadline',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 4),
'end_date': datetime(2021, 11, 4),
'color': 2,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [production_department.id],
}, {
'name': 'Team General Meeting',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 6),
'end_date': datetime(2021, 11, 6),
'color': 3,
'resource_calendar_id': self.default_calendar.id,
'job_ids': [production_manager.id]
}, {
'name': 'Department General Meeting',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 5),
'end_date': datetime(2021, 11, 5),
'color': 3,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [production_department.id],
'job_ids': [post_production_manager.id]
}])
# The employee should only be able to create a time off on mandatory days
# that do not conflict with their job or department restrictions
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'Vacation during Production Deadline',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 3),
'request_date_to': datetime(2021, 11, 3),
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'Vacation during Post-Production Deadline',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 4),
'request_date_to': datetime(2021, 11, 4),
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'Holiday During Team General Meating',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 6),
'request_date_to': datetime(2021, 11, 6),
})
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'Vacation during General Meeting',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 5),
'request_date_to': datetime(2021, 11, 5),
})
@freeze_time('2021-10-15')
def test_multiple_employees_mandatory_days(self):
production_department = self.env['hr.department'].create({
'name': 'Production Department',
'company_id': self.company.id,
})
post_production_department, deployment_department = self.env['hr.department'].create([{
'name': 'Post-Production Department',
'company_id': self.company.id,
'parent_id': production_department.id,
}, {
'name': 'Deployment Department',
'company_id': self.company.id,
'parent_id': production_department.id,
}])
employee_emp_2 = self.env['hr.employee'].create({
'name': 'Tototo Employee',
'company_id': self.company.id,
'resource_calendar_id': self.default_calendar.id,
'department_id': deployment_department.id,
})
self.employee_emp.write({
'department_id': post_production_department.id
})
self.env['hr.leave.mandatory.day'].create([{
'name': 'Last Rush Before Launch (post-production)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 4),
'end_date': datetime(2021, 11, 4),
'color': 3,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [post_production_department.id],
}, {
'name': 'Last Rush Before Launch (deployment)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 5),
'end_date': datetime(2021, 11, 5),
'color': 4,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [deployment_department.id],
}])
mandatory_days = (self.employee_emp + employee_emp_2).get_mandatory_days('2021-11-01', '2021-11-30')
# Mandatory Days spanning multiple days should be split in single days
expected_data = {'2021-11-02': 1, '2021-11-04': 3, '2021-11-05': 4, '2021-11-08': 2, '2021-11-09': 2, '2021-11-10': 2, '2021-11-11': 2, '2021-11-12': 2}
# All mandatory days for both employees should be returned
self.assertEqual(len(mandatory_days), len(expected_data))
for day, color in expected_data.items():
self.assertTrue(day in mandatory_days)
self.assertEqual(color, mandatory_days[day])
# Check that has_mandatory_day is computed correctly for multiple leaves
leave_1, leave_2 = self.env['hr.leave'].with_user(self.manager_user.id).create([{
'name': 'have been given the black spot',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2021, 11, 3),
'request_date_to': datetime(2021, 11, 4),
},
{
'name': 'have been given the gray spot',
'holiday_status_id': self.leave_type.id,
'employee_id': employee_emp_2.id,
'request_date_from': datetime(2021, 11, 6),
'request_date_to': datetime(2021, 11, 6),
}])
self.assertTrue(leave_1.has_mandatory_day)
self.assertFalse(leave_2.has_mandatory_day)

View file

@ -0,0 +1,429 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, date
from odoo.exceptions import ValidationError
from odoo.tests import Form, freeze_time, tagged
from odoo.addons.hr_holidays.tests.common import TestHolidayContract
@tagged('multi_contract')
class TestHolidaysMultiContract(TestHolidayContract):
@classmethod
def setUpClass(cls):
super().setUpClass()
def test_move_contract_in_leave(self):
# test move contract dates such that a leave is across two contracts
start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S')
self.contract_cdi.write({'contract_date_start': datetime.strptime('2015-12-30', '%Y-%m-%d').date()})
# begins during contract, ends after contract
leave = self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
# move contract in the middle of the leave
with self.assertRaises(ValidationError):
self.contract_cdi.contract_date_start = datetime.strptime('2015-11-17', '%Y-%m-%d').date()
def test_create_contract_in_leave(self):
# test create contract such that a leave is across two contracts
start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S')
self.contract_cdi.contract_date_start = datetime.strptime('2015-12-30', '%Y-%m-%d').date() # remove this contract to be able to create the leave
# begins during contract, ends after contract
leave = self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
# move contract in the middle of the leave
with self.assertRaises(ValidationError):
self.jules_emp.create_version({
'date_version': datetime.strptime('2015-11-30', '%Y-%m-%d').date(),
'contract_date_start': datetime.strptime('2015-11-30', '%Y-%m-%d').date(),
'contract_date_end': False,
'name': 'Contract for Richard',
'resource_calendar_id': self.calendar_40h.id,
'wage': 5000.0,
})
def test_leave_outside_contract(self):
# Leave outside contract => should not raise
start = datetime.strptime('2014-10-18 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2014-10-20 09:00:00', '%Y-%m-%d %H:%M:%S')
self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
# begins before contract, ends during contract => should not raise
start = datetime.strptime('2014-10-25 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-01-15 18:00:00', '%Y-%m-%d %H:%M:%S')
self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
# begins during contract, ends after contract => should not raise
self.contract_cdi.date_end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-12-05 18:00:00', '%Y-%m-%d %H:%M:%S')
self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
def test_no_leave_overlapping_contracts(self):
with self.assertRaises(ValidationError):
# Overlap two contracts
start = datetime.strptime('2015-11-12 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-11-17 18:00:00', '%Y-%m-%d %H:%M:%S')
self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
# Leave inside fixed term contract => should not raise
start = datetime.strptime('2015-11-04 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-11-07 09:00:00', '%Y-%m-%d %H:%M:%S')
self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
# Leave inside contract (no end) => should not raise
start = datetime.strptime('2015-11-18 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-11-20 09:00:00', '%Y-%m-%d %H:%M:%S')
self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
def test_leave_request_next_contracts(self):
start = datetime.strptime('2015-11-23 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2015-11-24 18:00:00', '%Y-%m-%d %H:%M:%S')
leave = self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
self.assertEqual(leave.number_of_hours, 14, "It should count hours according to the future contract.")
def test_leave_multi_contracts_same_schedule(self):
# TODO DBE / ARPI : Is this test still valid ?
# Allow leaves overlapping multiple contracts if same
# resource calendar
leave = self.create_leave(datetime(2022, 6, 1, 7, 0, 0), datetime(2022, 6, 30, 18, 0, 0), name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
self.contract_cdi.contract_date_end = date(2022, 6, 15)
self.jules_emp.create_version({
'date_version': date(2022, 6, 16),
'contract_date_start': date(2022, 6, 16),
'contract_date_end': False,
'name': 'New Contract for Jules',
'resource_calendar_id': self.calendar_35h.id,
'wage': 5000.0,
})
def test_leave_same_contract_multiple_versions_with_different_schedules(self):
self.contract_cdi.contract_date_end = date(2022, 7, 30)
self.jules_emp.create_version({
'date_version': date(2022, 6, 10),
'contract_date_start': self.contract_cdi.contract_date_start,
'contract_date_end': self.contract_cdi.contract_date_end,
'name': 'New Version with a different schedule for Jules',
'resource_calendar_id': self.calendar_40h.id,
'wage': 5000.0,
})
with self.assertRaises(ValidationError):
leave = self.create_leave(datetime(2022, 6, 1, 7, 0, 0), datetime(2022, 6, 30, 18, 0, 0), name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
leave = self.create_leave(datetime(2022, 5, 1, 7, 0, 0), datetime(2022, 5, 10, 18, 0, 0), name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
leave = self.create_leave(datetime(2022, 6, 15, 7, 0, 0), datetime(2022, 7, 20, 18, 0, 0), name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
def test_leave_multi_contracts_split(self):
# Check that setting a contract as running correctly
# splits the existing time off for this employee that
# are ovelapping with another contract with another
# working schedule
leave = self.create_leave(date(2022, 6, 1), date(2022, 6, 30), name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
self.assertEqual(leave.number_of_days, 22)
self.assertEqual(leave.state, 'validate')
self.contract_cdi.contract_date_end = date(2022, 6, 15)
self.jules_emp.create_version({
'date_version': date(2022, 6, 16),
'contract_date_start': date(2022, 6, 16),
'contract_date_end': False,
'name': 'New Contract for Jules',
'resource_calendar_id': self.calendar_40h.id,
'wage': 5000.0,
})
leaves = self.env['hr.leave'].search([('employee_id', '=', self.jules_emp.id)])
self.assertEqual(len(leaves), 3)
self.assertEqual(leave.state, 'refuse')
first_leave = leaves.filtered(lambda l: l.date_from.day == 1 and l.date_to.day == 15)
self.assertEqual(first_leave.state, 'validate')
self.assertEqual(first_leave.number_of_days, 11)
second_leave = leaves.filtered(lambda l: l.date_from.day == 16 and l.date_to.day == 30)
self.assertEqual(second_leave.state, 'confirm')
self.assertEqual(second_leave.number_of_days, 11)
def test_multi_contracts_draft(self):
# Check that setting a contract as running correctly
# make the existing time off to draft for this employee
# which is after another contract with another
# working schedule
leave = self.create_leave(date(2022, 6, 27), date(2022, 6, 30), name="Doctor Appointment", employee_id=self.jules_emp.id)
leave.action_approve()
self.assertEqual(leave.number_of_days, 4)
self.assertEqual(leave.state, 'validate')
self.contract_cdi.contract_date_end = date(2022, 6, 15)
self.jules_emp.create_version({
'date_version': date(2022, 6, 16),
'contract_date_start': date(2022, 6, 16),
'contract_date_end': False,
'name': 'New Contract for Jules',
'resource_calendar_id': self.calendar_40h.id,
'wage': 5000.0,
})
leaves = self.env['hr.leave'].search([('employee_id', '=', self.jules_emp.id)])
self.assertEqual(len(leaves), 1)
self.assertEqual(leave.state, 'confirm')
self.assertEqual(leave.number_of_days, 4)
def test_contract_traceability_calculate_nbr_leave(self):
"""
The goal is to test the traceability of contracts in the past,
i.e. to check that expired contracts are taken into account
to ensure the consistency of leaves (number of days/hours) in the past.
"""
calendar_full, calendar_partial = self.env['resource.calendar'].create([
{
'name': 'Full time (5/5)',
},
{
'name': 'Partial time (4/5)',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Evening', '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 Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
# Does not work on Wednesdays
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Evening', '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 Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
]
},
])
employee = self.env['hr.employee'].create({
'name': 'Employee',
'resource_calendar_id': calendar_partial.id,
})
employee.create_version({
'name': 'Full time (5/5)',
'date_version': datetime.strptime('2023-01-01', '%Y-%m-%d').date(),
'contract_date_start': datetime.strptime('2023-01-01', '%Y-%m-%d').date(),
'contract_date_end': datetime.strptime('2023-06-30', '%Y-%m-%d').date(),
'resource_calendar_id': calendar_full.id,
'wage': 1000.0,
})
employee.create_version({
'name': 'Partial time (4/5)',
'date_version': datetime.strptime('2023-07-01', '%Y-%m-%d').date(),
'contract_date_start': datetime.strptime('2023-07-01', '%Y-%m-%d').date(),
'contract_date_end': datetime.strptime('2023-12-31', '%Y-%m-%d').date(),
'resource_calendar_id': calendar_partial.id,
'wage': 1000.0,
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Leave Type',
'time_type': 'leave',
'requires_allocation': True,
'leave_validation_type': 'hr',
'request_unit': 'day',
})
allocation = self.env['hr.leave.allocation'].create({
'name': 'Allocation',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'number_of_days': 10,
'state': 'confirm',
'date_from': datetime.strptime('2023-01-01', '%Y-%m-%d').date(),
'date_to': datetime.strptime('2023-12-31', '%Y-%m-%d').date(),
})
allocation.action_approve()
leave_during_full_time, leave_during_partial_time = self.env['hr.leave'].create([
{
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': '2023-01-03', # Tuesday
'request_date_to': '2023-01-05', # Thursday
},
{
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': '2023-12-05', # Tuesday
'request_date_to': '2023-12-07', # Thursday
},
])
self.assertEqual(leave_during_full_time.number_of_days, 3)
self.assertEqual(leave_during_partial_time.number_of_days, 2)
self.assertEqual(leave_during_full_time.number_of_hours, 24)
self.assertEqual(leave_during_partial_time.number_of_hours, 16)
# Simulate the unit change days/hours of the time off type
(leave_during_full_time + leave_during_partial_time)._compute_duration()
self.assertEqual(leave_during_full_time.number_of_days, 3)
self.assertEqual(leave_during_partial_time.number_of_days, 2)
self.assertEqual(leave_during_full_time.number_of_hours, 24)
self.assertEqual(leave_during_partial_time.number_of_hours, 16)
# Check after leave approval
(leave_during_full_time + leave_during_partial_time).action_approve()
self.assertEqual(leave_during_full_time.number_of_hours, 24)
self.assertEqual(leave_during_partial_time.number_of_hours, 16)
@freeze_time('2024-01-05')
def test_multi_contract_out_of_office(self):
"""
Test that the out of office feature works correctly with multiple contracts
The Case is when the employee is out of the office for a period that overlaps multiple contracts
"""
calendar_full, calendar_partial = self.env['resource.calendar'].create([
{
'name': 'Full time (5/5)',
},
{
'name': 'Partial time (4/5)',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Evening', '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 Evening', '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 Evening', '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 Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
]
},
])
employee = self.env['hr.employee'].create({
'name': 'Employee',
'resource_calendar_id': calendar_full.id,
})
employee.create_version({
'name': 'Full time (5/5)',
'date_version': datetime.strptime('2024-01-01', '%Y-%m-%d').date(),
'contract_date_start': datetime.strptime('2024-01-01', '%Y-%m-%d').date(),
'contract_date_end': datetime.strptime('2024-01-31', '%Y-%m-%d').date(),
'resource_calendar_id': calendar_full.id,
'wage': 1000.0,
})
employee.create_version({
'name': 'Partial time (4/5)',
'date_version': datetime.strptime('2024-02-01', '%Y-%m-%d').date(),
'contract_date_start': datetime.strptime('2024-02-01', '%Y-%m-%d').date(),
'contract_date_end': False,
'resource_calendar_id': calendar_partial.id,
'wage': 1000.0,
# 'state': 'draft',
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Leave Type',
'time_type': 'leave',
'requires_allocation': False,
'leave_validation_type': 'hr',
'request_unit': 'day',
})
leave1 = self.env['hr.leave'].create({
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': '2024-01-01',
'request_date_to': '2024-01-31',
})
leave2 = self.env['hr.leave'].create({
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': '2024-02-01',
'request_date_to': '2024-02-29',
})
leave1.action_approve()
leave2.action_approve()
employee._compute_leave_status()
self.assertEqual(employee.leave_date_to, date(2024, 3, 4))
def test_multi_contracts_with_different_work_schedules(self):
"""
Test that the employee can have multiple non-overlapping versions with different work schedules,
and that the leave requests are correctly calculated based on corresponding the contract's working schedule.
"""
calendar_full, calendar_partial = self.env['resource.calendar'].create([
{
'name': 'Full time (5/5, 8h/day)',
'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'}),
]
},
{
'name': 'Partial time (5/5, 6h/day)',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
]
},
])
employee = self.env['hr.employee'].create({
'name': "Employee",
"resource_calendar_id": calendar_partial.id,
})
employee.create_version({
"name": "Full time (5/5)",
"date_version": datetime.strptime('2023-01-01', '%Y-%m-%d').date(),
"contract_date_start": datetime.strptime('2023-01-01', '%Y-%m-%d').date(),
"contract_date_end": datetime.strptime('2023-06-30', '%Y-%m-%d').date(),
"resource_calendar_id": calendar_full.id,
"wage": 1000.0,
})
employee.create_version({
"name": "Partial time (5/5)",
"date_version": datetime.strptime('2023-07-01', '%Y-%m-%d').date(),
"contract_date_start": datetime.strptime('2023-07-01', '%Y-%m-%d').date(),
"contract_date_end": datetime.strptime('2023-12-31', '%Y-%m-%d').date(),
"resource_calendar_id": calendar_partial.id,
"wage": 1000.0,
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Leave Type',
'time_type': 'leave',
'requires_allocation': False,
'request_unit': 'day',
})
with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form:
leave_form.holiday_status_id = leave_type
leave_form.request_date_from = date(2023, 2, 14) # full-time calendar
leave_form.request_date_to = date(2023, 2, 14)
leave = leave_form.save()
# Assert based on full-time calendar (8h)
self.assertEqual(leave.number_of_days, 1)
self.assertEqual(leave.number_of_hours, 8)
# Change to date under partial-time contract
leave.write({
'request_date_from': date(2023, 7, 14),
'request_date_to': date(2023, 7, 14),
})
# Assert based on partial-time calendar
self.assertEqual(leave.number_of_days, 1)
self.assertEqual(leave.number_of_hours, 6)

View file

@ -0,0 +1,41 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from odoo import tests
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tests.tagged('post_install', '-at_install')
class TestHrHolidaysAccessRightsCommon(TestHrHolidaysCommon):
@classmethod
def setUpClass(cls):
super(TestHrHolidaysAccessRightsCommon, cls).setUpClass()
cls.company_2 = cls.env['res.company'].create({'name': 'Test company 2'})
def test_unrelated_public_leave(self):
public_leave = self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off for Company 2',
'resource_id': False,
'date_from': datetime(2024, 1, 3, 6, 0, 0),
'date_to': datetime(2024, 1, 3, 19, 0, 0),
})
public_leave.company_id = self.company_2
leave_type = self.env['hr.leave.type'].create({
'name': 'Test Leave Type',
'requires_allocation': False,
'request_unit': 'day',
'company_id': False,
})
leave = self.env['hr.leave'].create({
'name': '3 days leave',
'employee_id': self.employee_emp_id,
'holiday_status_id': leave_type.id,
'request_date_from': date(2024, 1, 2),
'request_date_to': datetime(2024, 1, 4),
})
self.assertNotEqual(public_leave.company_id, self.employee_emp.company_id)
self.assertEqual(
leave.number_of_days, 3,
"The leave should not depend on other companies public leaves.")

View file

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from freezegun import freeze_time
from odoo.tests.common import tagged
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.exceptions import ValidationError
@tagged('negative_time_off')
class TestNegative(TestHrHolidaysCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Limited with negative',
'leave_validation_type': 'no_validation',
'requires_allocation': True,
'company_id': cls.company.id,
'allows_negative': True,
'max_allowed_negative': 5,
})
cls.allocation_2022 = cls.env['hr.leave.allocation'].create({
'employee_id': cls.employee_emp_id,
'holiday_status_id': cls.leave_type.id,
'date_from': '2022-01-01',
'date_to': '2022-12-31',
'number_of_days': 1,
})
cls.allocation_2022.action_approve()
cls.allocation_2023 = cls.env['hr.leave.allocation'].create({
'employee_id': cls.employee_emp_id,
'holiday_status_id': cls.leave_type.id,
'date_from': '2023-01-01',
'number_of_days': 5,
})
cls.allocation_2023.action_approve()
def test_negative_time_off(self):
with freeze_time('2022-10-02'):
# At the start of 2022, the user receives 1 days, his balance is at 1
# The first 2022 leave brings the user balance at -4
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'first 2022 leave of 5 days',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2022, 10, 24),
'request_date_to': datetime(2022, 10, 28),
})
with freeze_time('2023-10-02'):
# At the start of 2023, the user receives 5 days, his balance is at 1
# The first leave of 2023 brings the balance at -4
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'first 2023 leave of 5 days',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'request_date_from': datetime(2023, 10, 9),
'request_date_to': datetime(2023, 10, 13),
})
# The leave should not be possible to take since it would bring the balance at -9
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'not takable leaves of 5 days',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp_id,
'request_date_from': datetime(2023, 10, 16),
'request_date_to': datetime(2023, 10, 20),
})
# The second leave of 2023 brings the balance at -5
one_day_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Second 2023 leave of 1 day',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp_id,
'request_date_from': datetime(2023, 10, 23),
'request_date_to': datetime(2023, 10, 23),
})
# The leave should not be possible to edit since it would bring the balance at -6
with self.assertRaises(ValidationError):
one_day_leave.with_user(self.user_hrmanager_id).write({
'date_to': datetime(2023, 10, 24),
})

View file

@ -1,16 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from datetime import date, datetime, timezone
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import fields
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.tests.common import tagged, users, warmup
from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.addons.mail.tools.discuss import Store
@tagged('out_of_office')
@tagged("post_install", "-at_install", "out_of_office")
class TestOutOfOffice(TestHrHolidaysCommon):
@classmethod
@ -19,29 +21,41 @@ class TestOutOfOffice(TestHrHolidaysCommon):
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Legal Leaves',
'time_type': 'leave',
'requires_allocation': 'no',
'requires_allocation': False,
})
@freeze_time('2024-06-06')
def test_leave_ooo(self):
self.assertNotEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should not be on leave')
self.assertNotEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should not be on leave')
leave_date_end = (datetime.today() + relativedelta(days=3))
leave = self.env['hr.leave'].create({
# validate a leave from 2024-06-05 (Wednesday) to 2024-06-07 (Friday)
first_leave = self.env['hr.leave'].create({
'name': 'Christmas',
'employee_id': self.employee_hruser.id,
'holiday_status_id': self.leave_type.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': leave_date_end,
'number_of_days': 4,
'request_date_from': "2024-06-05",
'request_date_to': "2024-06-07",
})
leave.action_approve()
first_leave.action_approve()
# validate a leave from 2024-06-10 (Monday) to 2024-06-11 (Tuesday)
second_leave = self.env['hr.leave'].create({
'name': 'Christmas',
'employee_id': self.employee_hruser.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': "2024-06-10",
'request_date_to': "2024-06-11",
})
second_leave.action_approve()
# missing dependencies on compute functions
self.employee_hruser.user_id.invalidate_recordset(["im_status"])
self.employee_hruser.user_id.partner_id.invalidate_recordset(["im_status"])
self.assertEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should be out (leave_offline)')
self.assertEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should be out (leave_offline)')
partner = self.employee_hruser.user_id.partner_id
partner2 = self.user_employee.partner_id
channel = self.env['mail.channel'].with_user(self.user_employee).with_context({
channel = self.env['discuss.channel'].with_user(self.user_employee).with_context({
'mail_create_nolog': True,
'mail_create_nosubscribe': True,
}).create({
@ -49,14 +63,23 @@ class TestOutOfOffice(TestHrHolidaysCommon):
'channel_type': 'chat',
'name': 'test'
})
channel_info = channel.channel_info()[0]
# shape of channelMembers is [('insert', data...)], [0][1] accesses the data
members_data = channel_info['channel']['channelMembers'][0][1]
self.assertEqual(len(members_data), 2, "Channel info should get info for the 2 members")
partner_info = next(member for member in members_data if member['persona']['partner']['email'] == partner.email)
partner2_info = next(member for member in members_data if member['persona']['partner']['email'] == partner2.email)
self.assertFalse(partner2_info['persona']['partner']['out_of_office_date_end'], "current user should not be out of office")
self.assertEqual(partner_info['persona']['partner']['out_of_office_date_end'], leave_date_end.strftime(DEFAULT_SERVER_DATE_FORMAT), "correspondent should be out of office")
data = Store().add(channel).get_result()
partner_info = next(p for p in data["res.partner"] if p["id"] == partner.id)
partner2_info = next(p for p in data["res.partner"] if p["id"] == partner2.id)
user_info = next(u for u in data["res.users"] if u["id"] == partner_info["main_user_id"])
user2_info = next(u for u in data["res.users"] if u["id"] == partner2_info["main_user_id"])
employee_info = next(e for e in data["hr.employee"] if e["id"] == user_info["employee_ids"][0])
employee2_info = next(e for e in data["hr.employee"] if e["id"] == user2_info["employee_ids"][0])
self.assertFalse(employee2_info["leave_date_to"], "current user should not be out of office")
# The employee will be back in the office the day after his second leave ends
self.assertEqual(
employee_info["leave_date_to"], "2024-06-12", "correspondent should be out of office"
)
self.assertEqual(
self.employee_hruser.user_id.with_context(formatted_display_name=True).display_name,
'armande (base.group_user,hr_holidays.group_hr_holidays_user) \t ✈ --Back on Jun 12, 2024--',
'formatted display name should show the "Back on" formatted date'
)
@tagged('out_of_office')
@ -68,16 +91,15 @@ class TestOutOfOfficePerformance(TestHrHolidaysCommon, TransactionCaseWithUserDe
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Legal Leaves',
'time_type': 'leave',
'requires_allocation': 'no',
'requires_allocation': False,
})
cls.leave_date_end = (datetime.today() + relativedelta(days=3))
cls.leave_date_end = (datetime.today() + relativedelta(days=2))
cls.leave = cls.env['hr.leave'].create({
'name': 'Christmas',
'employee_id': cls.employee_hruser_id,
'holiday_status_id': cls.leave_type.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': (datetime.today() + relativedelta(days=3)),
'number_of_days': 4,
'request_date_from': (date.today() - relativedelta(days=1)),
'request_date_to': cls.leave_date_end,
})
cls.hr_user = cls.employee_hruser.user_id
@ -87,7 +109,7 @@ class TestOutOfOfficePerformance(TestHrHolidaysCommon, TransactionCaseWithUserDe
@users('__system__', 'demo')
@warmup
def test_leave_im_status_performance_partner_offline(self):
with self.assertQueryCount(__system__=2, demo=2):
with self.assertQueryCount(__system__=4, demo=4):
self.assertEqual(self.employer_partner.im_status, 'offline')
@users('__system__', 'demo')
@ -101,13 +123,13 @@ class TestOutOfOfficePerformance(TestHrHolidaysCommon, TransactionCaseWithUserDe
@warmup
def test_leave_im_status_performance_partner_leave_offline(self):
self.leave.write({'state': 'validate'})
with self.assertQueryCount(__system__=2, demo=2):
with self.assertQueryCount(__system__=4, demo=4):
self.assertEqual(self.hr_partner.im_status, 'leave_offline')
def test_search_absent_employee(self):
present_employees = self.env['hr.employee'].search([('is_absent', '!=', True)])
absent_employees = self.env['hr.employee'].search([('is_absent', '=', True)])
today_date = datetime.utcnow().date()
today_date = datetime.now(timezone.utc).date()
holidays = self.env['hr.leave'].sudo().search([
('employee_id', '!=', False),
('state', '=', 'validate'),

View file

@ -0,0 +1,70 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from freezegun import freeze_time
from odoo.tests import tagged
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('post_install', '-at_install', 'accruals')
class TestAccrualAllocations(TestHrHolidaysCommon):
@classmethod
def setUpClass(cls):
super(TestAccrualAllocations, cls).setUpClass()
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Accrual Time Off',
'time_type': 'leave',
'requires_allocation': True,
'allocation_validation_type': 'no',
})
cls.accrual_plan = cls.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Test Seniority Plan',
'level_ids': [
(0, 0, {
'milestone_date': 'after',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'day',
'frequency': 'yearly',
'cap_accrued_time': True,
'maximum_leave': 10000,
}),
(0, 0, {
'milestone_date': 'after',
'start_count': 4,
'start_type': 'year',
'added_value': 1,
'added_value_type': 'day',
'frequency': 'yearly',
'cap_accrued_time': True,
'maximum_leave': 10000,
}),
(0, 0, {
'milestone_date': 'after',
'start_count': 8,
'start_type': 'year',
'added_value': 1,
'added_value_type': 'day',
'frequency': 'yearly',
'cap_accrued_time': True,
'maximum_leave': 10000,
}),
]
})
def _test_past_accrual(self):
with freeze_time("2023-12-01"):
allocation = self.env['hr.leave.allocation'].create({
'employee_id': self.employee_emp_id,
'allocation_type': 'accrual',
'accrual_plan_id': self.accrual_plan.id,
'holiday_status_id': self.leave_type.id,
'date_from': date(2000, 1, 1),
'number_of_days': 0,
})
allocation._process_accrual_plans()
self.assertEqual(allocation.number_of_days, 0)

View file

@ -1,25 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import Command, fields
from odoo.tests.common import new_test_user, tagged, TransactionCase, users
from odoo.addons.mail.tools.discuss import Store
from odoo import Command
from odoo.tests.common import tagged, TransactionCase
from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
@tagged('post_install', '-at_install')
class TestPartner(TransactionCase):
@classmethod
@freeze_time('2024-06-04')
def setUpClass(cls):
super().setUpClass()
# use a single value for today throughout the tests to avoid weird scenarios around midnight
cls.today = date.today()
cls.today = fields.Date.today()
baseUser = cls.env['res.users'].create({
'email': 'e.e@example.com',
'groups_id': [Command.link(cls.env.ref('base.group_user').id)],
'group_ids': [Command.link(cls.env.ref('base.group_user').id)],
'login': 'emp',
'name': 'Ernest Employee',
'notification_type': 'inbox',
@ -36,32 +35,49 @@ class TestPartner(TransactionCase):
'user_id': user.id,
} for user in cls.users])
cls.leave_type = cls.env['hr.leave.type'].create({
'requires_allocation': 'no',
'requires_allocation': False,
'name': 'Legal Leaves',
'time_type': 'leave',
'responsible_ids': cls.users.ids
})
cls.leaves = cls.env['hr.leave'].create([{
'date_from': cls.today + relativedelta(days=-2),
'date_to': cls.today + relativedelta(days=2),
'request_date_from': "2024-06-03",
'request_date_to': "2024-06-06",
'employee_id': cls.employees[0].id,
'holiday_status_id': cls.leave_type.id,
}, {
'date_from': cls.today + relativedelta(days=-2),
'date_to': cls.today + relativedelta(days=3),
'request_date_from': "2024-06-02",
'request_date_to': "2024-06-05",
'employee_id': cls.employees[1].id,
'holiday_status_id': cls.leave_type.id,
}])
cls.user_no_hr_access = new_test_user(
cls.env, login="user_no_hr_access",
)
def test_res_partner_mail_partner_format(self):
@freeze_time('2024-06-04')
def test_res_partner_to_store(self):
self.leaves.write({'state': 'validate'})
self.assertEqual(
self.partner.mail_partner_format()[self.partner]['out_of_office_date_end'],
(self.today + relativedelta(days=2)).strftime(DEFAULT_SERVER_DATE_FORMAT),
'Return date is the first return date of all users associated with a partner',
Store().add(self.partner).get_result()["hr.employee"][0]["leave_date_to"],
"2024-06-07",
"Return date is the return date of the main user of the partner",
)
self.leaves[1].action_refuse()
self.leaves[0].action_refuse()
self.assertEqual(
self.partner.mail_partner_format()[self.partner]['out_of_office_date_end'],
Store().add(self.partner).get_result()["hr.employee"][0]["leave_date_to"],
False,
'Partner is not considered out of office if one of their users is not on holiday',
"Partner is not considered out of office if their main user is not on holiday",
)
@freeze_time("2024-06-04")
@users("user_no_hr_access")
def test_res_partner_to_store_no_hr_access(self):
self.leaves.write({"state": "validate"})
data = Store().add(self.partner.with_user(self.user_no_hr_access)).get_result()
self.assertEqual(
data["hr.employee"][0]["leave_date_to"],
"2024-06-07",
"Return date is the return date of the main user of the partner, "
"even if the user has no access to the company",
)

View file

@ -0,0 +1,75 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from odoo import Command
from odoo.tests.common import tagged
from odoo.addons.hr_holidays.tests.common import TestHolidayContract
@tagged('post_install', '-at_install')
class TestResourceCalendar(TestHolidayContract):
def test_time_off_half_day_duration(self):
"""if working schedule is of full day period, Duration should be correct"""
new_calendar_40h = self.env['resource.calendar'].create({
'name': '40h calendar new',
'attendance_ids': [
Command.create({
'name': 'Monday full day',
'dayofweek': '0',
'hour_from': 10,
'hour_to': 18,
'day_period': 'full_day',
}),
Command.create({
'name': 'Tuesday full day',
'dayofweek': '1',
'hour_from': 10,
'hour_to': 18,
'day_period': 'full_day',
}),
],
})
self.jules_emp.version_id.resource_calendar_id = new_calendar_40h.id
leave_type = self.env['hr.leave.type'].create({
'name': 'Test half day type',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
'request_unit': 'half_day',
})
leave_morning, leave_afternoon, leave_one_and_half = self.env['hr.leave'].create([
{
'name': 'Half Day Leave(morning)',
'employee_id': self.jules_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': date(2025, 4, 21),
'request_date_to': date(2025, 4, 21),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
},
{
'name': 'Half Day Leave(afternoon)',
'employee_id': self.jules_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': date(2025, 4, 22),
'request_date_to': date(2025, 4, 22),
'request_date_from_period': 'pm',
'request_date_to_period': 'pm',
},
{
'name': 'One and half Day Leave',
'employee_id': self.jules_emp.id,
'holiday_status_id': leave_type.id,
'request_date_from': date(2025, 4, 14),
'request_date_to': date(2025, 4, 15),
'request_date_from_period': 'am',
'request_date_to_period': 'am',
},
])
self.assertEqual(leave_morning.number_of_days, 0.5)
self.assertEqual(leave_afternoon.number_of_days, 0.5)
self.assertEqual(leave_one_and_half.number_of_days, 1.5)

View file

@ -1,208 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from freezegun import freeze_time
from odoo import tests
from odoo.tests import new_test_user
from odoo.tests.common import Form, TransactionCase
from odoo.exceptions import ValidationError
@tests.tagged('access_rights', 'post_install', '-at_install')
class TestHrLeaveStressDays(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.default_calendar = cls.env['resource.calendar'].create({
'name': 'moon calendar',
})
cls.company = cls.env['res.company'].create({
'name': 'super company',
'resource_calendar_id': cls.default_calendar.id,
})
cls.employee_user = new_test_user(cls.env, login='user', groups='base.group_user', company_ids=[(6, 0, cls.company.ids)], company_id=cls.company.id)
cls.manager_user = new_test_user(cls.env, login='manager', groups='base.group_user,hr_holidays.group_hr_holidays_manager', company_ids=[(6, 0, cls.company.ids)], company_id=cls.company.id)
cls.employee_emp = cls.env['hr.employee'].create({
'name': 'Toto Employee',
'company_id': cls.company.id,
'user_id': cls.employee_user.id,
'resource_calendar_id': cls.default_calendar.id,
})
cls.manager_emp = cls.env['hr.employee'].create({
'name': 'Toto Mananger',
'company_id': cls.company.id,
'user_id': cls.manager_user.id,
})
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Unlimited',
'leave_validation_type': 'hr',
'requires_allocation': 'no',
'company_id': cls.company.id,
})
cls.stress_day = cls.env['hr.leave.stress.day'].create({
'name': 'Super Event',
'company_id': cls.company.id,
'start_date': datetime(2021, 11, 2),
'end_date': datetime(2021, 11, 2),
'color': 1,
'resource_calendar_id': cls.default_calendar.id,
})
cls.stress_week = cls.env['hr.leave.stress.day'].create({
'name': 'Super Event End Of Week',
'company_id': cls.company.id,
'start_date': datetime(2021, 11, 8),
'end_date': datetime(2021, 11, 12),
'color': 2,
'resource_calendar_id': cls.default_calendar.id,
})
@freeze_time('2021-10-15')
def test_request_stress_days(self):
# An employee can request time off outside stress days
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'date_from': datetime(2021, 11, 3),
'date_to': datetime(2021, 11, 3),
'number_of_days': 1,
})
# Taking a time off during a Stress Day is not allowed for a simple employee...
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'date_from': datetime(2021, 11, 3),
'date_to': datetime(2021, 11, 17),
'number_of_days': 1,
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'date_from': datetime(2021, 11, 9),
'date_to': datetime(2021, 11, 9),
'number_of_days': 1,
})
# ... but is allowed for a Time Off Officer
self.env['hr.leave'].with_user(self.manager_user.id).create({
'name': 'coucou',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'date_from': datetime(2021, 11, 2),
'date_to': datetime(2021, 11, 2),
'number_of_days': 1,
})
@freeze_time('2021-10-15')
def test_get_stress_days(self):
stress_days = self.employee_emp.get_stress_days('2021-11-01', '2021-11-30')
# Stress Days spanning multiple days should be split in single days
expected_data = {'2021-11-02': 1, '2021-11-08': 2, '2021-11-09': 2, '2021-11-10': 2, '2021-11-11': 2, '2021-11-12': 2}
self.assertEqual(len(stress_days), len(expected_data))
for day, color in expected_data.items():
self.assertTrue(day in stress_days)
self.assertEqual(color, stress_days[day])
with self.assertRaises(ValidationError), Form(self.env['hr.leave'].with_user(self.employee_user.id).with_context(default_employee_id=self.employee_emp.id)) as leave_form:
leave_form.holiday_status_id = self.leave_type
leave_form.request_date_from = datetime(2021, 11, 1)
leave_form.request_date_to = datetime(2021, 11, 1)
self.assertFalse(leave_form.has_stress_day)
leave_form.request_date_to = datetime(2021, 11, 5)
self.assertTrue(leave_form.has_stress_day)
@freeze_time('2021-10-15')
def test_department_stress_days(self):
production_department = self.env['hr.department'].create({
'name': 'Production Department',
'company_id': self.company.id,
})
post_production_department = self.env['hr.department'].create({
'name': 'Post-Production Department',
'company_id': self.company.id,
'parent_id': production_department.id,
})
deployment_department = self.env['hr.department'].create({
'name': 'Deployment Department',
'company_id': self.company.id,
'parent_id': production_department.id,
})
self.employee_emp.write({
'department_id': post_production_department.id
})
# Create one stress day for each department
self.env['hr.leave.stress.day'].create({
'name': 'Last Rush Before Launch (production)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 3),
'end_date': datetime(2021, 11, 3),
'color': 1,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [production_department.id],
})
self.env['hr.leave.stress.day'].create({
'name': 'Last Rush Before Launch (post-production)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 4),
'end_date': datetime(2021, 11, 4),
'color': 1,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [post_production_department.id],
})
self.env['hr.leave.stress.day'].create({
'name': 'Last Rush Before Launch (deployment)',
'company_id': self.company.id,
'start_date': datetime(2021, 11, 5),
'end_date': datetime(2021, 11, 5),
'color': 1,
'resource_calendar_id': self.default_calendar.id,
'department_ids': [deployment_department.id],
})
# The employee should only be able to create a time off on stress days
# that do not include his department
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'have been given the black spot',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'date_from': datetime(2021, 11, 3),
'date_to': datetime(2021, 11, 3),
'number_of_days': 1,
})
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'have been given the black spot',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'date_from': datetime(2021, 11, 4),
'date_to': datetime(2021, 11, 4),
'number_of_days': 1,
})
self.env['hr.leave'].with_user(self.employee_user.id).create({
'name': 'have been given the black spot',
'holiday_status_id': self.leave_type.id,
'employee_id': self.employee_emp.id,
'date_from': datetime(2021, 11, 5),
'date_to': datetime(2021, 11, 5),
'number_of_days': 1,
})

View file

@ -0,0 +1,8 @@
from odoo.tests import HttpCase, tagged
@tagged("post_install", "-at_install")
class TestTimeOffAllocationTour(HttpCase):
def test_time_off_allocation_warning_tour(self):
self.start_tour("/", "time_off_allocation_warning_tour", login="admin")

View file

@ -0,0 +1,20 @@
from odoo.tests import HttpCase, tagged, users
@tagged('post_install', '-at_install')
class TestTimeOffCardTour(HttpCase):
@users('admin')
def test_time_off_card_tour(self):
leave_type = self.env['hr.leave.type'].create({
'name': 'Time Off with no validation for approval',
'time_type': 'leave',
'requires_allocation': True,
'allocation_validation_type': 'no_validation',
})
self.env['hr.leave.allocation'].create({
'employee_id': self.env.user.employee_id.id,
'holiday_status_id': leave_type.id,
'allocation_type': 'regular',
'type_request_unit': 'half_day',
})
self.start_tour('/', 'time_off_card_tour', login='admin')

View file

@ -0,0 +1,8 @@
from odoo.tests import HttpCase, tagged, users
@tagged('post_install', '-at_install')
class TestTimeOffGraphViewTour(HttpCase):
@users('admin')
def test_time_off_graph_view_tour(self):
self.start_tour('/', 'time_off_graph_view_tour', login='admin')

View file

@ -0,0 +1,35 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
class TestTimeoffEvent(TestHrHolidaysCommon):
def test_no_videocall_url_in_timeoff_event(self):
""" Test that the timeoff event does not need a video call """
self.hr_leave_type = self.env['hr.leave.type'].with_user(self.user_hrmanager).create({
'name': 'Time Off Type',
'requires_allocation': False,
})
self.holiday = self.env['hr.leave'].with_context(mail_create_nolog=True, mail_notrack=True).with_user(self.user_employee).create({
'name': 'Time Off 1 sura',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.hr_leave_type.id,
'request_date_from': datetime(2020, 1, 15),
'request_date_to': datetime(2020, 1, 15) + relativedelta(days=1),
})
self.holiday.with_user(self.user_hrmanager).action_approve()
# Finding the event corresponding to the leave
search_criteria = [
('name', 'like', self.holiday.employee_id.name),
('start_date', '>=', self.holiday.request_date_from),
('stop_date', '<=', self.holiday.request_date_to),
]
timeoff_event = self.env['calendar.event'].search(search_criteria)
self.assertTrue(timeoff_event, "The timeoff event should exist")
self.assertFalse(timeoff_event._need_video_call(), "The timeoff event does not need a video call")

View file

@ -15,7 +15,7 @@ class TestHrLeaveUninstall(TransactionCase):
holiday = self.env['hr.leave'].create({
'name': 'Time Off',
'employee_id': employee.id,
'holiday_status_id': self.env.ref('hr_holidays.holiday_status_sl').id,
'holiday_status_id': self.env.ref('hr_holidays.leave_type_sick_time_off').id,
'request_date_from': date(2020, 1, 7),
'date_from': date(2020, 1, 7),
'request_date_to': date(2020, 1, 9),

View file

@ -0,0 +1,144 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from odoo.addons.hr_calendar.tests.common import TestHrCalendarCommon
from odoo.tests import tagged
@tagged('work_hours')
class TestWorkingHours(TestHrCalendarCommon):
""" Test global leaves for a whole company, conflict resolutions """
@classmethod
def setUpClass(cls):
super().setUpClass()
# YTI TODO: Those tests seem to be never launched from now.
if 'hr.version' in cls.env:
cls.skipTest(cls,
"hr_contract module is installed. To test these features you need to install hr_holidays_contract"
)
cls.leave_type = cls.env['hr.leave.type'].create({
'name': 'Unpaid Time Off',
'requires_allocation': False,
'leave_validation_type': 'no_validation',
})
def test_multi_companies_2_employees_2_selected_companies_holidays(self):
"""
INPUT:
======
Employees Companies
employee A company A ---> 35h [X] A <- main company
employee A company B ---> 28h [X] B
employee A company A take a day off for monday and tuesday.
OUTPUT:
=======
The schedule will be : off on monday, following 28h schedule on tuesday and the union for the rest of the week.
"""
self.env.user.company_id = self.company_A
self.env.user.company_ids = [self.company_A.id, self.company_B.id]
self.env['hr.leave'].create({
'name': 'holiday from monday to tuesday',
'employee_id': self.employeeA.id,
'holiday_status_id': self.leave_type.id,
'request_date_from': datetime(2023, 12, 25),
'request_date_to': datetime(2023, 12, 26, 23, 59, 59),
})
work_hours = self.env['res.partner'].get_working_hours_for_all_attendees(
[self.partnerA.id],
datetime(2023, 12, 25).isoformat(),
datetime(2023, 12, 31).isoformat()
)
self.assertEqual(work_hours, [
{'daysOfWeek': [2], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [2], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [3], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [3], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [4], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [4], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [5], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [5], 'startTime': '13:00', 'endTime': '16:00'},
])
def test_multi_companies_2_employees_2_selected_companies_company_holidays(self):
"""
INPUT:
======
Employees Companies
employee A company A ---> 35h [X] A <- main company
employee A company B ---> 28h [X] B
Company A give a day off for everyone on monday and tuesday.
OUTPUT:
=======
The schedule will be : off on monday, following 28h schedule on tuesday and the union for the rest of the week.
"""
self.env.user.company_id = self.company_A
self.env.user.company_ids = [self.company_A.id, self.company_B.id]
company_leave = self.env['hr.leave.generate.multi.wizard'].create({
'name': 'holiday from monday to tuesday',
'allocation_mode': 'company',
'company_id': self.company_A.id,
'holiday_status_id': self.leave_type.id,
'date_from': date(2023, 12, 25),
'date_to': date(2023, 12, 26),
})
company_leave.action_generate_time_off()
work_hours = self.env['res.partner'].get_working_hours_for_all_attendees(
[self.partnerA.id],
datetime(2023, 12, 25).isoformat(),
datetime(2023, 12, 31).isoformat()
)
self.assertEqual(work_hours, [
{'daysOfWeek': [2], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [2], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [3], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [3], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [4], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [4], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [5], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [5], 'startTime': '13:00', 'endTime': '16:00'},
])
def test_multi_companies_2_employees_2_selected_companies_global_holidays(self):
"""
INPUT:
======
Employees Companies
employee A company A ---> 35h [X] A <- main company
employee A company B ---> 28h [X] B
Global leave for calendar 35h on monday and tuesday.
OUTPUT:
=======
The schedule will be : off on monday, following 28h schedule on tuesday and the union for the rest of the week.
"""
self.env.user.company_id = self.company_A
self.env.user.company_ids = [self.company_A.id, self.company_B.id]
self.env['resource.calendar.leaves'].create({
'name': 'Global Time Off',
'date_from': datetime(2023, 12, 25),
'date_to': datetime(2023, 12, 26, 23, 59, 59),
'calendar_id': self.calendar_35h.id,
})
work_hours = self.env['res.partner'].get_working_hours_for_all_attendees(
[self.partnerA.id],
datetime(2023, 12, 25).isoformat(),
datetime(2023, 12, 31).isoformat()
)
self.assertEqual(work_hours, [
{'daysOfWeek': [2], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [2], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [3], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [3], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [4], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [4], 'startTime': '13:00', 'endTime': '16:00'},
{'daysOfWeek': [5], 'startTime': '08:00', 'endTime': '12:00'},
{'daysOfWeek': [5], 'startTime': '13:00', 'endTime': '16:00'},
])