# Part of Odoo. See LICENSE file for full copyright and licensing details. import pytz from datetime import datetime from odoo.tests import Form from odoo.tests.common import TransactionCase class TestResourceCalendar(TransactionCase): def test_fully_flexible_attendance_interval_duration(self): """ Test that the duration of a fully flexible attendance interval is correctly computed. """ calendar = self.env['resource.calendar'].create({ 'name': 'Standard Calendar', 'two_weeks_calendar': False, }) resource = self.env['resource.resource'].create({ 'name': 'Wade Wilson', 'calendar_id': False, # Fully-flexible because no calendar is set 'tz': 'America/New_York', # -04:00 UTC offset in the summer }) self.env['resource.calendar.attendance'].create({ 'name': 'TEMP', 'calendar_id': calendar.id, 'dayofweek': '2', # Wednesday 'hour_from': 14, # 18:00 UTC 'hour_to': 17, # 21:00 UTC }) UTC = pytz.timezone('UTC') start_dt = datetime(2025, 6, 4, 18, 0, 0).astimezone(UTC) end_dt = datetime(2025, 6, 4, 21, 0, 0).astimezone(UTC) result_per_resource_id = calendar._attendance_intervals_batch( start_dt, end_dt, resource ) start, end, attendance = result_per_resource_id[resource.id]._items[0] # For a flexible resource, we expect the output times to match the # input times exactly, since the resource has no fixed calendar. # Further, the dummy attendance that is created should have a duration # equal to the difference between the start and end times. self.assertEqual(start, start_dt, "Output start time should match the input start time") self.assertEqual(end, end_dt, "Output end time should match the input end time") self.assertEqual(attendance.duration_hours, 3.0, "Attendance duration should be 3 hours") self.assertEqual(attendance.duration_days, 0.125, "Attendance duration should be 0.125 days (3 hours)") def test_flexible_calendar_attendance_interval_duration(self): """ Test that the duration of an attendance interval for flexible calendar is correctly computed. """ flexible_calendar = self.env['resource.calendar'].create({ 'name': 'Flexible Calendar', 'hours_per_day': 7.0, 'hours_per_week': 30, 'full_time_required_hours': 30, 'flexible_hours': True, 'tz': 'UTC', }) # Case 1: get attendances for the full week. # Expected: 7-7-7-7-2 (30 hours total) expected_hours = [7, 7, 7, 7, 2] start_dt = datetime(2025, 6, 2, 0, 0, 0).astimezone(pytz.UTC) end_dt = datetime(2025, 6, 7, 23, 59, 59).astimezone(pytz.UTC) result_per_resource_id = flexible_calendar._attendance_intervals_batch( start_dt, end_dt ) self.assertEqual(expected_hours, [(end - start).total_seconds() / 3600 for start, end, dummy_attendance in result_per_resource_id[0]._items]) self.assertEqual(expected_hours, [dummy_attendance.duration_hours for start, end, dummy_attendance in result_per_resource_id[0]._items]) # Case 2: check attendances are all contained between start_dt and end_dt start_dt = datetime(2025, 6, 2, 11, 0, 0).astimezone(pytz.UTC) end_dt = datetime(2025, 6, 7, 13, 0, 0).astimezone(pytz.UTC) result_per_resource_id = flexible_calendar._attendance_intervals_batch( start_dt, end_dt ) self.assertTrue(start_dt <= result_per_resource_id[0]._items[0][0], "First attendance interval should not start before start_dt") self.assertTrue(end_dt >= result_per_resource_id[0]._items[4][1], "Last attendance interval should not end after end_dt") def test_public_holiday_calendar_no_company(self): self.env['resource.calendar.leaves'].create([{ 'name': "Public Holiday for company", 'company_id': self.env.company.id, 'date_from': datetime(2019, 5, 29, 0, 0, 0), 'date_to': datetime(2019, 5, 30, 0, 0, 0), 'resource_id': False, 'time_type': "leave", }]) calendar = self.env['resource.calendar'].create({ 'name': '40 hours/week', 'hours_per_day': 8, 'full_time_required_hours': 40, }) calendar.company_id = False date_from = datetime(2019, 5, 27, 0, 0, 0).astimezone(pytz.UTC) date_to = datetime(2019, 5, 31, 23, 59, 59).astimezone(pytz.UTC) days = calendar._get_unusual_days(date_from, date_to, self.env.company) expected_res = { '2019-05-27': False, '2019-05-28': False, '2019-05-29': True, '2019-05-30': False, '2019-05-31': False, } self.assertEqual(days, expected_res) def test_resource_calendar_form_view(self): calendar = self.env['resource.calendar'].create({ 'name': 'Test calendar', 'attendance_ids': [(5, 0, 0), (0, 0, { 'name': 'Monday', 'hour_from': 8, 'hour_to': 16, 'day_period': 'full_day', 'dayofweek': '0', }), (0, 0, { 'name': 'Tuesday', 'hour_from': 8, 'hour_to': 16, 'day_period': 'full_day', 'dayofweek': '1', }), (0, 0, { 'name': 'Wednesday', 'hour_from': 8, 'hour_to': 16, 'day_period': 'full_day', 'dayofweek': '2', })], }) calendar_form = Form(calendar) self.assertEqual(calendar_form.hours_per_day, 8) self.assertEqual(calendar_form.hours_per_week, 24) with calendar_form.attendance_ids.edit(0) as line_form: line_form.hour_from = 11 line_form.save() # The calendar form values should be recomputed self.assertEqual(calendar_form.hours_per_day, 7) self.assertEqual(calendar_form.hours_per_week, 21) calendar_form.save() self.assertEqual(calendar.hours_per_day, 7) self.assertEqual(calendar.hours_per_week, 21) def test_resource_calendar_form_view_duration_based(self): calendar = self.env['resource.calendar'].create({ 'name': 'Test calendar', 'attendance_ids': [(5, 0, 0), (0, 0, { 'name': 'Monday', 'hour_from': 8, 'hour_to': 16, 'day_period': 'full_day', 'dayofweek': '0', }), (0, 0, { 'name': 'Tuesday', 'hour_from': 8, 'hour_to': 16, 'day_period': 'full_day', 'dayofweek': '1', }), (0, 0, { 'name': 'Wednesday', 'hour_from': 8, 'hour_to': 16, 'day_period': 'full_day', 'dayofweek': '2', })], }) calendar.duration_based = True calendar_form = Form(calendar) self.assertEqual(calendar_form.hours_per_day, 8) self.assertEqual(calendar_form.hours_per_week, 24) with calendar_form.attendance_ids.edit(0) as line_form: line_form.duration_hours = 5 line_form.save() # The calendar form values should be recomputed self.assertEqual(calendar_form.hours_per_day, 7) self.assertEqual(calendar_form.hours_per_week, 21) calendar_form.save() self.assertEqual(calendar.hours_per_day, 7) self.assertEqual(calendar.hours_per_week, 21)