Files

236 lines
11 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from pytz import UTC
from odoo.addons.hr_holidays.tests.common import TestHolidayContract
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo import Command
class TestHrLeaveGantt(TestHolidayContract):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.jules_emp.tz = 'UTC'
def test_gantt_view_without_schedule(self):
"""
Test that the Time Off Gantt view works correctly even
when an employee contract has no working schedule set.
"""
# Remove the working schedule (resource calendar) from the employee's contract
self.contract_cdi.write({
'date_start': datetime.strptime('2025-01-01', '%Y-%m-%d').date(),
'date_end': datetime.strptime('2025-12-31', '%Y-%m-%d').date(),
'resource_calendar_id': None
})
# Create a time off for the employee
start = datetime.strptime('2025-11-11 07:00:00', '%Y-%m-%d %H:%M:%S')
end = datetime.strptime('2025-11-11 18:00:00', '%Y-%m-%d %H:%M:%S')
self.create_leave(start, end, name="Doctor Appointment", employee_id=self.jules_emp.id)
# Get the Gantt overview data for employees's time off data
start_date = '2025-11-01 00:00:00'
stop_date = '2025-11-30 00:00:00'
gantt_domain = [('employee_id.active', '=', True)]
gantt_data = self.env["hr.leave.report.calendar"].get_gantt_data(
gantt_domain,
["employee_id"],
{},
unavailability_fields=["employee_id"],
start_date=start_date,
stop_date=stop_date,
scale="day",
)
group_employee_ids = [group['employee_id'][0] for group in gantt_data['groups']]
self.assertIn(self.jules_emp.id, group_employee_ids, "The employee should be in the gantt data groups.")
def test_gantt_view_flex_schedule_time_off_types(self):
"""Checks that the correct areas are grayed out when an employee with a flexible schedule takes days off
when the type of these days off are either half-days or hours."""
leave_type_half_day, leave_type_hours = self.env['hr.leave.type'].create([{
'name': 'Paid Time Off Half',
'requires_allocation': False,
'request_unit': 'half_day',
'allocation_validation_type': 'no_validation',
}, {
'name': 'Paid Time Off Hour',
'requires_allocation': False,
'request_unit': 'hour',
'allocation_validation_type': 'no_validation',
}])
half_day_leave, hours_leave = self.env['hr.leave'].create([{
'name': 'Half Day Leave',
'holiday_status_id': leave_type_half_day.id,
'employee_id': self.jules_emp.id,
'request_date_from': datetime(2026, 3, 24),
'request_date_to': datetime(2026, 3, 24),
'request_unit_half': True,
'request_date_from_period': 'am',
'request_date_to_period': 'am'
}, {
'name': 'Hour Leave',
'holiday_status_id': leave_type_hours.id,
'employee_id': self.jules_emp.id,
'request_date_from': datetime(2026, 3, 25),
'request_date_to': datetime(2026, 3, 25),
'request_unit_hours': True,
'request_hour_from': 12.0,
'request_hour_to': 16.0,
}])
flex_schedule = self.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'flexible_hours': True,
'leave_ids': [
Command.create({
'name': 'half day Leave',
'date_from': half_day_leave.request_date_from,
'date_to': half_day_leave.request_date_to,
'resource_id': self.jules_emp.resource_id.id,
'holiday_id': half_day_leave.id
}),
Command.create({
'name': 'hours Leave',
'date_from': hours_leave.request_date_from,
'date_to': hours_leave.request_date_to,
'resource_id': self.jules_emp.resource_id.id,
'holiday_id': hours_leave.id
})
]
})
self.jules_emp.write({'resource_calendar_id': flex_schedule.id})
gantt_domain = [('employee_id.active', '=', True)]
start_date_half = '2026-03-24 00:00:00'
stop_date_half = '2026-03-24 23:59:59'
gantt_data_half = self.env["hr.leave.report.calendar"].get_gantt_data(
gantt_domain,
["employee_id"],
{},
unavailability_fields=["employee_id"],
start_date=start_date_half,
stop_date=stop_date_half,
scale="day",
)
self.assertEqual(gantt_data_half['unavailabilities']['employee_id'][self.jules_emp.id][0]['start'],
datetime(2026, 3, 24, 0, 0, tzinfo=UTC))
self.assertEqual(gantt_data_half['unavailabilities']['employee_id'][self.jules_emp.id][0]['stop'],
datetime(2026, 3, 24, 12, 0, tzinfo=UTC))
start_date_hour = '2026-03-25 00:00:00'
stop_date_hour = '2026-03-25 23:59:59'
gantt_data_hour = self.env["hr.leave.report.calendar"].get_gantt_data(
gantt_domain,
["employee_id"],
{},
unavailability_fields=["employee_id"],
start_date=start_date_hour,
stop_date=stop_date_hour,
scale="day",
)
self.assertEqual(gantt_data_hour['unavailabilities']['employee_id'][self.jules_emp.id][0]['start'],
datetime(2026, 3, 25, 12, 0, tzinfo=UTC))
self.assertEqual(gantt_data_hour['unavailabilities']['employee_id'][self.jules_emp.id][0]['stop'],
datetime(2026, 3, 25, 23, 59, 59, tzinfo=UTC))
# Verify basic user can compute unavailability
basic_user = mail_new_test_user(
self.env, login='marc_demo',
groups='base.group_user',
)
self.env['hr.employee'].create({
'name': 'Marc Demo',
'user_id': basic_user.id,
})
start = datetime(2026, 3, 24, 0, 0, tzinfo=UTC)
stop = datetime(2026, 3, 24, 23, 59, 59, tzinfo=UTC)
unavailable_intervals = self.jules_emp.resource_id.with_user(basic_user)._get_unavailable_intervals(start, stop)
self.assertTrue(unavailable_intervals.get(self.jules_emp.resource_id.id))
self.assertEqual(unavailable_intervals[self.jules_emp.resource_id.id][0][0], datetime(2026, 3, 24, 0, 0, tzinfo=UTC))
self.assertEqual(unavailable_intervals[self.jules_emp.resource_id.id][0][1], datetime(2026, 3, 24, 12, 0, tzinfo=UTC))
def test_gantt_view_flex_schedule_partial_hours_leave(self):
"""A flexible-schedule leave of a few hours (neither half nor full day) must gray out
only its requested hours, not the whole day."""
leave_type_hours = self.env['hr.leave.type'].create({
'name': 'Paid Time Off Hour',
'requires_allocation': False,
'request_unit': 'hour',
'allocation_validation_type': 'no_validation',
})
hours_leave = self.env['hr.leave'].create({
'name': 'Hour Leave',
'holiday_status_id': leave_type_hours.id,
'employee_id': self.jules_emp.id,
'request_date_from': datetime(2026, 3, 25),
'request_date_to': datetime(2026, 3, 25),
'request_unit_hours': True,
'request_hour_from': 9.0,
'request_hour_to': 11.0,
})
flex_schedule = self.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'flexible_hours': True,
'leave_ids': [Command.create({
'name': 'hours Leave',
'date_from': hours_leave.request_date_from,
'date_to': hours_leave.request_date_to,
'resource_id': self.jules_emp.resource_id.id,
'holiday_id': hours_leave.id,
})],
})
self.jules_emp.write({'resource_calendar_id': flex_schedule.id})
gantt_data = self.env["hr.leave.report.calendar"].get_gantt_data(
[('employee_id.active', '=', True)],
["employee_id"],
{},
unavailability_fields=["employee_id"],
start_date='2026-03-25 00:00:00',
stop_date='2026-03-25 23:59:59',
scale="day",
)
unavailability = gantt_data['unavailabilities']['employee_id'][self.jules_emp.id][0]
self.assertEqual(unavailability['start'], datetime(2026, 3, 25, 9, 0, tzinfo=UTC))
self.assertEqual(unavailability['stop'], datetime(2026, 3, 25, 11, 0, tzinfo=UTC))
def test_gantt_view_duration_based_schedule_with_leaves(self):
"""
A duration-based-schedule leave of full day must gray out
the whole day not just the amount of hours in the calendar
"""
duration_calendar = self.env['resource.calendar'].create({
'name': 'Duration-based 8h Calendar',
'duration_based': True,
'hours_per_day': 8.0,
'tz': 'UTC',
'attendance_ids': [
(0, 0, {'name': 'Monday', 'dayofweek': '0', 'duration_hours': 8, 'day_period': 'full_day'}),
(0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'duration_hours': 8, 'day_period': 'full_day'}),
(0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'duration_hours': 8, 'day_period': 'full_day'}),
(0, 0, {'name': 'Thursday', 'dayofweek': '3', 'duration_hours': 8, 'day_period': 'full_day'}),
(0, 0, {'name': 'Friday', 'dayofweek': '4', 'duration_hours': 8, 'day_period': 'full_day'}),
],
})
self.jules_emp.write({'resource_calendar_id': duration_calendar.id})
self.create_leave(date(2026, 3, 17), date(2026, 3, 18), employee_id=self.jules_emp.id).action_approve()
unavailabilities = self.env['hr.leave']._gantt_unavailability(
'employee_id',
[self.jules_emp.id],
datetime(2026, 3, 16, 0, 0, tzinfo=UTC),
datetime(2026, 3, 20, 0, 0, tzinfo=UTC),
'day',
)
self.assertEqual(unavailabilities[self.jules_emp.id][0]['start'], datetime(2026, 3, 17, 0, 0, tzinfo=UTC))
self.assertEqual(unavailabilities[self.jules_emp.id][0]['stop'], datetime(2026, 3, 19, 0, 0, tzinfo=UTC))