# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import date, datetime, timedelta from freezegun import freeze_time from odoo.addons.appointment_hr.tests.common import AppointmentHrCommon from odoo.exceptions import ValidationError from odoo.tests import tagged, users class AppointmentHRLeavesTest(AppointmentHrCommon): @users('apt_manager', 'staff_user_bxls') def test_partner_on_leave_with_calendar_leave(self): """Check that resource leaves are correctly reflected in the partners_on_leave field. Overlapping times between the leave time of an employee and the meeting should add the partner to the list of unavailable partners. """ self.env['calendar.event'].search([('user_id', '=', self.staff_user_bxls.id)]).unlink() self.env['resource.calendar.leaves'].sudo().search([('calendar_id', '=', self.staff_user_bxls.resource_calendar_id.id)]).unlink() [meeting] = self._create_meetings( self.staff_user_bxls, [(self.reference_monday + timedelta(days=1), self.reference_monday + timedelta(days=1, hours=3), False, )], self.apt_type_bxls_2days.id ) self.assertFalse(meeting.partners_on_leave) self.env['resource.calendar.leaves'].sudo().create({ 'calendar_id': self.staff_user_bxls.resource_calendar_id.id, 'date_from': self.reference_monday + timedelta(days=1), 'date_to': self.reference_monday + timedelta(days=1, minutes=5), 'name': 'Tuesday Morning Leave' }) # a sane depedency cannot be expressed so it will only be updated when removed from cache meeting.invalidate_recordset() self.assertEqual(meeting.partners_on_leave, self.staff_user_bxls.partner_id) @users('apt_manager', 'staff_user_bxls') def test_partner_on_leave_with_conflicting_event(self): """Check that conflicting meetings are correctly reflected in the partners_on_leave field. Overlapping times between any other meeting of the employee and the meeting should add the partner to the list of unavailable partners. """ self.env['calendar.event'].search([('user_id', '=', self.staff_user_bxls.id)]).unlink() self.env['resource.calendar.leaves'].sudo().search([('calendar_id', '=', self.staff_user_bxls.resource_calendar_id.id)]).unlink() [meeting] = self._create_meetings( self.staff_user_bxls, [(self.reference_monday, self.reference_monday + timedelta(hours=3), False, )], self.apt_type_bxls_2days.id ) self.assertFalse(meeting.partners_on_leave) [conflicting_meeting] = self._create_meetings( self.staff_user_bxls, [(self.reference_monday, self.reference_monday + timedelta(minutes=5), False, )], ) meeting.invalidate_recordset() self.assertEqual(meeting.partners_on_leave, self.staff_user_bxls.partner_id) self.assertFalse(conflicting_meeting.partners_on_leave) @tagged('appointment_slots') class AppointmentHrTest(AppointmentHrCommon): @classmethod def setUpClass(cls): super(AppointmentHrTest, cls).setUpClass() cls.apt_type_bxls_2days.write({ 'work_hours_activated': True, }) @users('apt_manager') def test_generate_slots_recurring(self): """ Generates recurring slots, check begin and end slot boundaries. """ apt_type = self.apt_type_bxls_2days.with_user(self.env.user) with freeze_time(self.reference_now): slots = apt_type._get_appointment_slots('Europe/Brussels') self.assertSlots( slots, [{'name_formated': 'February 2022', 'month_date': datetime(2022, 2, 1), 'weeks_count': 5, # 31/01 -> 28/02 (06/03) } ], {'enddate': self.global_slots_enddate, 'startdate': self.reference_now_monthweekstart, 'slots_start_hours': [8, 9, 10, 11, 13], # based on appointment type start hours of slots but 12 is pause midi 'slots_startdate': self.reference_monday.date(), # first Monday after reference_now 'slots_weekdays_nowork': range(2, 7) # working hours only on Monday/Tuesday (0, 1) } ) @users('apt_manager') def test_generate_slots_recurring_midnight(self): """ Generates recurring slots, check around midnight """ late_night_cal = self.env['resource.calendar'].sudo().create({ 'attendance_ids': [(0, 0, { 'dayofweek': f'{day - 1}', 'hour_from': hour, 'hour_to': hour + 4, 'name': f'Day {day} H {hour} {hour + 4}', }) for hour in [0, 20] for day in [1, 2, 3, 4]], 'company_id': self.company_admin.id, 'name': '00:00-04:00 and 20:00-00:00 on Mondays through Thursday', 'tz': 'UTC', }) # Current employee is Europe/Brussels current_employee_sudo = self.env.user.employee_id.sudo() current_employee_sudo.resource_calendar_id = late_night_cal current_employee_sudo.tz = 'UTC' apt_type = self.env['appointment.type'].create({ 'appointment_duration': 1, 'appointment_tz': 'UTC', 'category': 'recurring', 'name': 'Midnight Test', 'max_schedule_days': 4, 'min_cancellation_hours': 1, 'min_schedule_hours': 0, 'slot_ids': [ (0, False, {'weekday': '1', 'start_hour': hour, 'end_hour': (hour + 1) % 24, }) for hour in [0, 1, 2, 21, 22, 23] ] + [ (0, False, {'weekday': '2', 'start_hour': hour, 'end_hour': hour + 1, }) for hour in [0, 1, 2] ] + [ (0, False, {'weekday': '2', 'start_hour': 20, 'end_hour': 00, }) ] + [ (0, False, {'weekday': '3', 'start_hour': hour, 'end_hour': (hour + 5) % 24, }) for hour in [0, 19] ], 'staff_user_ids': self.env.user.ids, 'work_hours_activated': True, }) # freeze the day before, early enough to be able to schedule the first hour with freeze_time(self.reference_monday.replace(hour=1, minute=36)): slots = apt_type._get_appointment_slots('UTC') self.assertSlots( slots, [{'name_formated': 'February 2022', 'month_date': datetime(2022, 2, 1), 'weeks_count': 5, # 31/01 -> 28/02 (06/03) } ], {'enddate': self.global_slots_enddate, 'startdate': self.reference_now_monthweekstart, 'slots_day_specific': {self.reference_monday.date(): [ {'start': 2, 'end': 3}, # 02:00 is the first valid slot when the current time is 01:36 {'start': 21, 'end': 22}, {'start': 22, 'end': 23}, {'start': 23, 'end': 00}, ], (self.reference_monday + timedelta(days=1)).date(): [ {'start': 0, 'end': 1}, {'start': 1, 'end': 2}, {'start': 2, 'end': 3}, {'start': 20, 'end': 21}, {'start': 21, 'end': 22}, {'start': 22, 'end': 23}, {'start': 23, 'end': 00}, ], (self.reference_monday + timedelta(days=2)).date(): [ {'start': 0, 'end': 1}, {'start': 1, 'end': 2}, {'start': 2, 'end': 3}, {'start': 3, 'end': 4}, # slots start at 20 and end at 4 because of resource schedule {'start': 20, 'end': 21}, {'start': 21, 'end': 22}, {'start': 22, 'end': 23}, {'start': 23, 'end': 00}, ]}, 'slots_startdate': self.reference_monday, } ) @users('apt_manager') def test_generate_slots_recurring_UTC(self): """ Generates recurring slots, check begin and end slot boundaries. Force UTC results event if everything is Europe/Brussels based. """ apt_type = self.apt_type_bxls_2days.with_user(self.env.user) with freeze_time(self.reference_now): slots = apt_type._get_appointment_slots('UTC') self.assertSlots( slots, [{'name_formated': 'February 2022', 'month_date': datetime(2022, 2, 1), 'weeks_count': 5, # 31/01 -> 28/02 (06/03) } ], {'enddate': self.global_slots_enddate, 'startdate': self.reference_now_monthweekstart, 'slots_start_hours': [7, 8, 9, 10, 12], # based on appointment type start hours of slots but 12 is pause midi 'slots_startdate': self.reference_monday.date(), # first Monday after reference_now 'slots_weekdays_nowork': range(2, 7) # working hours only on Monday/Tuesday (0, 1) } ) @users('apt_manager') def test_generate_slots_recurring_wleaves(self): """ Generates recurring slots, check begin and end slot boundaries with leaves involved. """ apt_type = self.apt_type_bxls_2days.with_user(self.env.user) # create personal leaves _leaves = self._create_leaves( self.staff_user_bxls, [(self.reference_monday + timedelta(days=1), # 2 hours first Tuesday self.reference_monday + timedelta(days=1, hours=2)), (self.reference_monday + timedelta(days=7), # next Monday: one hour self.reference_monday + timedelta(days=7, hours=1)) ], ) # add global leaves _leaves += self._create_leaves( self.env['res.users'], [(self.reference_monday + timedelta(days=8), # next Tuesday is bank holiday self.reference_monday + timedelta(days=8, hours=12)) ], calendar=self.staff_user_bxls.resource_calendar_id, ) with freeze_time(self.reference_now): slots = apt_type._get_appointment_slots('Europe/Brussels') self.assertSlots( slots, [{'name_formated': 'February 2022', 'month_date': datetime(2022, 2, 1), 'weeks_count': 5, # 31/01 -> 28/02 (06/03) } ], {'enddate': self.global_slots_enddate, 'startdate': self.reference_now_monthweekstart, 'slots_day_specific': { (self.reference_monday + timedelta(days=1)).date(): [ {'end': 11, 'start': 10}, {'end': 12, 'start': 11}, {'end': 14, 'start': 13} ], # leaves on 7-9 UTC (self.reference_monday + timedelta(days=7)).date(): [ {'end': 10, 'start': 9}, {'end': 11, 'start': 10}, {'end': 12, 'start': 11}, {'end': 14, 'start': 13} ], # leaves on 7-8 (self.reference_monday + timedelta(days=8)).date(): [], # 12 hours long bank holiday }, 'slots_start_hours': [8, 9, 10, 11, 13], # based on appointment type start hours of slots but 12 is pause midi 'slots_startdate': self.reference_monday.date(), # first Monday after reference_now 'slots_weekdays_nowork': range(2, 7) # working hours only on Monday/Tuesday (0, 1) } ) @users('apt_manager') def test_generate_slots_recurring_wmeetings(self): """ Generates recurring slots, check begin and end slot boundaries with leaves involved. """ apt_type = self.apt_type_bxls_2days.with_user(self.env.user) # create meetings _meetings = self._create_meetings( self.staff_user_bxls, [(self.reference_monday + timedelta(days=1), # 3 hours first Tuesday self.reference_monday + timedelta(days=1, hours=3), False ), (self.reference_monday + timedelta(days=7), # next Monday: one full day self.reference_monday + timedelta(days=7, hours=1), True, ), (self.reference_monday + timedelta(days=8, hours=2), # 1 hour next Tuesday (9 UTC) self.reference_monday + timedelta(days=8, hours=3), False, ), (self.reference_monday + timedelta(days=8, hours=3), # 1 hour next Tuesday (10 UTC, declined) self.reference_monday + timedelta(days=8, hours=4), False, ), (self.reference_monday + timedelta(days=8, hours=5), # 2 hours next Tuesday (12 UTC) self.reference_monday + timedelta(days=8, hours=7), False, ), ] ) attendee = _meetings[-2].attendee_ids.filtered(lambda att: att.partner_id == self.staff_user_bxls.partner_id) attendee.do_decline() with freeze_time(self.reference_now): slots = apt_type._get_appointment_slots('Europe/Brussels') self.assertSlots( slots, [{'name_formated': 'February 2022', 'month_date': datetime(2022, 2, 1), 'weeks_count': 5, # 31/01 -> 28/02 (06/03) } ], {'enddate': self.global_slots_enddate, 'startdate': self.reference_now_monthweekstart, 'slots_day_specific': { (self.reference_monday + timedelta(days=1)).date(): [ {'end': 12, 'start': 11}, {'end': 14, 'start': 13}, ], # meetings on 7-10 UTC (self.reference_monday + timedelta(days=7)).date(): [], # on meeting "allday" (self.reference_monday + timedelta(days=8)).date(): [ {'end': 9, 'start': 8}, {'end': 10, 'start': 9}, {'end': 12, 'start': 11}, ], # meetings 9-10 and 12-14 }, 'slots_start_hours': [8, 9, 10, 11, 13], # based on appointment type start hours of slots but 12 is pause midi 'slots_startdate': self.reference_monday.date(), # first Monday after reference_now 'slots_weekdays_nowork': range(2, 7) # working hours only on Monday/Tuesday (0, 1) } )