236 lines
11 KiB
Python
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))
|