Files
enterprise-addons/appointment/tests/test_appointment.py
T

2077 lines
95 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import date, datetime, timedelta, timezone
from freezegun import freeze_time
from psycopg2.errors import CheckViolation
from werkzeug.urls import url_encode
import odoo
from odoo.addons.appointment.tests.common import AppointmentCommon
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.exceptions import ValidationError
from odoo.tests import Form, tagged, users
from odoo.tools import mute_logger
from odoo.tools.urls import urljoin as url_join
from odoo.fields import Command
@tagged('appointment_slots')
class AppointmentTest(AppointmentCommon, HttpCaseWithUserDemo):
@freeze_time('2023-12-12')
@users('apt_manager')
def test_appointment_availability_after_utc_conversion(self):
""" Check that when an event starts the day before,
it doesn't show the date as available for the user.
ie: In the brussels TZ, when placing an event on the 15 dec at 00:15 to 18:00,
the event is stored in the UTC TZ on the 14 dec at 23:15 to 17:00
Because the event start on another day, the 15th was displayed as available.
"""
staff_user = self.staff_users[0]
week_days = [0, 1, 2]
# The user works on mondays, tuesdays, and wednesdays
# Only one hour slot per weekday
self.apt_type_bxls_2days.slot_ids = [(5, 0)] + [(0, 0, {
'weekday': str(week_day + 1),
'start_hour': 8,
'end_hour': 9,
}) for week_day in week_days]
# Available on the 12, 13, 18, 19, 20, 25, 26, 27 dec
max_available_slots = 8
test_data = [
# Test 1, after UTC
# Brussels TZ: 2023-12-19 00:15 to 2023-12-19 18:00 => same day
# UTC TZ: 2023-12-18 23:15 to 2023-12-19 17:00 => different day
(
datetime(2023, 12, 18, 23, 15),
datetime(2023, 12, 19, 17, 0),
max_available_slots - 1,
{date(2023, 12, 19): []},
),
# Test 2, before UTC
# New York TZ: 2023-12-18 10:00 to 2023-12-18 22:00 => same day
# UTC TZ: 2023-12-18 15:00 to 2023-12-19 03:00 => different day
(
datetime(2023, 12, 18, 15, 0),
datetime(2023, 12, 19, 3, 0),
max_available_slots - 0,
{},
),
]
global_slots_startdate = date(2023, 11, 26)
global_slots_enddate = date(2024, 1, 6)
slots_startdate = date(2023, 12, 12)
slots_enddate = slots_startdate + timedelta(days=15)
for start, stop, nb_available_slots, slots_day_specific in test_data:
with self.subTest(start=start, stop=stop, nb_available_slots=nb_available_slots):
event = self.env["calendar.event"].create([
{
"name": "event-1",
"start": start,
"stop": stop,
"show_as": 'busy',
"partner_ids": staff_user.partner_id.ids,
"attendee_ids": [(0, 0, {
"state": "accepted",
"availability": "busy",
"partner_id": staff_user.partner_id.id,
})],
},
])
slots = self.apt_type_bxls_2days._get_appointment_slots(timezone='Europe/Brussels', filter_users=staff_user)
self.assertSlots(
slots,
[{'name_formated': 'December 2023',
'month_date': datetime(2023, 12, 1),
'weeks_count': 6,
}
],
{'startdate': global_slots_startdate,
'enddate': global_slots_enddate,
'slots_startdate': slots_startdate,
'slots_enddate': slots_enddate,
'slots_start_hours': [8],
'slots_weekdays_nowork': range(3, 7),
'slots_day_specific': slots_day_specific,
}
)
available_slots = self._filter_appointment_slots(slots, filter_weekdays=week_days)
self.assertEqual(nb_available_slots, len(available_slots))
event.unlink()
@freeze_time('2023-01-06')
@users('apt_manager')
def test_appointment_availability_with_show_as(self):
""" Checks that if a normal event and custom event both set at the same time but
the normal event is set as free then the custom meeting should be available and
available_unique_slots will contains only available slots """
employee = self.staff_users[0]
self.env["calendar.event"].create([
{
"name": "event-1",
"start": datetime(2023, 6, 5, 10, 10),
"stop": datetime(2023, 6, 5, 11, 11),
"show_as": 'free',
"partner_ids": [(Command.set(employee.partner_id.ids))],
"attendee_ids": [(0, 0, {
"state": "accepted",
"availability": "free",
"partner_id": employee.partner_id.id,
})],
}, {
"name": "event-2",
"start": datetime(2023, 6, 5, 12, 0),
"stop": datetime(2023, 6, 5, 13, 0),
"show_as": 'busy',
"partner_ids": [(Command.set(employee.partner_id.ids))],
"attendee_ids": [(0, 0, {
"state": "accepted",
"availability": "busy",
"partner_id": employee.partner_id.id,
})],
},
])
unique_slots = [{
'allday': False,
'start_datetime': datetime(2023, 6, 5, 10, 10),
'end_datetime': datetime(2023, 6, 5, 11, 11),
}, {
'allday': False,
'start_datetime': datetime(2023, 6, 5, 12, 0),
'end_datetime': datetime(2023, 6, 5, 13, 0),
}]
hour_fifty_float_repr_A = 1.8333333333333335
hour_fifty_float_repr_B = 1.8333333333333333
apt_types = self.env['appointment.type'].create([
{
'category': 'custom',
'name': 'Custom Meeting 1',
'staff_user_ids': [(4, employee.id)],
'slot_ids': [(0, 0, {
'allday': slot['allday'],
'end_datetime': slot['end_datetime'],
'slot_type': 'unique',
'start_datetime': slot['start_datetime'],
}) for slot in unique_slots
],
}, {
'category': 'custom',
'name': 'Custom Meeting 2',
'staff_user_ids': [(4, employee.id)],
'slot_ids': [(0, 0, {
'allday': unique_slots[1]['allday'],
'end_datetime': unique_slots[1]['end_datetime'],
'slot_type': 'unique',
'start_datetime': unique_slots[1]['start_datetime'],
})
],
}, {
'category': 'custom',
'name': 'Custom Meeting 3',
'appointment_tz': 'UTC',
'staff_user_ids': [(4, employee.id)],
'slot_ids': [(0, 0, {
'allday': False,
'start_datetime': datetime(2026, 1, 9, 8, 0, 0),
'end_datetime': datetime(2026, 1, 9, 8, 20, 0),
})
],
}, {
'category': 'recurring',
'name': 'Recurring Meeting 4',
'staff_user_ids': [(4, employee.id)],
'appointment_duration': hour_fifty_float_repr_A, # float presenting 1h 50min
'slot_creation_interval': hour_fifty_float_repr_A,
'appointment_tz': 'UTC',
'slot_ids': [
(0, False, {
'weekday': '1', # Monday
'start_hour': 8,
'end_hour': 17,
}
)
]
},
])
self.assertTrue(
apt_types[-1]._check_appointment_is_valid_slot(
employee,
0,
0,
'UTC',
datetime(2023, 1, 9, 8, 0, tzinfo=timezone.utc), # First monday in the future
duration=hour_fifty_float_repr_B,
allday=False,
),
"Small imprecision on float value for duration should not impact slot validity"
)
slots = apt_types[0]._get_appointment_slots('UTC')
available_unique_slots = self._filter_appointment_slots(
slots,
filter_months=[(6, 2023)],
filter_users=employee)
self.assertEqual(len(available_unique_slots), 1)
available_custom_3_slots = self._filter_appointment_slots(
apt_types[2]._get_appointment_slots('UTC', filter_users=employee),
)
self.assertEqual(len(available_custom_3_slots), 1)
self.assertEqual(
available_custom_3_slots[0]['slot_duration'],
(datetime(2026, 1, 9, 8, 20) - datetime(2026, 1, 9, 8, 0)).total_seconds() / 3600,
)
self.assertTrue(
apt_types[2]._check_appointment_is_valid_slot(
employee,
0,
0,
'UTC',
datetime(2026, 1, 9, 8, 0, tzinfo=timezone.utc),
duration=float(available_custom_3_slots[0]['slot_duration']),
allday=False,
),
"Generated precise duration should validate the matching custom slot",
)
for unique_slot, apt_type, is_available in zip(unique_slots, apt_types, [True, False]):
duration = (unique_slot['end_datetime'] - unique_slot['start_datetime']).total_seconds() / 3600
self.assertEqual(
apt_type._check_appointment_is_valid_slot(
employee,
0,
0,
'UTC',
unique_slot['start_datetime'],
duration,
allday=False,
),
is_available
)
self.assertEqual(
employee.partner_id.calendar_verify_availability(
unique_slot['start_datetime'],
unique_slot['end_datetime'],
),
is_available
)
@freeze_time('2025-05-19')
def test_appointment_not_blocked_by_nextday_allday_event(self):
"""Ensure Monday 7-8:30 PM (local) is not blocked by a Tuesday all-day event,
even if both overlap on May 20 UTC due to time zone conversion.
"""
self.staff_user_bxls.tz = 'America/Chicago'
local_tz = pytz.timezone(self.staff_user_bxls.tz)
# Monday 7:00 PM → 8:30 PM (local) converted to UTC - start time is Tuesday 0:0 UTC and end time is 1:30 UTC
slot_start_utc = local_tz.localize(datetime(2025, 5, 19, 19, 0)).astimezone(pytz.utc).replace(tzinfo=None)
slot_end_utc = local_tz.localize(datetime(2025, 5, 19, 20, 30)).astimezone(pytz.utc).replace(tzinfo=None)
busy_day = datetime(2025, 5, 20, 0, 0) # 2025-5-20 is the following Tuesday
self.env['calendar.event'].create({
'name': "All-Day Tuesday",
'start': busy_day,
'stop': busy_day + timedelta(days=1),
'allday': True,
'show_as': 'busy',
'partner_ids': [self.staff_user_bxls.partner_id.id],
'user_id': self.staff_user_bxls.id,
'attendee_ids': [Command.create({
'state': 'accepted',
'availability': 'busy',
'partner_id': self.staff_user_bxls.partner_id.id,
}),
],
})
self.assertTrue(
self.staff_user_bxls.partner_id.with_context(tz='America/Chicago').calendar_verify_availability(slot_start_utc, slot_end_utc),
"Time slots should not be blocked by next-day all-day events."
)
@users('apt_manager')
def test_appointment_type_create_anytime(self):
# Any Time: only 1 / employee
apt_type = self.env['appointment.type'].create({
'category': 'anytime',
'name': 'Any time on me',
})
self.assertEqual(apt_type.staff_user_ids, self.apt_manager)
# should be able to create 2 'anytime' appointment types at once on different users
self.env['appointment.type'].create([{
'category': 'anytime',
'name': 'Any on staff user',
'staff_user_ids': [(4, staff_user.id)],
} for staff_user in self.staff_users])
with self.assertRaises(ValidationError):
self.env['appointment.type'].create({
'category': 'anytime',
'name': 'Any time on me, duplicate',
})
with self.assertRaises(ValidationError):
self.env['appointment.type'].create({
'name': 'Any time without employees',
'category': 'anytime',
'staff_user_ids': False
})
with self.assertRaises(ValidationError):
self.env['appointment.type'].create({
'name': 'Any time with multiple employees',
'category': 'anytime',
'staff_user_ids': [(6, 0, self.staff_users.ids)]
})
@users('apt_manager')
def test_appointment_type_create_custom(self):
# Custom: current user set as default
apt_type = self.env['appointment.type'].create({
'category': 'custom',
'name': 'Custom without user',
})
self.assertEqual(apt_type.staff_user_ids, self.apt_manager)
apt_type = self.env['appointment.type'].create({
'category': 'custom',
'staff_user_ids': [(4, self.staff_users[0].id)],
'name': 'Custom with user',
})
self.assertEqual(apt_type.staff_user_ids, self.staff_users[0])
apt_type = self.env['appointment.type'].create({
'category': 'custom',
'staff_user_ids': self.staff_users.ids,
'name': 'Custom with users',
})
self.assertEqual(apt_type.staff_user_ids, self.staff_users)
@users('apt_manager')
def test_appointment_type_form_category_slot_scheduling_category_time_display(self):
""" Test form reactivity and consistency on changing category_slot_scheduling and category_time_display """
apt_type = self.env['appointment.type'].create({
'category': 'recurring',
'name': 'Starting as Recurring',
'slot_ids': [(0, 0, {
'weekday': '1', # Monday
'start_hour': 9,
'end_hour': 17,
})],
})
self.assertEqual(len(apt_type.slot_ids), 1)
self.assertFalse(apt_type.start_datetime or apt_type.end_datetime)
appt_form = Form(apt_type)
self.assertEqual(appt_form.category_slot_scheduling, 'weekly')
self.assertEqual(appt_form.category, 'recurring')
appt_form.category_slot_scheduling = 'flexible'
self.assertEqual(appt_form.category, 'custom')
self.assertFalse(appt_form.slot_ids)
appt_form.category_slot_scheduling = 'weekly'
self.assertEqual(appt_form.category, 'recurring')
self.assertEqual(len(appt_form.slot_ids), 10)
appt_form.category_time_display = 'punctual_fields'
self.assertEqual(appt_form.category, 'recurring')
appt_form.start_datetime = self.reference_monday
appt_form.end_datetime = self.reference_monday + timedelta(days=7)
self.assertEqual(appt_form.category, 'punctual')
self.assertEqual(len(appt_form.slot_ids), 10)
appt_form.category_slot_scheduling = 'flexible'
self.assertEqual(appt_form.category, 'custom')
self.assertFalse(appt_form.slot_ids)
self.assertFalse(apt_type.start_datetime or apt_type.end_datetime)
@freeze_time('2022-02-14')
@users('apt_manager')
def test_appointment_type_remaining_capacity_for_multiple_bookings(self):
""" Test the remaining capacity computation for appointment type having multiple bookings. """
apt_user, apt_resource = self.apt_user_multiple_bookings, self.apt_resource_multiple_bookings
user = self.staff_user_bxls
resource = self.env['appointment.resource'].create({
'appointment_type_ids': [(4, apt_resource.id)],
'capacity': 2,
'name': 'Resource 1',
})
start = datetime(2022, 2, 15, 14, 0, 0)
end = start + timedelta(hours=1)
user_remaining_capacity = apt_user._get_users_remaining_capacity(user, start, end)['total_remaining_capacity']
resource_remaining_capacity = apt_resource._get_resources_remaining_capacity(resource, start, end)['total_remaining_capacity']
# Check initial remaining capacity
self.assertEqual(
user_remaining_capacity, 3, 'Initial user remaining capacity should be 3.'
)
self.assertEqual(
resource_remaining_capacity, 3,
'Initial resource remaining capacity should be 3.',
)
# Create 3 bookings one-by-one for both appointment types
for booking_number in range(1, 4):
self.env['calendar.event'].with_context(self._test_context).create([{
'appointment_type_id': apt_user.id,
'booking_line_ids': [(0, 0, {'capacity_reserved': 1})],
'name': 'Booking 1',
'start': start,
'stop': end,
'user_id': user.id,
}, {
'appointment_type_id': apt_resource.id,
'booking_line_ids': [(0, 0, {'capacity_reserved': 1, 'appointment_resource_id': resource.id})],
'name': 'Booking 2',
'start': start,
'stop': end,
}])
user_remaining_capacity = apt_user._get_users_remaining_capacity(user, start, end)['total_remaining_capacity']
resource_remaining_capacity = apt_resource._get_resources_remaining_capacity(resource, start, end)['total_remaining_capacity']
self.assertEqual(
user_remaining_capacity, 3 - booking_number,
f'User remaining capacity should be {5 - booking_number} after {booking_number} booking(s)',
)
self.assertEqual(
resource_remaining_capacity, 3 - booking_number,
f'Resource remaining capacity should be {5 - booking_number} after {booking_number} booking(s).',
)
@mute_logger('odoo.sql_db')
@users('apt_manager')
def test_appointment_slot_start_and_end_datetimes_constraint(self):
""" Test that 'unique' slot start_datetime is before end_datetime. """
with self.assertRaises(CheckViolation):
self.env['appointment.type'].create({
'category': 'custom',
'name': 'A custom appointment',
'slot_ids': [Command.create({
'start_datetime': self.reference_monday,
'end_datetime': self.reference_monday - timedelta(days=7),
})]
})
# Ensure constraint does not fail when changing to 'custom' manually
apt_type = self.env['appointment.type'].create({
'category': 'recurring',
'name': 'Starting as Recurring',
'slot_ids': [(0, 0, {
'weekday': '1',
'start_hour': 9,
'end_hour': 17,
})],
})
apt_type.category = 'custom'
self.assertFalse(apt_type.slot_ids)
@mute_logger('odoo.sql_db')
@users('apt_manager')
def test_appointment_slot_start_end_hour_auto_correction(self):
""" Test the autocorrection of invalid intervals [start_hour, end_hour]. """
appt_type = self.env['appointment.type'].create({
'category': 'recurring',
'name': 'Schedule a demo',
'appointment_duration': 1,
'slot_ids': [(0, 0, {
'weekday': '1', # Monday
'start_hour': 9,
'end_hour': 17,
})],
})
appt_form = Form(appt_type)
# invalid interval, no adaptation because start_hour is not changed
with self.assertRaises(ValidationError):
with appt_form.slot_ids.edit(0) as slot_form:
slot_form.end_hour = 8
appt_form.save()
# invalid interval, adapted because start_hour is changed
with appt_form.slot_ids.edit(0) as slot_form:
slot_form.start_hour = 18
self.assertEqual(slot_form.start_hour, 18)
self.assertEqual(slot_form.end_hour, 19)
appt_form.save()
# empty interval, adapted because start_hour is changed
with appt_form.slot_ids.edit(0) as slot_form:
slot_form.start_hour = 19
self.assertEqual(slot_form.start_hour, 19)
self.assertEqual(slot_form.end_hour, 20)
appt_form.save()
# invalid interval, end_hour not adapted [23.5, 19] because it will exceed 24
with self.assertRaises(ValidationError):
with appt_form.slot_ids.edit(0) as slot_form:
slot_form.start_hour = 23.5
appt_form.save()
def test_generate_slots_until_midnight(self):
""" Generate recurring slots until midnight. """
appt_type = self.env['appointment.type'].create({
'category': 'recurring',
'name': 'Schedule a demo',
'max_schedule_days': 1,
'appointment_duration': 1,
'appointment_tz': 'Europe/Brussels',
'slot_ids': [(0, 0, {
'weekday': '1', # Monday
'start_hour': 18,
'end_hour': 0,
})],
'staff_user_ids': [(4, self.staff_user_bxls.id)],
}).with_user(self.env.user)
with freeze_time(self.reference_now):
slots = appt_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': [18, 19, 20, 21, 22, 23],
'slots_startdate': self.reference_monday.date(), # first Monday after reference_now
'slots_enddate': self.reference_monday.date(), # only test that day
}
)
def test_appointment_type_upcoming_count(self):
"""
Test upcoming count for appointment type regardless of event status.
"""
apt_type = self.apt_type_bxls_2days
self.assertEqual(apt_type.appointment_count_upcoming, 0)
meeting_1 = self._create_meetings(
self.staff_user_bxls,
[(datetime.now() + timedelta(days=1), (datetime.now() + timedelta(days=2)), False)],
appointment_type_id=apt_type.id,
)
meeting_1.write({'appointment_status': 'booked'})
self.assertEqual(apt_type.appointment_count_upcoming, 1)
meeting_2 = self._create_meetings(
self.staff_user_bxls,
[(datetime.now() + timedelta(days=1), (datetime.now() + timedelta(days=2)), False)],
appointment_type_id=apt_type.id,
)
meeting_2.write({'appointment_status': 'no_show'})
self.assertEqual(apt_type.appointment_count_upcoming, 2)
@freeze_time('2023-01-09')
def test_booking_validity(self):
"""
When confirming an appointment, we must recheck that it is indeed a valid slot,
because the user can modify the date URL parameter used to book the appointment.
We make sure the date is a valid slot, not outside of those specified by the employee,
and that it's not an old valid slot (a slot that is valid, but it's in the past,
so we shouldn't be able to book for a date that has already passed)
"""
# add the timezone of the visitor on the session (same as appointment to simplify)
session = self.authenticate(None, None)
session['timezone'] = self.apt_type_bxls_2days.appointment_tz
odoo.http.root.session_store.save(session)
appointment = self.apt_type_bxls_2days
appointment_invite = self.env['appointment.invite'].create({'appointment_type_ids': appointment.ids})
appointment_url = url_join(appointment.get_base_url(), '/appointment/%s' % appointment.id)
appointment_info_url = "%s/info?" % appointment_url
url_inside_of_slot = appointment_info_url + url_encode({
'staff_user_id': self.staff_user_bxls.id,
'date_time': datetime(2023, 1, 9, 9, 0), # 9/01/2023 is a Monday, there is a slot at 9:00
'duration': 1,
**appointment_invite._get_redirect_url_parameters(),
})
response = self.url_open(url_inside_of_slot)
self.assertEqual(response.status_code, 200, "Response should be Ok (200)")
url_outside_of_slot = appointment_info_url + url_encode({
'staff_user_id': self.staff_user_bxls.id,
'date_time': datetime(2023, 1, 9, 22, 0), # 9/01/2023 is a Monday, there is no slot at 22:00
'duration': 1,
**appointment_invite._get_redirect_url_parameters(),
})
response = self.url_open(url_outside_of_slot)
self.assertEqual(response.status_code, 404, "Response should be Page Not Found (404)")
url_inactive_past_slot = appointment_info_url + url_encode({
'staff_user_id': self.staff_user_bxls.id,
'date_time': datetime(2023, 1, 2, 22, 0),
# 2/01/2023 is a Monday, there is a slot at 9:00, but that Monday has already passed
'duration': 1,
**appointment_invite._get_redirect_url_parameters(),
})
response = self.url_open(url_inactive_past_slot)
self.assertEqual(response.status_code, 404, "Response should be Page Not Found (404)")
@freeze_time('2023-04-23')
def test_booking_validity_timezone(self):
"""
When the utc offset of the timezone is large, it is possible that the day of the week no longer corresponds.
It is necessary to take this into account when checking the slots.
"""
appointment = self.env['appointment.type'].create({
'appointment_tz': 'Pacific/Auckland',
'appointment_duration': 1,
'is_auto_assign': True,
'category': 'recurring',
'location_id': self.staff_user_nz.partner_id.id,
'name': 'New Zealand Appointment',
'max_schedule_days': 15,
'min_cancellation_hours': 1,
'min_schedule_hours': 1,
'slot_ids': [
(0, False, {'weekday': weekday,
'start_hour': hour,
'end_hour': hour + 1,
})
for weekday in ['1']
for hour in range(9, 12)
],
'staff_user_ids': [(4, self.staff_user_nz.id)],
})
session = self.authenticate(None, None)
session['timezone'] = appointment.appointment_tz
odoo.http.root.session_store.save(session)
appointment_invite = self.env['appointment.invite'].create({'appointment_type_ids': appointment.ids})
appointment_url = url_join(appointment.get_base_url(), '/appointment/%s' % appointment.id)
appointment_info_url = "%s/info?" % appointment_url
url = appointment_info_url + url_encode({
'staff_user_id': self.staff_user_nz.id,
'date_time': datetime(2023, 4, 24, 9, 0),
'duration': 1,
**appointment_invite._get_redirect_url_parameters(),
})
response = self.url_open(url)
self.assertEqual(response.status_code, 200, "Response should be Ok (200)")
def test_exclude_all_day_events(self):
""" Ensure appointment slots don't overlap with "busy" allday events. """
staff_user = self.staff_users[0]
valentime = datetime(2022, 2, 14, 0, 0) # 2022-02-14 is a Monday
slots = self.apt_type_bxls_2days._get_appointment_slots(
self.apt_type_bxls_2days.appointment_tz,
reference_date=valentime,
)
slot = slots[0]['weeks'][2][1]
self.assertEqual(slot['day'], valentime.date())
self.assertTrue(slot['slots'], "Should be available on 2022-02-14")
self.env['calendar.event'].with_user(staff_user).create({
'name': "Valentine's day",
'start': valentime,
'stop': valentime,
'allday': True,
'show_as': 'busy',
'attendee_ids': [(0, 0, {
'state': 'accepted',
'availability': 'busy',
'partner_id': staff_user.partner_id.id,
})],
})
slots = self.apt_type_bxls_2days._get_appointment_slots(
self.apt_type_bxls_2days.appointment_tz,
reference_date=valentime,
)
slot = slots[0]['weeks'][2][1]
self.assertEqual(slot['day'], valentime.date())
self.assertFalse(slot['slots'], "Shouldn't be available on 2022-02-14")
@users('apt_manager')
def test_customer_event_description(self):
"""
Check calendar description and summary generation.
This test verifies that the `_get_customer_description` method
correctly appends the appointment type's `message_confirmation`
and the reschedule link to the event's base description.
It also validates the `_get_customer_summary` method for both
user-based and resource-based appointments.
"""
appointment_type = self.apt_type_bxls_2days
appointment_type.message_confirmation = '<p>Please try to be there <strong>5 minutes</strong> before the time.<p><br>Thank you.'
booker = self.env['res.partner'].sudo().create({
'name': '<p>John Doe</p>',
'email': 'john@example.com',
'phone': '123456789',
})
extra_attendee = self.env['res.partner'].sudo().create({
'name': 'Jean',
'email': 'jean@example.com',
'phone': '888888888',
})
start_dt = datetime.now()
appointment = (
self.env["calendar.event"]
.with_context(skip_contact_description=True)
.create({
**appointment_type._prepare_calendar_event_values(
asked_capacity=1,
booking_line_values=[{'capacity_reserved': 1, 'capacity_used': 1}],
description="A beautiful description written for external calendar",
duration=appointment_type.appointment_duration,
allday=False,
appointment_invite=self.env['appointment.invite'],
guests=extra_attendee,
name=booker.name,
customer=booker,
staff_user=self.staff_user_bxls,
start=start_dt,
stop=start_dt + timedelta(hours=appointment_type.appointment_duration),
),
})
)
url = f"{appointment_type.get_base_url()}/calendar/view/{appointment.access_token}"
customer_description = (
'<p>A beautiful description written for external calendar</p><br>'
'<p>Please try to be there <strong>5 minutes</strong> before the time.</p>'
'<p><br>Thank you.</p><span>Need to reschedule? <a href=%s>Click here</a></span>'
) % (url)
self.assertEqual(appointment._get_customer_description(), customer_description)
# Test summary for all appointment types
resource_appointment = self.apt_type_resource
resource_event = self.env['calendar.event'].create({
'name': '%s - %s' % (resource_appointment.name, booker.name),
'start': datetime.now(),
'stop': datetime.now() + timedelta(hours=1),
'appointment_type_id': resource_appointment.id,
'user_id': self.apt_manager.id,
})
user_summary = f'{appointment_type.name} with {self.staff_user_bxls.name or "somebody"}'
for event, summary in ((appointment, user_summary), (resource_event, resource_event.name)):
with self.subTest(summary=summary):
self.assertEqual(event._get_customer_summary(), summary)
@users('apt_manager')
@freeze_time('2022-02-13T20:00:00')
def test_generate_slots_punctual_appointment_type(self):
""" Generates recurring slots, check begin and end slot boundaries depending on the start and end datetimes. """
apt_type = self.env['appointment.type'].create({
'appointment_tz': 'Europe/Brussels',
'appointment_duration': 1,
'is_auto_assign': True,
'category': 'punctual',
'location_id': self.staff_user_bxls.partner_id.id,
'name': 'Punctual Appt Type',
'max_schedule_days': False,
'min_cancellation_hours': 1,
'min_schedule_hours': 1,
'start_datetime': datetime(2022, 2, 14, 8, 0, 0),
'end_datetime': datetime(2022, 2, 20, 20, 0, 0),
'slot_ids': [
(0, False, {'weekday': weekday,
'start_hour': hour,
'end_hour': hour + 1,
})
for weekday in ['1', '2']
for hour in range(8, 14)
],
'staff_user_ids': [(4, self.staff_user_bxls.id)],
}).with_user(self.env.user)
slots_weekdays = {slot.weekday for slot in apt_type.slot_ids}
timezone = 'Europe/Brussels'
requested_tz = pytz.timezone(timezone)
# reference_now: datetime(2022, 2, 13, 20, 0, 0) (sunday evening)
# apt slot_ids: Monday 8AM -> 2PM, Tuesday 8AM -> 2PM
cases = [
# start datetime / end_datetime (UTC)
(datetime(2022, 2, 14, 9, 0, 0), datetime(2022, 2, 25, 9, 0, 0)), # Slots fully in the future
(datetime(2022, 2, 1, 9, 0, 0), datetime(2022, 2, 25, 9, 0, 0)), # start_datetime < now < end_datetimes
(datetime(2022, 2, 1, 9, 0, 0), datetime(2022, 2, 12, 9, 0, 0)), # Slots fully in the past
]
expected = [
# first slot start datetime / last slot end_datetime (UTC)
(datetime(2022, 2, 14, 9, 0, 0), datetime(2022, 2, 22, 13, 0, 0)), # start = specified start_datetime
(datetime(2022, 2, 14, 7, 0, 0), datetime(2022, 2, 22, 13, 0, 0)), # start = 8AM from apt slots_ids converted to UTC
(False, False)
]
for (start_datetime, end_datetime), (first_slot_expected_start, last_slot_expected_end) in zip(cases, expected):
with self.subTest(start_datetime=start_datetime, end_datetime=end_datetime):
apt_type.write({'start_datetime': start_datetime, 'end_datetime': end_datetime})
reference_date = start_datetime if start_datetime > self.reference_now else self.reference_now
first_day = requested_tz.fromutc(reference_date)
last_day = requested_tz.fromutc(end_datetime)
slots = apt_type._slots_generate(first_day, last_day, timezone, reference_date=reference_date)
if not slots:
self.assertFalse(first_slot_expected_start)
self.assertFalse(last_slot_expected_end)
continue
self.assertTrue({slot['slot'].weekday for slot in slots}.issubset(slots_weekdays), 'Slots: wrong weekday')
self.assertEqual(slots[0]['UTC'][0], first_slot_expected_start, 'Slots: wrong first slot start datetime')
self.assertEqual(slots[-1]['UTC'][1], last_slot_expected_end, 'Slots: wrong last slot end datetime')
@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, 12, 13], # based on appointment type start hours of slots, no work hours / no meetings / no leaves
'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_start_hour_day_overflow(self):
""" Generates recurring slots, make sure we don't overshoot the current day and generate meaningless slots """
slots = [{
'weekday': '1',
'start_hour': 9.0,
'end_hour': 10.0,
}, {
'weekday': '1',
'start_hour': 10.0,
'end_hour': 11.0,
}, {
'weekday': '1',
'start_hour': 15.0,
'end_hour': 16.0,
}, {
'weekday': '2',
'start_hour': 9.0,
'end_hour': 17.0,
},]
apt_type = self.env['appointment.type'].create({
'appointment_duration': 1.0,
'appointment_tz': 'Europe/Brussels',
'category': 'recurring',
'name': 'Overflow Appointment',
'max_schedule_days': 8,
'min_schedule_hours': 12.0,
'slot_ids': [(0, 0, slot) for slot in slots],
'staff_user_ids': [self.env.user.id],
})
# Check around the 11AM(Brussels) mark, or 15:30PM(Kolkata)
# If we add 12 for the minimum schedule hour it's past 11PM
# Past 11 the appointment duration will put us past the current day
brussels_tz = pytz.timezone('Europe/Brussels')
for hour, minute in [[h, m] for h in [2, 9, 10, 11, 12] for m in [0, 1, 59]]:
time = brussels_tz.localize(self.reference_monday.replace(hour=hour, minute=minute))
with freeze_time(time):
slots = apt_type._get_appointment_slots('Asia/Kolkata')
self.assertSlots(
slots,
[{'name_formated': 'February 2022',
'month_date': datetime(2022, 2, 1),
'weeks_count': 5, # 31/01 -> 28/02 (06/03)
}
],
{'enddate': date(2022, 3, 5),
'startdate': self.reference_now_monthweekstart,
'slots_day_specific': { # +4 instead of +4.5 because the test method only accounts for the absolute hour
time.date(): [{'start': 15 + 4, 'end': 16 + 4}] if hour == 2 else [], # min_schedule_hours is too large
(time + timedelta(days=1)).date(): [{'start': start + 4, 'end': start + 5} for start in range(9, 17)],
(time + timedelta(days=7)).date(): [{'start': start + 4, 'end': start + 5} for start in range(9, 11)] + [{'start': 15 + 4, 'end': 16 + 4}],
(time + timedelta(days=8)).date(): [{'start': start + 4, 'end': start + 5} for start in range(9, 17)],
},
'slots_start_hours': [],
'slots_startdate': time.date(),
'slots_weekdays_nowork': range(2, 7)
},
)
@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, 11, 12], # based on appointment type start hours of slots, no work hours / no meetings / no leaves
'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('admin')
def test_generate_slots_recurring_westrict(self):
""" Generates recurring slots, check user restrictions """
apt_type = self.apt_type_bxls_2days.with_user(self.env.user)
# add second staff user and split days based on the two people
apt_type.write({'staff_user_ids': [(4, self.staff_user_aust.id)]})
apt_type.slot_ids.filtered(lambda slot: slot.weekday == '1').write({
'restrict_to_user_ids': [(4, self.staff_user_bxls.id)],
})
apt_type.slot_ids.filtered(lambda slot: slot.weekday != '1').write({
'restrict_to_user_ids': [(4, self.staff_user_aust.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_start_hours': [8, 9, 10, 11, 12, 13], # based on appointment type start hours of slots, no work hours / no meetings / no leaves
'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)
}
)
# check staff_user_id
monday_slots = [
slot
for month in slots for week in month['weeks'] for day in week
for slot in day['slots']
if day['day'].weekday() == 0
]
tuesday_slots = [
slot
for month in slots for week in month['weeks'] for day in week
for slot in day['slots']
if day['day'].weekday() == 1
]
self.assertEqual(len(monday_slots), 18, 'Slots: 3 mondays of 6 slots')
self.assertTrue(all(slot['staff_user_id'] == self.staff_user_bxls.id for slot in monday_slots))
self.assertEqual(len(tuesday_slots), 12, 'Slots: 2 tuesdays of 6 slots (3rd tuesday is out of range')
self.assertTrue(all(slot['staff_user_id'] == self.staff_user_aust.id for slot in tuesday_slots))
@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': 13, 'start': 12},
{'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},
{'end': 13, 'start': 12},
], # meetings 9-10 and 12-14
},
'slots_start_hours': [8, 9, 10, 11, 12, 13], # based on appointment type start hours of slots, no work hours / no meetings / no leaves
'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_unique_and_options(self):
""" Check unique slots (note: custom appointment type does not check working
hours). Also check min_schedule_hours, and user restriction with restrict_to_user_ids"""
unique_slots = [{
'start_datetime': self.reference_monday.replace(microsecond=0),
'end_datetime': (self.reference_monday + timedelta(hours=1)).replace(microsecond=0),
'allday': False,
}, {
'start_datetime': (self.reference_monday + timedelta(days=1)).replace(microsecond=0),
'end_datetime': (self.reference_monday + timedelta(days=2)).replace(microsecond=0),
'allday': True,
}]
apt_type = self.env['appointment.type'].create({
'category': 'custom',
'is_auto_assign': False,
'is_date_first': True,
'min_schedule_hours': 1,
'name': 'Custom with unique slots',
'slot_ids': [(5, 0)] + [
(0, 0, {'allday': slot['allday'],
'end_datetime': slot['end_datetime'],
'slot_type': 'unique',
'start_datetime': slot['start_datetime'],
}
) for slot in unique_slots
],
'staff_user_ids': [self.apt_manager.id, self.staff_user_bxls.id]
})
self.assertEqual(apt_type.category, 'custom', "It should be a custom appointment type")
self.assertEqual(len(apt_type.slot_ids), 2, "Two slots should have been assigned to the appointment type")
self.assertFalse(apt_type.slot_ids.restrict_to_user_ids)
with freeze_time(self.reference_now):
slots = apt_type._get_appointment_slots('Europe/Brussels')
expected_months = [{
'name_formated': 'February 2022',
'month_date': datetime(2022, 2, 1),
'weeks_count': 5, # 31/01 -> 28/02 (06/03)
}]
expected_slot_data = {
'enddate': self.global_slots_enddate,
'startdate': self.reference_now_monthweekstart,
'slots_day_specific': {
self.reference_monday.date(): [{'end': 9, 'start': 8}], # first unique 1 hour long
(self.reference_monday + timedelta(days=1)).date(): [{'allday': True, 'end': False, 'start': 8}], # second unique all day-based
},
'slots_start_hours': [], # all slots in this tests are unique, other dates have no slots
'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)
}
self.assertSlots(slots, expected_months, expected_slot_data)
# Check min schedule hours
with freeze_time(self.reference_monday - timedelta(minutes=30)):
slots = apt_type._get_appointment_slots('Europe/Brussels')
expected_slots_custom = expected_slot_data.copy()
expected_slots_custom.update({
'slots_day_specific': {(self.reference_monday + timedelta(days=1)).date(): [{'allday': True, 'end': False, 'start': 8}]},
'slots_weekdays_nowork': [0, 2, 3, 4, 5, 6], # only slot is Tuesday as Monday slot is only 30 min after freeze_time
})
self.assertSlots(slots, expected_months, expected_slots_custom)
# With restrict_to_user_ids:
apt_type.slot_ids[0].restrict_to_user_ids = [self.apt_manager.id, self.staff_user_bxls.id]
apt_type.slot_ids[1].restrict_to_user_ids = self.staff_user_bxls.ids
with freeze_time(self.reference_now):
slots = apt_type._get_appointment_slots('Europe/Brussels')
self.assertSlots(slots, expected_months, expected_slot_data)
monday_slot = self._filter_appointment_slots(slots, filter_weekdays=[0])
tuesday_slot = self._filter_appointment_slots(slots, filter_weekdays=[1])
available_users_monday = [resource['id'] for resource in monday_slot[0]['available_staff_users']]
available_users_tuesday = [resource['id'] for resource in tuesday_slot[0]['available_staff_users']]
self.assertSetEqual(set(available_users_monday), {self.apt_manager.id, self.staff_user_bxls.id})
self.assertListEqual(available_users_tuesday, self.staff_user_bxls.ids)
@users('apt_manager')
def test_multi_user_slot_availabilities(self):
""" Check that when called with no user / one user / several users, the methods computing the slots work as expected:
if no user is set, all users of the appointment_type will be used. If one or more users are set, they will be used to
compute availabilities. If users given as argument is not among the staff of the appointment type, return empty list.
This test only concern random appointments: if it were 'chosen' assignment, then the dropdown of user selection would
be in the view. Hence, in practice, only one user would be used to generate / update the slots : the one selected. For
random ones, the users can be multiple if a filter is set, assigning randomly among several users. This tests asserts
that _get_appointment_slots returns slots properly when called with several users too. If no filter, then the update
method would be called with staff_users = False (since select not in view, getting the input value returns false) """
reference_monday = self.reference_monday.replace(microsecond=0)
reccuring_slots_utc = [{
'weekday': '1',
'start_hour': 6.0, # 1 slot : Monday 06:00 -> 07:00
'end_hour': 7.0,
}, {
'weekday': '2',
'start_hour': 9.0, # 2 slots : Tuesday 09:00 -> 11:00
'end_hour': 11.0,
}]
staff_user_no_tz = mail_new_test_user(
self.env(su=True),
company_id=self.company_admin.id,
email='no_tz@test.example.com',
groups='base.group_user',
name='Employee Without Tz',
notification_type='email',
login='staff_user_no_tz',
tz=False,
)
apt_type_UTC = self.env['appointment.type'].create({
'appointment_tz': 'UTC',
'is_auto_assign': True,
'category': 'recurring',
'max_schedule_days': 5, # Only consider the first three slots
'name': 'Private Guitar Lesson',
'slot_ids': [(0, False, {
'weekday': slot['weekday'],
'start_hour': slot['start_hour'],
'end_hour': slot['end_hour'],
}) for slot in reccuring_slots_utc],
'staff_user_ids': [self.staff_user_aust.id, self.staff_user_bxls.id, staff_user_no_tz.id],
})
exterior_staff_user = self.apt_manager
# staff_user_bxls is only available on Wed and staff_user_aust only on Mon and Tue
self._create_meetings(
self.staff_user_bxls,
[(reference_monday - timedelta(hours=1), # Monday 06:00 -> 07:00
reference_monday,
False
)]
)
self._create_meetings(
self.staff_user_aust,
[(reference_monday + timedelta(days=1, hours=2), # Tuesday 09:00 -> 11:00
reference_monday + timedelta(days=1, hours=4),
False
)]
)
# staff_user_no_tz is only available on Tue between 10 and 11 AM
self._create_meetings(
staff_user_no_tz,
[(
self.reference_monday,
self.reference_monday.replace(hour=9),
True
), (
self.reference_monday + timedelta(days=1, hours=2),
self.reference_monday + timedelta(days=1, hours=3),
False
)])
with freeze_time(self.reference_now):
slots_no_user = apt_type_UTC._get_appointment_slots('UTC')
slots_exterior_user = apt_type_UTC._get_appointment_slots('UTC', exterior_staff_user)
slots_user_aust = apt_type_UTC._get_appointment_slots('UTC', self.staff_user_aust)
slots_user_all = apt_type_UTC._get_appointment_slots('UTC', self.staff_user_bxls | self.staff_user_aust)
slots_user_bxls_exterior_user = apt_type_UTC._get_appointment_slots('UTC', self.staff_user_bxls | exterior_staff_user)
slots_user_no_tz = apt_type_UTC._get_appointment_slots('UTC', staff_user_no_tz)
self.assertTrue(len(self._filter_appointment_slots(slots_no_user)) == 3)
self.assertFalse(slots_exterior_user)
self.assertTrue(len(self._filter_appointment_slots(slots_user_aust)) == 1)
self.assertTrue(len(self._filter_appointment_slots(slots_user_all)) == 3)
self.assertTrue(len(self._filter_appointment_slots(slots_user_bxls_exterior_user)) == 2)
self.assertTrue(len(self._filter_appointment_slots(slots_user_no_tz)) == 1)
@users('apt_manager')
def test_slots_for_today(self):
test_reference_now = datetime(2022, 2, 14, 11, 0, 0) # is a Monday
appointment = self.env['appointment.type'].create({
'appointment_tz': 'UTC',
'min_schedule_hours': 1.0,
'max_schedule_days': 8,
'name': 'Test',
'slot_ids': [(0, 0, {
'weekday': str(test_reference_now.isoweekday()),
'start_hour': 6,
'end_hour': 18,
})],
'staff_user_ids': [self.staff_user_bxls.id],
})
first_day = (test_reference_now + timedelta(hours=appointment.min_schedule_hours)).astimezone(pytz.UTC)
last_day = (test_reference_now + timedelta(days=appointment.max_schedule_days)).astimezone(pytz.UTC)
with freeze_time(test_reference_now):
slots = appointment._slots_generate(first_day, last_day, 'UTC')
self.assertEqual(len(slots), 18, '2 mondays of 12 slots but 6 would be before reference date')
for slot in slots:
self.assertTrue(
test_reference_now.astimezone(pytz.UTC) < slot['UTC'][0].astimezone(pytz.UTC),
"A slot shouldn't be generated before the first_day datetime")
@users('apt_manager')
def test_slots_days_min_schedule(self):
""" Test that slots are generated correctly when min_schedule_hours is 47.0.
This means that the first returned slots should be on wednesday at 11:36.
"""
test_reference_now = datetime(2022, 2, 14, 11, 45, 0) # is a Monday
appointment = self.env['appointment.type'].create({
'appointment_tz': 'UTC',
'appointment_duration': 1.2, # 1h12
'slot_creation_interval': 1.2,
'min_schedule_hours': 47.0,
'max_schedule_days': 8,
'name': 'Test',
'slot_ids': [
(0, False, {'weekday': weekday,
'start_hour': 8,
'end_hour': 14,
})
for weekday in map(str, range(1, 4))
],
'staff_user_ids': [self.staff_user_bxls.id],
})
first_day = (test_reference_now + timedelta(hours=appointment.min_schedule_hours)).astimezone(pytz.UTC)
last_day = (test_reference_now + timedelta(days=appointment.max_schedule_days)).astimezone(pytz.UTC)
with freeze_time(test_reference_now):
slots = appointment._slots_generate(first_day, last_day, 'UTC')
for slot in slots:
self.assertTrue(
first_day < slot['UTC'][0].astimezone(pytz.UTC),
"A slot shouldn't be generated before the first_day datetime")
self.assertEqual(len(slots), 12) # 2 days of 5 slots and 2 slots on wednesday
def test_slot_creation_interval(self):
"""The slot creation interval is equal to the appointment duration.
Regular configuration, leading to 1 slot every 'appointment duration'.
"""
self.apt_type_bxls_2days.write({
'appointment_tz': 'UTC',
'appointment_duration': 2,
'slot_creation_interval': 2,
'slot_ids': [(0, 0, {
'weekday': str(weekday),
'start_hour': 9,
'end_hour': 19,
}) for weekday in range(1, 8)],
})
expected_slots = {
'enddate': self.global_slots_enddate,
'startdate': self.reference_now_monthweekstart,
'slots_start_hours': list(range(9, 18, 2)),
'slots_startdate': self.reference_monday.date(),
'slots_enddate': self.reference_monday.date() + timedelta(days=15),
}
expected_months = [{
'name_formated': 'February 2022',
'month_date': datetime(2022, 2, 14),
'weeks_count': 5,
}]
with freeze_time(self.reference_now):
slots = self.apt_type_bxls_2days._get_appointment_slots('UTC')
self.assertSlots(slots, expected_months, expected_slots)
def test_slot_creation_interval_shorter(self):
""" The slot creation interval is shorter than the appointment duration.
This configuration will create *more* slots, typically used for a restaurant to allow people
coming every 1 hour but staying for a duration of 2 hours.
"""
self.apt_type_bxls_2days.write({
'appointment_tz': 'UTC',
'appointment_duration': 2,
'slot_creation_interval': 1,
'slot_ids': [(0, 0, {
'weekday': str(weekday),
'start_hour': 9,
'end_hour': 19,
}) for weekday in range(1, 8)],
})
expected_slots = {
'enddate': self.global_slots_enddate,
'startdate': self.reference_now_monthweekstart,
'slots_start_hours': list(range(9, 18)),
'slots_startdate': self.reference_monday.date(),
'slots_enddate': self.reference_monday.date() + timedelta(days=15),
}
expected_months = [{
'name_formated': 'February 2022',
'month_date': datetime(2022, 2, 14),
'weeks_count': 5,
}]
with freeze_time(self.reference_now):
slots = self.apt_type_bxls_2days._get_appointment_slots('UTC')
self.assertSlots(slots, expected_months, expected_slots)
def test_slot_creation_interval_longer(self):
""" The slot creation interval is longer than the appointment duration.
This configuration will create *less* slots, allowing for example to leave a buffer after the appointment.
For example, people will book a 2h session for an escape game but you need 1h to reset the game before
the next slot.
"""
self.apt_type_bxls_2days.write({
'appointment_tz': 'UTC',
'appointment_duration': 2,
'slot_creation_interval': 3,
'slot_ids': [(0, 0, {
'weekday': str(weekday),
'start_hour': 9,
'end_hour': 19,
}) for weekday in range(1, 8)],
})
expected_slots = {
'enddate': self.global_slots_enddate,
'startdate': self.reference_now_monthweekstart,
'slots_start_hours': [9, 12, 15],
'slots_startdate': self.reference_monday.date(),
'slots_enddate': self.reference_monday.date() + timedelta(days=15),
}
expected_months = [{
'name_formated': 'February 2022',
'month_date': datetime(2022, 2, 14),
'weeks_count': 5,
}]
with freeze_time(self.reference_now):
slots = self.apt_type_bxls_2days._get_appointment_slots('UTC')
self.assertSlots(slots, expected_months, expected_slots)
@users('apt_manager')
def test_slots_days_min_schedule_punctual(self):
""" Test that slots are generated correctly when min_schedule_hours is 47.0 for punctual appointment.
This means that the first returned slots should be on wednesday at 11:36.
"""
test_reference_now = datetime(2022, 2, 14, 11, 45, 0) # is a Monday
appointment = self.env['appointment.type'].create({
'appointment_tz': 'UTC',
'appointment_duration': 1.2, # 1h12
'slot_creation_interval': 1.2,
'category': 'punctual',
'min_schedule_hours': 47.0,
'max_schedule_days': False,
'name': 'Test',
'slot_ids': [
(0, False, {'weekday': weekday,
'start_hour': 8,
'end_hour': 14,
})
for weekday in ['1', '2', '3', '4', '5']
],
'start_datetime': datetime(2022, 2, 15, 9, 0, 0),
'end_datetime': datetime(2022, 2, 25, 9, 0, 0),
'staff_user_ids': [self.staff_user_bxls.id],
})
with freeze_time(test_reference_now):
slots = appointment.sudo()._get_appointment_slots('UTC')
slots = self._filter_appointment_slots(slots)
self.assertEqual(slots[0]['datetime'], "2022-02-16 11:36:00",
"The first slot should take into account the min schedule hours")
self.assertEqual(slots[-1]['datetime'], "2022-02-24 12:48:00")
@users('staff_user_aust')
def test_timezone_delta(self):
""" Test timezone delta. Not sure what original test was really doing. """
# As if the second user called the function
apt_type = self.apt_type_bxls_2days.with_user(self.env.user).with_context(
lang='en_US',
tz=self.staff_user_aust.tz,
uid=self.staff_user_aust.id,
)
# Do what the controller actually does, aka sudo
with freeze_time(self.reference_now):
slots = apt_type.sudo()._get_appointment_slots('Australia/Perth', filter_users=None)
global_slots_enddate = date(2022, 4, 2) # last day of last week of March
self.assertSlots(
slots,
[{'name_formated': 'February 2022',
'month_date': datetime(2022, 2, 1),
'weeks_count': 5, # 31/01 -> 28/02 (06/03)
},
{'name_formated': 'March 2022',
'month_date': datetime(2022, 3, 1),
'weeks_count': 5, # 28/02 -> 28/03 (03/04)
}
],
{'enddate': global_slots_enddate,
'startdate': self.reference_now_monthweekstart,
'slots_enddate': self.reference_now.date() + timedelta(days=15), # maximum 2 weeks of slots
'slots_start_hours': [15, 16, 17, 18, 19, 20], # based on appointment type start hours of slots, no work hours / no meetings / no leaves, set in UTC+8
'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_unique_allday_slots_availabilities(self):
reference_monday = self.reference_monday.replace(hour=0, minute=0, microsecond=0)
unique_slots = [{
'allday': True,
'end_datetime': reference_monday,
'start_datetime': reference_monday,
}, {
'allday': True,
'end_datetime': reference_monday + timedelta(days=2),
'start_datetime': reference_monday + timedelta(days=1),
}]
apt_type = self.env['appointment.type'].create({
'appointment_tz': 'UTC',
'category': 'custom',
'name': 'Custom with allday slots',
'slot_ids': [(0, 0, {
'allday': slot['allday'],
'end_datetime': slot['end_datetime'],
'slot_type': 'unique',
'start_datetime': slot['start_datetime'],
}) for slot in unique_slots],
})
with freeze_time(self.reference_now):
slots = apt_type._get_appointment_slots('Europe/Brussels')
# get all monday slots where apt_manager is available
available_unique_slots = self._filter_appointment_slots(
slots,
filter_months=[(2, 2022)],
filter_users=self.apt_manager)
self.assertEqual(len(available_unique_slots), 2)
# Create an all day meeting before the first slot and another
# at the start of the second slot.
self._create_meetings(self.apt_manager, [(
reference_monday - timedelta(days=1),
reference_monday - timedelta(days=1),
True,
), (
reference_monday + timedelta(days=1),
reference_monday + timedelta(days=1, hours=1),
False,
), (
reference_monday + timedelta(days=2),
reference_monday + timedelta(days=2, hours=1),
False,
)])
with freeze_time(self.reference_now):
slots = apt_type._get_appointment_slots('Europe/Brussels')
available_unique_slots = self._filter_appointment_slots(
slots,
filter_months=[(2, 2022)],
filter_users=self.apt_manager)
self.assertEqual(len(available_unique_slots), 1)
self.assertEqual(
available_unique_slots[0]['datetime'],
unique_slots[0]['start_datetime'].strftime('%Y-%m-%d %H:%M:%S'),
)
@users('apt_manager')
def test_unique_slots_availabilities(self):
""" Check that the availability of each unique slot is correct.
First we test that the 2 unique slots of the custom appointment type
are available. Then we check that there is now only 1 availability left
after the creation of a meeting which encompasses a slot. """
reference_monday = self.reference_monday.replace(microsecond=0)
unique_slots = [{
'allday': False,
'end_datetime': reference_monday + timedelta(hours=1),
'start_datetime': reference_monday,
}, {
'allday': False,
'end_datetime': reference_monday + timedelta(hours=3),
'start_datetime': reference_monday + timedelta(hours=2),
}]
apt_type = self.env['appointment.type'].create({
'category': 'custom',
'name': 'Custom with unique slots',
'slot_ids': [(0, 0, {
'allday': slot['allday'],
'end_datetime': slot['end_datetime'],
'slot_type': 'unique',
'start_datetime': slot['start_datetime'],
}) for slot in unique_slots
],
})
with freeze_time(self.reference_now):
slots = apt_type._get_appointment_slots('UTC')
# get all monday slots where apt_manager is available
available_unique_slots = self._filter_appointment_slots(
slots,
filter_months=[(2, 2022)],
filter_weekdays=[0],
filter_users=self.apt_manager)
self.assertEqual(len(available_unique_slots), 2)
# Create a meeting encompassing the first unique slot
self._create_meetings(self.apt_manager, [(
unique_slots[0]['start_datetime'],
unique_slots[0]['end_datetime'],
False,
)])
with freeze_time(self.reference_now):
slots = apt_type._get_appointment_slots('UTC')
available_unique_slots = self._filter_appointment_slots(
slots,
filter_months=[(2, 2022)],
filter_weekdays=[0],
filter_users=self.apt_manager)
self.assertEqual(len(available_unique_slots), 1)
self.assertEqual(
available_unique_slots[0]['datetime'],
unique_slots[1]['start_datetime'].strftime('%Y-%m-%d %H:%M:%S'),
)
@users('apt_manager')
def test_synchronize_restricted_resource_and_staff_to_appointment_type(self):
""" Check that when changing staff users or resources, the appointment slots adapt their
restricted users/resources to only keep the new staff users / resources. Remove the rest.
When changing schedule_based_on, remove previous mode users/ resources on both type and slots.
"""
appointment_type = self.apt_type_bxls_2days
self.assertTrue(len(appointment_type.slot_ids) >= 3)
slot_1, slot_2, slot_3 = appointment_type.slot_ids[:3]
appointment_type.staff_user_ids = [(6, False, [self.apt_manager.id, self.staff_user_bxls.id])]
self.assertEqual(appointment_type.staff_user_ids, self.apt_manager | self.staff_user_bxls)
slot_1.restrict_to_user_ids = [self.apt_manager.id, self.staff_user_bxls.id]
slot_2.restrict_to_user_ids = [self.apt_manager.id]
slot_3.restrict_to_user_ids = []
self.assertEqual(slot_1.restrict_to_user_ids, self.apt_manager | self.staff_user_bxls)
self.assertEqual(slot_2.restrict_to_user_ids, self.apt_manager)
self.assertFalse(slot_3.restrict_to_user_ids)
appointment_type.staff_user_ids = [(6, False, [self.staff_user_bxls.id, self.staff_user_aust.id])]
self.assertEqual(appointment_type.staff_user_ids, self.staff_user_bxls | self.staff_user_aust)
self.assertEqual(slot_1.restrict_to_user_ids, self.staff_user_bxls)
self.assertFalse(slot_2.restrict_to_user_ids)
self.assertFalse(slot_3.restrict_to_user_ids)
appointment_type.schedule_based_on = 'resources'
self.assertFalse(appointment_type.staff_user_ids)
self.assertFalse(slot_1.restrict_to_user_ids | slot_2.restrict_to_user_ids | slot_3.restrict_to_user_ids)
resource_1, resource_2, resource_3 = self.env['appointment.resource'].create([{
'appointment_type_ids': appointment_type.ids,
'name': 'Resource 1',
}, {
'appointment_type_ids': appointment_type.ids,
'name': 'Resource 2',
}, {
'appointment_type_ids': appointment_type.ids,
'name': 'Resource 3',
}])
self.assertEqual(appointment_type.resource_ids, resource_1 | resource_2 | resource_3)
slot_1.restrict_to_resource_ids = [resource_1.id, resource_2.id]
slot_2.restrict_to_resource_ids = [resource_1.id]
slot_3.restrict_to_resource_ids = []
self.assertEqual(slot_1.restrict_to_resource_ids, resource_1 | resource_2)
self.assertEqual(slot_2.restrict_to_resource_ids, resource_1)
self.assertFalse(slot_3.restrict_to_user_ids)
appointment_type.resource_ids = [(6, False, [resource_2.id, resource_3.id])]
self.assertEqual(appointment_type.resource_ids, resource_2 | resource_3)
self.assertEqual(slot_1.restrict_to_resource_ids, resource_2)
self.assertFalse(slot_2.restrict_to_resource_ids)
self.assertFalse(slot_3.restrict_to_resource_ids)
appointment_type.schedule_based_on = 'users'
self.assertFalse(appointment_type.resource_ids)
self.assertFalse(slot_1.restrict_to_resource_ids | slot_2.restrict_to_resource_ids | slot_3.restrict_to_resource_ids)
def test_check_appointment_timezone(self):
session = self.authenticate(None, None)
odoo.http.root.session_store.save(session)
appointment = self.apt_type_bxls_2days
appointment_invite = self.env['appointment.invite'].create({'appointment_type_ids': appointment.ids})
appointment_url = url_join(appointment.get_base_url(), '/appointment/%s' % appointment.id)
appointment_info_url = "%s/info?" % appointment_url
url_inside_of_slot = appointment_info_url + url_encode({
'staff_user_id': self.staff_user_bxls.id,
'date_time': datetime(2023, 1, 9, 9, 0), # 9/01/2023 is a Monday, there is a slot at 9:00
'duration': 1,
**appointment_invite._get_redirect_url_parameters(),
})
# User should be able open url without timezone session
self.url_open(url_inside_of_slot)
@freeze_time('2022-02-14')
@users('apt_manager')
def test_different_timezones_with_allday_events_availabilities(self):
"""
When the utc offset of the timezone is large, it is possible that the day of the week no longer corresponds.
Testing that allday event slots are all not available.
"""
appointment = self.env['appointment.type'].create({
'appointment_tz': 'Pacific/Auckland',
'appointment_duration': 21,
'slot_creation_interval': 21,
'is_auto_assign': True,
'category': 'recurring',
'location_id': self.staff_user_nz.partner_id.id,
'name': 'New Zealand Appointment',
'max_schedule_days': 14,
'min_cancellation_hours': 1,
'min_schedule_hours': 1,
'slot_ids': [(0, 0, {
'weekday': '1',
'start_hour': 1,
'end_hour': 23,
})],
'staff_user_ids': [(4, self.staff_user_nz.id)],
})
self._create_meetings(
self.staff_user_nz,
[(self.reference_monday + timedelta(days=7),
self.reference_monday + timedelta(days=7, hours=1),
True
)])
slots = appointment._get_appointment_slots(
appointment.appointment_tz)
self.assertSlots(
slots,
[{'name_formated': 'February 2022',
'month_date': date(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': [],
# first Monday after reference_now
'slots_startdate': self.reference_monday.date() + timedelta(days=7),
# only test that day
'slots_enddate': self.reference_monday.date() + timedelta(days=14),
'slots_day_specific': {date(2022, 2, 28): [{'start':1}]}
}
)
def test_no_activity_creation_on_apt_booking(self):
""" Test that no activity is created on appointment booking. """
apt_type = self.apt_type_bxls_2days
self.env['calendar.event'].with_user(self.apt_manager).with_context(
default_res_model=apt_type._name,
default_res_id=apt_type.id,
).create({
'name': 'Appointment Meeting',
'start': datetime(2022, 2, 1, 10, 0, 0),
'stop': datetime(2022, 2, 1, 11, 0, 0),
'appointment_type_id': apt_type.id,
})
self.assertEqual(len(apt_type.activity_ids), 0)
# Ensure that activities are created if model is not appointment.type
test_record = self.env['res.partner'].create({
'name': 'User 0',
})
self.env['calendar.event'].with_user(self.user_demo).with_context(
default_res_model=test_record._name,
default_res_id=test_record.id,
).create({
'name': 'Normal Meeting',
'start': datetime(2022, 2, 1, 11, 0, 0),
'stop': datetime(2022, 2, 1, 12, 0, 0),
})
self.assertEqual(len(test_record.activity_ids), 1)
@users('apt_manager')
def test_resource_on_leave_with_conflicting_event(self):
"""
Check conflicting event with resources are correctly reflected in the unavailable_resource_ids field.
Overlapping times between already booked resources and the event resources should add the resource
to the list of unavailable resources.
"""
start = datetime(2022, 2, 14, 15, 0, 0)
end = start + timedelta(hours=1)
court1, court2, court3 = self.env['appointment.resource'].create([{
'appointment_type_ids': self.apt_type_resource.ids,
'name': 'Court 1',
}, {
'appointment_type_ids': self.apt_type_resource.ids,
'name': 'Court 2',
}, {
'appointment_type_ids': self.apt_type_resource.ids,
'name': 'Court 3',
'shareable': True,
'capacity': 3,
}])
booking_1 = self.env['calendar.event'].create({
'appointment_type_id': self.apt_type_resource.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': court1.id})],
'name': 'Booking 1',
'start': start,
'stop': end,
})
self.assertFalse(booking_1.unavailable_resource_ids)
booking_2 = self.env['calendar.event'].create({
'appointment_type_id': self.apt_type_resource.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': court1.id}), (0, 0, {'appointment_resource_id': court2.id})],
'name': 'Booking 1',
'start': start,
'stop': end,
})
(booking_1 + booking_2)._compute_unavailable_resource_ids()
self.assertEqual(booking_1.unavailable_resource_ids, court1)
self.assertEqual(booking_2.unavailable_resource_ids, court1)
# Shared resource
booking_3 = self.env['calendar.event'].create({
'appointment_type_id': self.apt_type_resource.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': court3.id, 'capacity_reserved': 1})],
'name': 'Booking 3',
'start': start,
'stop': end,
})
booking_4 = self.env['calendar.event'].create({
'appointment_type_id': self.apt_type_resource.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': court3.id, 'capacity_reserved': 1})],
'name': 'Booking 4',
'start': start,
'stop': end,
})
(booking_3 + booking_4)._compute_unavailable_resource_ids()
self.assertFalse(booking_3.unavailable_resource_ids)
self.assertFalse(booking_4.unavailable_resource_ids)
# add full capacity
booking_4.booking_line_ids = [(0, 0, {'appointment_resource_id': court3.id, 'capacity_reserved': 3})]
(booking_3 + booking_4)._compute_unavailable_resource_ids()
self.assertEqual(booking_3.unavailable_resource_ids, court3)
self.assertEqual(booking_4.unavailable_resource_ids, court3)
@freeze_time('2022-02-14')
def test_resource_unavailability_with_multiple_appointment_events(self):
""" Test that resources are correctly computed as unavailable when multiple appointments are booked
on the same resource and overlapping events.
Here are the cases which are tested, resource with:
- bookings in appointments, some with manage capacity True and some with False.
- bookings in appointment with manage capacity and exceeding the resource capacity.
- bookings in appointment without manage capacity and exceeding the booking appointment capacity.
- bookings in appointments with all manage capacity False.
- bookings in appointments with manage capacity and unshareable resource type.
"""
court1, court2, court3 = self.env['appointment.resource'].create([{
'appointment_type_ids': self.apt_resource_multiple_bookings.ids,
'name': 'Court 1',
'capacity': 5,
}, {
'appointment_type_ids': self.apt_type_resource.ids,
'capacity': 4,
'name': 'Court 2',
'shareable': True,
}, {
'appointment_type_ids': (self.apt_resource_multiple_bookings + self.apt_type_resource).ids,
'capacity': 6,
'name': 'Court 3',
}])
start = datetime(2022, 2, 14, 15, 0, 0)
end = start + timedelta(hours=1)
# Book for one appointments for each resources
booking1, booking2, booking3 = self.env['calendar.event'].create([{
'appointment_type_id': self.apt_resource_multiple_bookings.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': court1.id, 'capacity_reserved': 1})],
'name': 'Booking 1',
'start': start,
'stop': end,
}, {
'appointment_type_id': self.apt_type_resource.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': court2.id, 'capacity_reserved': 2})],
'name': 'Booking 2',
'start': start,
'stop': end,
}, {
'appointment_type_id': self.apt_type_resource.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': court3.id, 'capacity_reserved': 2})],
'name': 'Booking 3',
'start': start,
'stop': end,
}])
self.assertFalse(booking1.unavailable_resource_ids, 'Booking 1 should not have unavailable resources')
self.assertFalse(booking2.unavailable_resource_ids, 'Booking 2 should not have unavailable resources')
self.assertFalse(booking3.unavailable_resource_ids, 'Booking 3 should not have unavailable resources')
appointment_wcapacity = self.apt_type_resource.copy()
appointment_wocapacity = self.apt_resource_multiple_bookings.copy()
self.apt_resource_multiple_bookings.write({'max_bookings': 1})
for (appointment, resource, capacity_reserved, unavailable_resource, conflicting_booking) in [
# Multiple capacity methods
(self.apt_resource_multiple_bookings, court3, 1, court3, booking3),
# All managing capacity and exceeding resource capacity (2 + 4 > 4)
(appointment_wcapacity, court2, 4, court2, booking2),
# Un shareable resource in more than one appointment with manage capacity True.
(appointment_wcapacity, court3, 2, court3, booking3),
# Manage capacity false and exceeding appointment booking capacity (2 > 1)
(self.apt_resource_multiple_bookings, court1, 1, court1, booking1),
# All not managing capacity and booking in more than one appointment.
(appointment_wocapacity, court1, 1, court1, booking1),
]:
with self.subTest(
appointment=appointment, resource=resource, capacity_reserved=capacity_reserved,
unavailable_resource=unavailable_resource, conflicting_booking=conflicting_booking
):
booking = self.env['calendar.event'].create({
'appointment_type_id': appointment.id,
'booking_line_ids': [(0, 0, {'appointment_resource_id': resource.id, 'capacity_reserved': capacity_reserved})],
'name': 'Booking',
'start': start,
'stop': end,
})
(booking + conflicting_booking)._compute_unavailable_resource_ids()
self.assertEqual(conflicting_booking.unavailable_resource_ids, unavailable_resource)
self.assertEqual(booking.unavailable_resource_ids, unavailable_resource)
booking.unlink()
@freeze_time('2022-02-14')
@users('apt_manager')
def test_staff_user_unavailability_with_multiple_appointment_events(self):
""" Test that unavailable users are correctly computed when multiple appointments are booked
on the same user at the same time.
"""
user1, user2 = self.staff_user_aust, self.staff_user_bxls
start = datetime(2022, 2, 14, 15, 0, 0)
end = start + timedelta(hours=1)
self.apt_type_manage_capacity_users.write({'user_capacity': 5})
self.apt_user_multiple_bookings.write({'max_bookings': 1})
# Book one appointment for each users
booking1 = self.env['calendar.event'].with_context(self._test_context).create({
'appointment_type_id': self.apt_type_manage_capacity_users.id,
'booking_line_ids': [(0, 0, {'appointment_user_id': user1.id, 'capacity_reserved': 3})],
'name': 'Booking 1',
'partner_ids': [(4, user1.partner_id.id), (4, self.env.user.partner_id.id)],
'start': start,
'stop': end,
'user_id': user1.id,
})
booking2 = self.env['calendar.event'].with_context(self._test_context).create({
'appointment_type_id': self.apt_user_multiple_bookings.id,
'booking_line_ids': [(0, 0, {'appointment_user_id': user2.id, 'capacity_reserved': 1})],
'name': 'Booking 2',
'partner_ids': [(4, user2.partner_id.id), (4, self.env.user.partner_id.id)],
'start': start,
'stop': end,
'user_id': user2.id,
})
self.assertNotIn(user1.partner_id, booking1.unavailable_partner_ids, f'Booking 1 should not have {user1.partner_id} as unavailable.')
self.assertNotIn(user2.partner_id, booking2.unavailable_partner_ids, f'Booking 2 should not have {user2.partner_id} as unavailable.')
for (appointment, user, capacity_reserved, unavailable_user, conflicting_booking) in [
# More than one appointments
(self.apt_type_manage_capacity_users, user2, 2, user2, booking2),
# Exceeding appointment booking capacity (2 > 1)
(self.apt_user_multiple_bookings, user2, 1, user2, booking2),
# Exceeding user capacity (6 > 5)
(self.apt_type_manage_capacity_users, user1, 3, user1, booking1),
]:
with self.subTest(
appointment=appointment, user=user, capacity_reserved=capacity_reserved,
unavailable_user=unavailable_user, conflicting_booking=conflicting_booking
):
booking = self.env['calendar.event'].with_context(self._test_context).create({
'appointment_type_id': appointment.id,
'booking_line_ids': [(0, 0, {'appointment_user_id': user.id, 'capacity_reserved': capacity_reserved})],
'name': 'Booking',
'partner_ids': [(4, user.partner_id.id), (4, self.env.user.partner_id.id)],
'start': start,
'stop': end,
'user_id': user.id,
})
(booking + conflicting_booking)._compute_unavailable_partner_ids()
self.assertIn(unavailable_user.partner_id, conflicting_booking.unavailable_partner_ids)
self.assertIn(unavailable_user.partner_id, booking.unavailable_partner_ids)
booking.unlink()
@users('apt_manager')
def test_appointment_user_remaining_capacity(self):
""" Test that the remaining capacity of users are correctly computed """
appointment = self.apt_type_manage_capacity_users
user_1 = self.staff_user_aust
user_2 = self.staff_user_bxls
start = datetime(2022, 2, 15, 14, 0, 0)
end = start + timedelta(hours=1)
user_1_remaining_capacity = appointment._get_users_remaining_capacity(user_1, start, end)['total_remaining_capacity']
user_2_remaining_capacity = appointment._get_users_remaining_capacity(user_2, start, end)['total_remaining_capacity']
self.assertTrue(user_1_remaining_capacity == 5)
self.assertTrue(user_2_remaining_capacity == 5)
# Create bookings for users
booking_1, booking_2 = self.env['calendar.event'].with_context(self._test_context).create([{
'appointment_type_id': appointment.id,
'booking_line_ids': [(0, 0, {'capacity_reserved': 2, 'capacity_used': 2})],
'name': 'Booking 1',
'start': start,
'stop': end,
'user_id': user_1.id,
}, {
'appointment_type_id': appointment.id,
'booking_line_ids': [(0, 0, {'capacity_reserved': 1})],
'name': 'Booking 2',
'start': start,
'stop': end,
'user_id': user_2.id,
}])
user_1_remaining_capacity = appointment._get_users_remaining_capacity(user_1, start, end)['total_remaining_capacity']
user_2_remaining_capacity = appointment._get_users_remaining_capacity(user_2, start, end)['total_remaining_capacity']
self.assertTrue(user_1_remaining_capacity == 3, 'The user should have 3 availabilities left.')
self.assertTrue(user_2_remaining_capacity == 4, 'The user should have 4 availabilities left.')
(booking_1 + booking_2).unlink()
self.assertDictEqual(
appointment._get_users_remaining_capacity(self.env['res.users'], start, end),
{'total_remaining_capacity': 0},
'No result should give dict with correct accumulated values.'
)
@users('apt_manager')
def test_appointment_users_shareable(self):
""" Check a user is shareable across only one appointment type """
apt_type_manage_capacity_other = self.env['appointment.type'].create([{
'appointment_tz': 'Europe/Brussels',
'appointment_duration': 1,
'category': 'recurring',
'is_auto_assign': False,
'is_date_first': True,
'location_id': self.staff_user_bxls.partner_id.id,
'name': 'Bxls Appt Type with capacity (Other)',
'max_schedule_days': 15,
'min_cancellation_hours': 1,
'min_schedule_hours': 1,
'manage_capacity': True,
'staff_user_ids': [(6, 0, [self.staff_user_bxls.id, self.staff_user_aust.id])],
'slot_ids': [(0, 0, {
'weekday': str(self.reference_monday.isoweekday()),
'start_hour': 15,
'end_hour': 16,
})],
'user_capacity': 5,
}])
# User has no bookings
with freeze_time(self.reference_now):
slots = self.apt_type_manage_capacity_users._get_appointment_slots('UTC', asked_capacity=2)
user_slots_1 = self._filter_appointment_slots(slots)
slots = apt_type_manage_capacity_other._get_appointment_slots('UTC', asked_capacity=3)
user_slots_2 = self._filter_appointment_slots(slots)
available_users_1 = [user['id'] for user in user_slots_1[0]['available_staff_users']]
available_users_2 = [user['id'] for user in user_slots_2[0]['available_staff_users']]
self.assertIn(self.staff_user_bxls.id, available_users_1)
self.assertIn(self.staff_user_bxls.id, available_users_2)
# Book a user on monday from 14 to 15 for 3 people
start = datetime(2022, 2, 14, 14, 0, 0)
end = start + timedelta(hours=1)
event = self.env['calendar.event'].create([{
'appointment_type_id': self.apt_type_manage_capacity_users.id,
'booking_line_ids': [(0, 0, {'appointment_user_id': self.staff_user_bxls.id, 'capacity_reserved': 3, 'capacity_used': 3})],
'attendee_ids': [(0, 0, {'partner_id': self.staff_user_bxls.partner_id.id, 'state': 'accepted'})],
'name': 'Booking 1',
'partner_ids': [(4, self.staff_user_bxls.partner_id.id)],
'start': start,
'stop': end,
'user_id': self.staff_user_bxls.id
}])
# Check if user is available for 2 people again
with freeze_time(self.reference_monday):
slots = self.apt_type_manage_capacity_users._get_appointment_slots('UTC', asked_capacity=2)
user_slots_1 = self._filter_appointment_slots(slots)
slots = apt_type_manage_capacity_other._get_appointment_slots('UTC', asked_capacity=3)
user_slots_2 = self._filter_appointment_slots(slots)
available_user_1 = [user['id'] for user in user_slots_1[0]['available_staff_users']]
available_user_2 = [user['id'] for user in user_slots_2[0]['available_staff_users']]
event.unlink()
# User is available on the booked appointment until capacity
self.assertIn(self.staff_user_bxls.id, available_user_1)
# User is not available for other appointment when booking has been made for them.
self.assertNotIn(self.staff_user_bxls.id, available_user_2)
@users('apt_manager')
def test_appointment_user_must_have_appointment_type_access(self):
appointment_type = self.env['appointment.type'].create([{
'name': 'Test appointment',
'staff_user_ids': [(6, 0, [self.apt_user.id])],
}])
# Case 1: Staff user should be allowed
event_staff = self.env['calendar.event'].create({
'name': 'Staff user event',
'appointment_type_id': appointment_type.id,
'booking_line_ids': [(0, 0, {
'appointment_user_id': self.apt_user.id,
'capacity_reserved': 1,
})],
'user_id': self.apt_user.id,
})
self.assertTrue(event_staff.exists())
# Case 2: appointment manager should be allowed
event_apt_manager = self.env['calendar.event'].create({
'name': 'Admin non-staff event',
'appointment_type_id': appointment_type.id,
'booking_line_ids': [(0, 0, {
'appointment_user_id': self.env.user.id,
'capacity_reserved': 1,
})],
'user_id': self.env.user.id,
})
self.assertTrue(event_apt_manager.exists())
# Case 3: User who cannot read the appointment type -> Should raise ValidationError
with self.assertRaises(ValidationError):
self.env['calendar.event'].create({
'name': 'Restricted event',
'appointment_type_id': appointment_type.id,
'booking_line_ids': [(0, 0, {
'appointment_user_id': self.staff_user_bxls.id,
'capacity_reserved': 1,
})],
'user_id': self.staff_user_bxls.id,
})