# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import json import pytz from datetime import datetime, timedelta from freezegun import freeze_time from lxml import html from unittest.mock import patch from odoo import Command, http from odoo.addons.appointment.tests.common import AppointmentCommon from odoo.addons.base.models.ir_qweb import IrQweb from odoo.addons.mail.tests.common import mail_new_test_user from odoo.tests import common, tagged, users from odoo.tools import float_compare from odoo.tools.misc import mute_logger class AppointmentUICommon(AppointmentCommon, common.HttpCase): @classmethod def setUpClass(cls): super(AppointmentUICommon, cls).setUpClass() cls.env.user.tz = "Europe/Brussels" cls.std_user = mail_new_test_user( cls.env, company_id=cls.company_admin.id, email='std_user@test.example.com', groups='base.group_user', name='Solène StandardUser', notification_type='email', login='std_user', tz='Europe/Brussels' # UTC + 1 (at least in February) ) cls.portal_user = cls._create_portal_user() def _fetch_appointment_page_info_from_invite(self, invite): page = self.url_open(invite.book_url) arch = html.fromstring(page.text) [slots_form] = arch.xpath("//form[@id='slots_form']") [selected_user_option] = slots_form.xpath("//*[@id='selectStaffUser']/*[@selected]") [slots_calendar] = arch.xpath("//*[@id='calendar']") return slots_form, selected_user_option, slots_calendar @tagged('appointment_ui', '-at_install', 'post_install') class AppointmentUITest(AppointmentUICommon): @users('apt_manager') def test_route_apt_type_search_create_anytime(self): self.authenticate(self.env.user.login, self.env.user.login) request = self.url_open( "/appointment/appointment_type/search_create_anytime", data=json.dumps({}), headers={"Content-Type": "application/json"}, ).json() result = request.get('result', {}) self.assertTrue(result.get('appointment_type_id'), 'The request returns the id of the custom appointment type') appointment_type = self.env['appointment.type'].browse(result['appointment_type_id']) self.assertEqual(appointment_type.category, 'anytime') self.assertEqual(len(appointment_type.slot_ids), 7, "7 slots have been created: (1 / days for 7 days)") self.assertTrue(all(slot.slot_type == 'recurring' for slot in appointment_type.slot_ids), "All slots are 'recurring'") @users('apt_manager') def test_route_apt_type_search_create_anytime_with_context(self): self.authenticate(self.env.user.login, self.env.user.login) request = self.url_open( "/appointment/appointment_type/search_create_anytime", data=json.dumps({ 'params': { 'context': { 'default_is_auto_assign': True, }, } }), headers={"Content-Type": "application/json"}, ).json() result = request.get('result', dict()) self.assertTrue(result.get('appointment_type_id'), 'The request returns the id of the custom appointment type') appointment_type = self.env['appointment.type'].browse(result['appointment_type_id']) # All default context fields should be ignored because of clean_context() self.assertFalse(appointment_type.is_auto_assign) @users('apt_manager') def test_route_apt_type_create_custom(self): self.authenticate(self.env.user.login, self.env.user.login) with freeze_time(self.reference_now): unique_slots = [{ 'start': (datetime.now() + timedelta(hours=1)).replace(microsecond=0).isoformat(' '), 'end': (datetime.now() + timedelta(hours=2)).replace(microsecond=0).isoformat(' '), 'allday': False, }, { 'start': (datetime.now() + timedelta(days=2)).replace(microsecond=0).isoformat(' '), 'end': (datetime.now() + timedelta(days=3)).replace(microsecond=0).isoformat(' '), 'allday': True, }] request = self.url_open( "/appointment/appointment_type/create_custom", data=json.dumps({ 'params': { 'slots': unique_slots, } }), headers={"Content-Type": "application/json"}, ).json() result = request.get('result', {}) self.assertTrue(result.get('appointment_type_id'), 'The request returns the id of the custom appointment type') appointment_type = self.env['appointment.type'].browse(result['appointment_type_id']) self.assertEqual(appointment_type.category, 'custom') self.assertEqual(appointment_type.name, "%s - My availabilities" % self.env.user.name) self.assertEqual(len(appointment_type.slot_ids), 2, "Two slots have been created") with freeze_time(self.reference_now): for slot in appointment_type.slot_ids: self.assertEqual(slot.slot_type, 'unique', 'All slots should be unique') if slot.allday: self.assertEqual(slot.start_datetime, datetime.now() + timedelta(days=2)) self.assertEqual(slot.end_datetime, datetime.now() + timedelta(days=3)) else: self.assertEqual(slot.start_datetime, datetime.now() + timedelta(hours=1)) self.assertEqual(slot.end_datetime, datetime.now() + timedelta(hours=2)) @users('apt_manager') def test_route_create_custom_with_context(self): self.authenticate(self.env.user.login, self.env.user.login) now = datetime.now() unique_slots = [{ 'start': (now + timedelta(hours=1)).replace(microsecond=0).isoformat(' '), 'end': (now + timedelta(hours=2)).replace(microsecond=0).isoformat(' '), 'allday': False, }, { 'start': (now + timedelta(days=2)).replace(microsecond=0).isoformat(' '), 'end': (now + timedelta(days=3)).replace(microsecond=0).isoformat(' '), 'allday': True, }] request = self.url_open( "/appointment/appointment_type/create_custom", data=json.dumps({ 'params': { 'slots': unique_slots, 'context': { 'default_is_auto_assign': True, }, } }), headers={"Content-Type": "application/json"}, ).json() result = request.get('result', dict()) self.assertTrue(result.get('appointment_type_id'), 'The request returns the id of the custom appointment type') appointment_type = self.env['appointment.type'].browse(result['appointment_type_id']) # The default context fields should be ignored as the fields are not whitelisted self.assertFalse(appointment_type.is_auto_assign) def test_share_appointment_type(self): self._create_invite_test_data() self.authenticate(None, None) res = self.url_open(self.invite_apt_type_bxls_2days.book_url) self.assertEqual(res.status_code, 200, "Response should = OK") def test_share_appointment_type_multi(self): self._create_invite_test_data() self.authenticate(None, None) res = self.url_open(self.invite_all_apts.book_url) self.assertEqual(res.status_code, 200, "Response should = OK") @users('apt_manager') @freeze_time('2022-02-15T14:00:00') def test_action_meeting_from_appointment_type(self): """Check values of the action of viewing meetings from clicking an appointment type. Example: Click on 'View Meetings' from an appointment type with no resource management -> open gantt """ now = datetime.now() appointment_types = self.env['appointment.type'].create([{ 'name': 'Type Test Actions Users', 'schedule_based_on': 'users', 'staff_user_ids': (self.apt_manager | self.std_user).ids, }, { 'name': 'Type Test Actions Resource', 'schedule_based_on': 'resources', 'resource_ids': [Command.create({'name': 'Test Resource', 'capacity': 1})] }]) # create an event to test smart scale self.env['calendar.event'].create({ 'name': 'Next Month Appointment', 'appointment_type_id': appointment_types[1].id, 'start': datetime(2022, 3, 1), 'stop': datetime(2022, 3, 1, 1), }) self.maxDiff = None expected_xml_ids = ['appointment.calendar_event_action_view_bookings_users', 'appointment.calendar_event_action_view_bookings_resources'] expected_views_orders = [['gantt', 'calendar'], ['gantt', 'calendar']] expected_contexts = [{ 'appointment_default_assign_user_attendees': True, 'default_scale': 'day', 'default_appointment_type_id': appointment_types[0].id, 'default_duration': appointment_types[0].appointment_duration, 'search_default_appointment_type_id': appointment_types[0].id, 'default_mode': 'week', 'default_partner_ids': [], 'default_start_date': now, 'initial_date': now, }, { 'appointment_booking_gantt_domain': [('appointment_resource_ids', '!=', False)], 'appointment_default_assign_user_attendees': False, 'default_scale': 'day', 'default_appointment_type_id': appointment_types[1].id, 'default_duration': appointment_types[1].appointment_duration, 'search_default_appointment_type_id': appointment_types[1].id, 'default_mode': 'month', 'default_partner_ids': [], 'default_start_date': now, 'initial_date': datetime(2022, 3, 1), }] for (appointment_type, expected_views_order, expected_context, xml_id) in zip(appointment_types, expected_views_orders, expected_contexts, expected_xml_ids): with self.subTest(appointment_type=appointment_type): action = appointment_type.action_calendar_meetings() views_order = [view_id_type[1] for view_id_type in action['views'][:len(expected_views_order)]] self.assertEqual(views_order, expected_views_order) self.assertDictEqual(action['context'], expected_context) self.assertEqual(action['xml_id'], xml_id) @users('apt_manager') def test_appointment_meeting_url(self): """ Test if a meeting linked to an appointment has the right meeting URL no matter its location. """ CalendarEvent = self.env['calendar.event'] self.authenticate(self.env.user.login, self.env.user.login) datetime_format = "%Y-%m-%d %H:%M:%S" discuss_route = CalendarEvent.DISCUSS_ROUTE appointment_with_discuss_videocall_source = self.env['appointment.type'].create({ 'name': 'Schedule a Demo with Odoo Discuss URL', 'staff_user_ids': self.staff_user_bxls, 'event_videocall_source': 'discuss', }) appointment_without_videocall_source = self.env['appointment.type'].create({ 'name': 'Schedule a Demo without meeting URL', 'staff_user_ids': self.staff_user_bxls, 'event_videocall_source': False, }) phone_question = appointment_with_discuss_videocall_source._get_main_phone_question() self.assertTrue(phone_question) self.assertEqual(phone_question, appointment_without_videocall_source._get_main_phone_question()) online_meeting = { 'allday': 0, 'duration_str': '1.0', 'datetime_str': '2022-07-04 12:30:00', 'staff_user_id': self.staff_user_bxls.id, 'name': 'Online Meeting', f'question_{phone_question.id}': '2025550999', 'email': 'test1@test.example.com', 'csrf_token': http.Request.csrf_token(self) } meeting_with_location = { 'allday': 0, 'duration_str': '1.0', 'datetime_str': '2022-07-05 10:30:00', 'staff_user_id': self.staff_user_bxls.id, 'name': 'Meeting with location', f'question_{phone_question.id}': '2025550888', 'email': 'test2@test.example.com', 'csrf_token': http.Request.csrf_token(self), } cases = [ (appointment_with_discuss_videocall_source, online_meeting, True), (appointment_without_videocall_source, online_meeting, False), (appointment_with_discuss_videocall_source, meeting_with_location, True), (appointment_without_videocall_source, meeting_with_location, False), ] for appointment, appointment_data, expect_discuss in cases: # Prevent booking the same slot appt_datetime = datetime.strptime(appointment_data.get('datetime_str'), datetime_format) new_appt_datetime = pytz.timezone(appointment.appointment_tz).localize(appt_datetime) + timedelta(hours=1) appointment_data.update({'datetime_str': new_appt_datetime.strftime(datetime_format)}) if appointment_data.get('name') == 'Meeting with location': appointment.update({'location_id': self.staff_user_bxls.partner_id.id}) url = f"/appointment/{appointment.id}/submit" res = self.url_open(url, data=appointment_data) self.assertEqual(res.status_code, 200, "Response should = OK") event = CalendarEvent.search([('appointment_type_id', '=', appointment.id), ('start', '=', new_appt_datetime.astimezone(pytz.utc).replace(tzinfo=None))]) self.assertIn(event.access_token, res.url) with self.subTest(expect_discuss=expect_discuss, access_token=event.access_token): if expect_discuss: self.assertIn(discuss_route, event.videocall_location, "Should have discuss link as videocall_location as the appointment type videocall source is set to Odoo Discuss") else: self.assertFalse(event.videocall_location, "Should not have a videocall_location as the appointment type doesn't have any videocall source") @freeze_time('2022-02-13') @users('apt_manager') def test_appointment_allday_slot(self): self.authenticate(self.env.user.login, self.env.user.login) 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], }) phone_question = apt_type._get_main_phone_question() appointment_data = { "allday": 1, "appointment_type_id": apt_type.id, "csrf_token": http.Request.csrf_token(self), "datetime_str": "2022-02-15 00:00:00", "duration_str": "48.0", "email": "test@test.example.com", "name": "Online Meeting", f"question_{phone_question.id}": "2025550999", "staff_user_id": self.apt_manager.id, } url = f"/appointment/{apt_type.id}/submit" res = self.url_open(url, data=appointment_data) self.assertEqual(res.status_code, 200, "Response should = OK") meeting = self.env["calendar.event"].search([("appointment_type_id", "=", apt_type.id)]) self.assertTrue(meeting) self.assertTrue(meeting.allday) self.assertEqual(meeting.start_date, unique_slots[1]['start_datetime'].date()) self.assertEqual(meeting.stop_date, unique_slots[1]['end_datetime'].date()) @freeze_time('2022-02-13') @users('apt_manager') def test_appointment_crossing_manual_confirmation_treshold(self): """ Test that when crossing over the manual confirmation treshold, the attendees are not confirmed """ self.assertFalse(self.apt_type_resource.meeting_ids) # Assert initial data self.authenticate(self.env.user.login, self.env.user.login) resource = self.env['appointment.resource'].sudo().create([{ "appointment_type_ids": self.apt_type_resource.ids, "capacity": 4, "name": "Resource", }]) phone_question = self.apt_type_resource._get_main_phone_question() self.assertTrue(phone_question) self.assertTrue(self.apt_type_resource.auto_confirm) self.apt_type_resource.sudo().manual_confirmation_percentage = 0.5 # Set Manual Confirmation at 50% appointment_data = { "allday": 0, "asked_capacity": 4, "available_resource_ids": [resource.id], "csrf_token": http.Request.csrf_token(self), "datetime_str": "2022-02-14 15:00:00", "duration_str": "1.0", "email": "test@test.example.com", "name": "Online Meeting", f"question_{phone_question.id}": "2025550999", } url = f"/appointment/{self.apt_type_resource.id}/submit" res = self.url_open(url, data=appointment_data) self.assertEqual(res.status_code, 200, "Response should = OK") meeting = self.env["calendar.event"].search([("appointment_type_id", "=", self.apt_type_resource.id)]) self.assertTrue(meeting) self.assertEqual(meeting.appointment_status, "request") self.assertEqual(meeting.attendee_ids.state, "accepted", "Crossing over the manual confirmation percentage should confirm the attendees immediately.") self.assertEqual(meeting.total_capacity_reserved, 4) @freeze_time('2022-02-14') @mute_logger('odoo.http') @users('apt_manager') def test_appointment_answer_required_questions(self): """ Check that when submitting a form through the controller, the required questions have indeed been answered. """ self.authenticate(self.env.user.login, self.env.user.login) self.apt_type_bxls_2days.write({ 'max_bookings': 10, 'meeting_ids': False, 'question_ids': False }) event_values = { 'csrf_token': http.Request.csrf_token(self), 'datetime_str': '2022-02-14 11:00:00', 'duration_str': '1.0', 'email': 'test1@test.example.com', 'name': 'Meeting Test', 'staff_user_id': self.staff_user_bxls.id, } answers_vals = [ {'name': 'answer 0', 'sequence': 0}, {'name': 'answer 1', 'sequence': 1} ] questions = self.env['appointment.question'].create([{ 'answer_ids': [Command.create(vals) for vals in answers_vals] if question_type in ['select', 'checkbox'] else False, 'appointment_type_ids': self.apt_type_bxls_2days.ids, 'name': f'question {i}', 'question_type': question_type, 'question_required': question_required, 'sequence': i, } for i, (question_type, question_required) in enumerate( [('phone', False), ('phone', True), ('char', True), ('select', True), ('checkbox', True)] )]) self.assertEqual(len(questions), 5) self.assertEqual(len(questions.answer_ids), 4) self.assertListEqual(self.apt_type_bxls_2days.question_ids.ids, questions.ids) event_answers = { f'question_{questions[0].id}': ' ', f'question_{questions[1].id}': '01234', f'question_{questions[2].id}': 'Hello', f'question_{questions[3].id}': str(questions[3].answer_ids[1].id), f'question_{questions[4].id}_answer_{questions[4].answer_ids[0].id}': True, } res = self.url_open(f"/appointment/{self.apt_type_bxls_2days.id}/submit", {**event_values, **event_answers}) self.assertEqual(res.status_code, 200, "Response should be OK") self.assertEqual(len(self.apt_type_bxls_2days.meeting_ids), 1) self.assertEqual(len(self.apt_type_bxls_2days.meeting_ids.appointment_answer_input_ids), 4) # main phone was stripped out for question_key in [key for key in event_answers if key != f'question_{questions[0].id}']: with self.subTest(missing_mandatory_question_key=question_key): event_answers_copy = event_answers.copy() event_answers_copy.pop(question_key) res = self.url_open(f"/appointment/{self.apt_type_bxls_2days.id}/submit", {**event_values, **event_answers_copy}) self.assertEqual(res.status_code, 422) @freeze_time('2022-03-25 08:00:00') def test_appointment_calendar_navigation_far_min_schedule_hours(self): """ Check that later months still show their slots when a large minimum booking delay moves the first free slot into a later month. """ # Monday only. On Fri 2022-03-25 the 96h delay lands on Tue 03-29, past # March's last Monday (03-28), so the first free slot is Mon 04-04. apt_type = self.env['appointment.type'].create({ 'appointment_tz': 'UTC', 'appointment_duration': 1, 'category': 'recurring', 'is_auto_assign': True, 'max_schedule_days': 90, 'min_schedule_hours': 96, 'name': 'Monday Appt Type', 'schedule_based_on': 'users', 'slot_ids': [Command.create({'weekday': '1', 'start_hour': 8, 'end_hour': 9})], 'staff_user_ids': [Command.link(self.staff_user_bxls.id)], }) invite = self.env['appointment.invite'].create({ 'appointment_type_ids': [Command.set(apt_type.ids)], }) captured_slots = [] render_origin = IrQweb._render def _capture_render(self, template, values=None, **options): """ Capture the month list the controller feeds to the calendar template. """ if template == 'appointment.appointment_calendar': captured_slots.append(values['slots']) return render_origin(self, template, values, **options) def _navigate_to_month(month_id, month_before_update=None): """ Make the same call the browser makes and return the rendered months. """ with patch.object(IrQweb, '_render', _capture_render): response = self.url_open( '/appointment/%s/update_available_slots' % apt_type.id, data=json.dumps({'params': { 'invite_token': invite.access_token, 'filter_appointment_type_ids': json.dumps(apt_type.ids), 'filter_staff_user_ids': '[]', 'filter_resource_ids': '[]', 'timezone': 'UTC', 'month_id': month_id, 'month_before_update': month_before_update, }}), headers={'Content-Type': 'application/json'}, ) self.assertEqual(response.status_code, 200) return captured_slots[-1] # The calendar opens on April, since March has no Monday left after the delay. slots = _navigate_to_month(month_id=0) self.assertEqual(slots[0]['month'], 'April 2022') self.assertTrue( slots[0]['has_availabilities'], "The first displayed month (April) should list bookable slots") # Click 'next': the browser asks for the following month by its index. slots = _navigate_to_month(month_id=1, month_before_update='May 2022') self.assertEqual(slots[1]['month'], 'May 2022') self.assertTrue( slots[1]['has_availabilities'], "Navigating to May must display its Monday slots instead of an empty month") @freeze_time('2022-02-14') @users('apt_manager') def test_appointment_resource_manual_confirmation(self): """ Test that when auto_confirm is False, the appointment type is not confirmed and the attendees are not confirmed until the booking is confirmed manually. """ self.authenticate(self.env.user.login, self.env.user.login) self.assertFalse(self.apt_resource_multiple_bookings.meeting_ids) phone_question = self.apt_resource_multiple_bookings._get_main_phone_question() self.assertTrue(phone_question) resource = self.env['appointment.resource'].sudo().create([{ 'appointment_type_ids': self.apt_resource_multiple_bookings.ids, 'capacity': 4, 'name': 'Resource', }]) self.assertTrue(self.apt_resource_multiple_bookings.auto_confirm) self.assertEqual(float_compare(self.apt_resource_multiple_bookings.manual_confirmation_percentage, 1.0, 3), 0) appointment_data = { 'available_resource_ids': [resource.id], 'csrf_token': http.Request.csrf_token(self), 'datetime_str': '2022-02-14 15:00:00', 'duration_str': '1.0', 'email': 'test@test.example.com', 'name': 'Online Meeting', 'phone': '2025550999', f'question_{phone_question.id}': '2025550999', } url = f'/appointment/{self.apt_resource_multiple_bookings.id}/submit' res = self.url_open(url, data=appointment_data) self.assertEqual(res.status_code, 200, 'Response should = OK') self.assertEqual(len(self.apt_resource_multiple_bookings.meeting_ids), 1) self.assertEqual(self.apt_resource_multiple_bookings.meeting_ids[0].appointment_status, 'booked') self.apt_resource_multiple_bookings.write({'auto_confirm': False}) appointment_data['datetime_str'] = '2022-02-14 16:00:00' res = self.url_open(url, data=appointment_data) self.assertEqual(res.status_code, 200, 'Response should = OK') self.assertEqual(len(self.apt_resource_multiple_bookings.meeting_ids), 2) self.assertEqual( self.apt_resource_multiple_bookings.meeting_ids[0].appointment_status, 'request', 'When manual confirmation is set, the booking status should be set to request.' ) @freeze_time('2022-02-14') @users('apt_manager') def test_appointment_staff_user_manual_confirmation(self): """ Check that appointment and attendee status are correctly set based on the auto_confirm and manual_confirmation_percentage fields""" self.authenticate(self.env.user.login, self.env.user.login) phone_question = self.apt_type_bxls_2days._get_main_phone_question() self.assertTrue(phone_question) event_values = { 'allday': 0, 'csrf_token': http.Request.csrf_token(self), 'datetime_str': '2022-02-14 11:00:00', 'duration_str': '1.0', 'email': 'test1@test.example.com', 'name': 'Meeting Test', f'question_{phone_question.id}': '2025550999', 'staff_user_id': self.staff_user_bxls.id, } self.apt_type_bxls_2days.max_bookings = 3 self.assertTrue(self.apt_type_bxls_2days.auto_confirm) self.assertEqual(float_compare(self.apt_type_bxls_2days.manual_confirmation_percentage, 1.0, 3), 0) self.assertTrue(self.apt_type_bxls_2days.is_always_confirm) res = self.url_open(f"/appointment/{self.apt_type_bxls_2days.id}/submit", event_values) self.assertEqual(res.status_code, 200, "Response should be OK") first_meeting = self.apt_type_bxls_2days.meeting_ids self.assertEqual(len(first_meeting), 1) self.assertEqual(first_meeting.appointment_status, "booked") self.assertTrue(all(attendee.state == 'accepted' for attendee in first_meeting.attendee_ids)) self.apt_type_bxls_2days.manual_confirmation_percentage = 0.5 self.assertFalse(self.apt_type_bxls_2days.is_always_confirm) res = self.url_open(f"/appointment/{self.apt_type_bxls_2days.id}/submit", event_values) self.assertEqual(res.status_code, 200, "Response should be OK") second_meeting = self.apt_type_bxls_2days.meeting_ids - first_meeting self.assertEqual(len(second_meeting), 1) self.assertEqual(second_meeting.appointment_status, "request") self.assertTrue(all(attendee.state == 'accepted' for attendee in second_meeting.attendee_ids)) # Despite 100% manual_confirmation_percentage, should be 'request' as auto_confirm is False self.apt_type_bxls_2days.write({ 'auto_confirm': False, 'manual_confirmation_percentage': 1.0 }) self.assertFalse(self.apt_type_bxls_2days.is_always_confirm) res = self.url_open(f"/appointment/{self.apt_type_bxls_2days.id}/submit", event_values) self.assertEqual(res.status_code, 200, "Response should be OK") third_meeting = self.apt_type_bxls_2days.meeting_ids - (first_meeting | second_meeting) self.assertEqual(len(third_meeting), 1) self.assertEqual(third_meeting.appointment_status, "request") self.assertTrue(all(attendee.state == 'accepted' for attendee in third_meeting.attendee_ids)) @freeze_time('2022-02-14') @users('apt_manager') def test_appointment_staff_user_manual_confirmation_percentage(self): """ Check that appointment and attendee status are correctly set based on the auto_confirm and manual_confirmation_percentage fields when enabled, when booking (multiple) users and managing capacity """ self.authenticate(self.env.user.login, self.env.user.login) # 2 users, 3 capacity -> 6 total. self.apt_type_manage_capacity_users.write({ 'auto_confirm': True, 'manual_confirmation_percentage': 0.5, 'user_capacity': 3, }) self.apt_type_manage_capacity_users.staff_user_ids = [Command.set([self.staff_user_aust.id, self.staff_user_bxls.id])] phone_question = self.apt_type_manage_capacity_users._get_main_phone_question() self.assertTrue(phone_question) event_values = { 'csrf_token': http.Request.csrf_token(self), 'datetime_str': '2022-02-14 11:00:00', 'duration_str': '1.0', 'email': 'test1@test.example.com', 'name': 'Meeting Test', f'question_{phone_question.id}': '2025550999', 'staff_user_id': self.staff_user_bxls.id, 'asked_capacity': 3, } # Booking for 3 capacity with one staff user. Total used is 3/6. It should create a 'booked' event as <= 50% res = self.url_open(f"/appointment/{self.apt_type_manage_capacity_users.id}/submit", event_values) self.assertEqual(res.status_code, 200, "Response should be OK") first_meeting = self.apt_type_manage_capacity_users.meeting_ids self.assertEqual(len(first_meeting), 1) self.assertEqual(first_meeting.appointment_status, "booked") self.assertTrue(all(attendee.state == 'accepted' for attendee in first_meeting.attendee_ids)) # Booking for 1 capacity with another staff user. Total used is 4/6. It should create a 'request' event as > 50% event_values.update({ 'asked_capacity': 1, 'staff_user_id': self.staff_user_aust.id, }) res = self.url_open(f"/appointment/{self.apt_type_manage_capacity_users.id}/submit", event_values) self.assertEqual(res.status_code, 200, "Response should be OK") second_meeting = self.apt_type_manage_capacity_users.meeting_ids - first_meeting self.assertEqual(len(second_meeting), 1) self.assertEqual(second_meeting.appointment_status, "request") self.assertTrue(all(attendee.state == 'accepted' for attendee in second_meeting.attendee_ids)) @tagged('appointment_ui', '-at_install', 'post_install') class CalendarTest(AppointmentUICommon): def test_meeting_accept_authenticated(self): event = self.env["calendar.event"].create( {"name": "Doom's day", "start": datetime(2019, 10, 25, 8, 0), "stop": datetime(2019, 10, 27, 18, 0), "partner_ids": [(4, self.std_user.partner_id.id)], } ) token = event.attendee_ids[0].access_token url = "/calendar/meeting/accept?token=%s&id=%d" % (token, event.id) self.authenticate(self.std_user.login, self.std_user.login) res = self.url_open(url) self.assertEqual(res.status_code, 200, "Response should = OK") event.attendee_ids[0].invalidate_recordset() self.assertEqual(event.attendee_ids[0].state, "accepted", "Attendee should have accepted") def test_meeting_accept_unauthenticated(self): event = self.env["calendar.event"].create( {"name": "Doom's day", "start": datetime(2019, 10, 25, 8, 0), "stop": datetime(2019, 10, 27, 18, 0), "partner_ids": [(4, self.std_user.partner_id.id)], } ) token = event.attendee_ids[0].access_token url = "/calendar/meeting/accept?token=%s&id=%d" % (token, event.id) res = self.url_open(url) self.assertEqual(res.status_code, 200, "Response should = OK") event.attendee_ids[0].invalidate_recordset() self.assertEqual(event.attendee_ids[0].state, "accepted", "Attendee should have accepted") @freeze_time('2023, 11, 22') def test_meeting_booker_cancel(self): """ Test that when appointment Booker cancels the meeting then the event should be archived. """ self.authenticate(self.portal_user.login, self.portal_user.login) self.apt_type_bxls_2days.write({'is_published': 'True'}) event = self.env['calendar.event'].create({ 'name': 'Test-Meeting 2', 'user_id': self.portal_user.id, 'appointment_booker_id': self.portal_user.partner_id.id, 'start': datetime(2023, 11, 23, 9, 0), 'stop': datetime(2023, 11, 23, 10, 0), 'partner_ids': [ (4, self.staff_user_nz.partner_id.id), (4, self.staff_user_aust.partner_id.id), (4, self.apt_manager.partner_id.id), (4, self.portal_user.partner_id.id), ], 'appointment_type_id': self.apt_type_bxls_2days.id, }) cancel_meeting_data = { 'access_token': event.access_token, 'partner_id': self.portal_user.partner_id.id, 'csrf_token': http.Request.csrf_token(self), } cancel_meeting_url = f"/calendar/{event.access_token}/cancel" res = self.url_open(cancel_meeting_url, data=cancel_meeting_data) self.assertEqual(res.status_code, 200) self.assertFalse(event.active) @freeze_time('2023, 11, 22') def test_meeting_cancel_no_partner(self): """ Test cancellation of meeting with no partner. """ self.authenticate(None, None) self.apt_type_bxls_2days.write({'is_published': 'True'}) event = self.env['calendar.event'].create({ 'name': 'Test-Meeting 1', 'user_id': self.staff_user_aust.id, 'appointment_booker_id': self.staff_user_nz.partner_id.id, 'start': datetime(2023, 11, 23, 8, 0), 'stop': datetime(2023, 11, 23, 9, 0), 'partner_ids': [ (4, self.staff_user_nz.partner_id.id), (4, self.portal_user.partner_id.id), (4, self.staff_user_aust.partner_id.id), ], 'appointment_type_id': self.apt_type_bxls_2days.id, }) cancel_meeting_data = { 'access_token': event.access_token, 'csrf_token': http.Request.csrf_token(self), } cancel_meeting_url = f"/calendar/{event.access_token}/cancel" res = self.url_open(cancel_meeting_url, data=cancel_meeting_data) self.assertEqual(res.status_code, 200) self.assertFalse(event.active) @tagged('appointment_ui') class AppointmentUIAtInstallTest(AppointmentUICommon): @freeze_time('2022-02-14T7:00:00') def test_get_appointment_type_page_view(self): """ Test if the appointment_type_page always shows available slots if there are some. """ now = self.reference_monday user_admin = self.env.ref('base.user_admin') slot_time = now.replace(hour=9, minute=0, second=0, microsecond=0) + timedelta(days=1) staff_users = self.std_user | user_admin appointment_type = self.env['appointment.type'].create([{ 'name': 'Type Test Appointment View', 'schedule_based_on': 'users', 'staff_user_ids': staff_users.ids, 'min_schedule_hours': 1.0, 'max_schedule_days': 5, 'slot_ids': [(0, 0, { 'weekday': str(slot_time.isoweekday()), 'start_hour': slot_time.hour, 'end_hour': slot_time.hour + 1, })], 'show_avatars': False, 'is_auto_assign': False, 'is_date_first': False, }]) invite = self.env['appointment.invite'].create({ 'appointment_type_ids': appointment_type.ids, }) slots_form, selected_user_option, slots_calendar = self._fetch_appointment_page_info_from_invite(invite) self.assertIn( int(selected_user_option.attrib['value']), staff_users.ids, f"Selected user must be one of {staff_users.ids}" ) self.assertFalse( 'd-none' in slots_form.getparent().attrib['class'], "Staff user selector should be visible") self.assertTrue( slots_calendar.getchildren(), "Slots calendar should be visible") # create an event to make the first staff user busy and remove its available slots selected_staff_user = self.env['res.users'].browse(int(selected_user_option.attrib['value'])) remaining_staff_user = staff_users - selected_staff_user self._create_meetings(selected_staff_user, [(slot_time, slot_time + timedelta(hours=1), True)]) slots_form, selected_user_option, slots_calendar = self._fetch_appointment_page_info_from_invite(invite) self.assertEqual( int(selected_user_option.attrib['value']), remaining_staff_user.id, f"Selected user should be user with ID: {remaining_staff_user.id}") self.assertFalse( 'd-none' in slots_form.getparent().attrib['class'], "Staff user selector should be visible") self.assertTrue( slots_calendar.getchildren(), "Slots calendar should be visible") # create another event to make both staff user busy and remove all slots self._create_meetings(remaining_staff_user, [(slot_time, slot_time + timedelta(hours=1), True)]) slots_form, _, slots_calendar = self._fetch_appointment_page_info_from_invite(invite) self.assertTrue( 'd-none' in slots_form.getparent().attrib['class'], "Staff user selector should not be visible") self.assertFalse( slots_calendar.getchildren(), "Slots calendar should not be visible")