# Part of Odoo. See LICENSE file for full copyright and licensing details. from werkzeug.exceptions import Forbidden from odoo import http, fields, _ from odoo.exceptions import ValidationError from odoo.http import request, route class AppointmentCalendarView(http.Controller): # ------------------------------------------------------------ # APPOINTMENT JSON ROUTES FOR BACKEND # ------------------------------------------------------------ @route('/appointment/appointment_type/create_custom', type='jsonrpc', auth='user') def appointment_type_create_custom(self, slots, context=None): """ Return the info (id and url) of the custom appointment type that is created with the time slots in the calendar. Users would typically use this feature to create a custom appointment type for a specific customer and suggest a few hand-picked slots from the calendar view that work best for that appointment. Contrary to regular appointment types that are meant to be re-used several times week after week (e.g.: "Schedule Demo"), this category of appointment type will be unlink after some time has passed. - slots format: [{ 'start': '2021-06-25 13:30:00', 'end': '2021-06-25 15:30:00', 'allday': False, }, { 'start': '2021-06-25 22:00:00', 'end': '2021-06-26 22:00:00', 'allday': True, },...] The timezone used for the slots is UTC """ if not slots: raise ValidationError(_("A list of slots information is needed to create a custom appointment type")) # Check if the user is a member of group_user to avoid portal user and the like to create appointment types if not request.env.user._is_internal(): raise Forbidden() if context: request.update_context(**context) AppointmentType = request.env['appointment.type'] appointment_type = AppointmentType.with_context( AppointmentType._get_clean_appointment_context() ).create({ 'name': _('%(name)s - My availabilities', name=request.env.user.name), 'category': 'custom', 'slot_ids': [(0, 0, { 'start_datetime': fields.Datetime.from_string(slot.get('start')), 'end_datetime': fields.Datetime.from_string(slot.get('end')), 'allday': slot.get('allday'), 'slot_type': 'unique', }) for slot in slots], 'staff_user_ids': request.env.user.ids, }) return self._get_staff_user_appointment_invite_info(appointment_type) @route('/appointment/appointment_type/update_custom', type='jsonrpc', auth='user') def appointment_type_update_custom(self, appointment_type_id, slots): """ Updates the slots of a custom appointment when a user changes them on the calendar view, on sharing link / opening the configuration form. """ if not appointment_type_id or not request.env.user._is_internal(): raise Forbidden() if not slots: raise ValidationError(_("A list of slots information is needed to update this custom appointment type")) appointment_type = request.env['appointment.type'].browse(int(appointment_type_id)) if appointment_type.category != 'custom' or appointment_type.create_uid != request.env.user: raise Forbidden() appointment_type.write({ 'slot_ids': [(5, 0, 0)] + [(0, 0, { 'allday': slot.get('allday'), 'end_datetime': fields.Datetime.from_string(slot.get('end')), 'slot_type': 'unique', 'start_datetime': fields.Datetime.from_string(slot.get('start')), }) for slot in slots], }) return True @route('/appointment/appointment_type/get_book_url', type='jsonrpc', auth='user') def appointment_get_book_url(self, appointment_type_id, context=None): """ Get the information of the appointment invitation used to share the link of the appointment type selected. """ if context: request.update_context(**context) appointment_type = request.env['appointment.type'].browse(int(appointment_type_id)).exists() if not appointment_type: raise ValidationError(_("An appointment type is needed to get the link.")) return self._get_staff_user_appointment_invite_info(appointment_type) @route('/appointment/appointment_type/get_staff_user_appointment_types', type='jsonrpc', auth='user') def appointment_get_user_appointment_types(self): appointment_types_info = [] domain = [('schedule_based_on', '=', 'users'), ('staff_user_ids', 'in', [request.env.user.id]), ('category', 'in', ['punctual', 'recurring'])] appointment_types_info = request.env['appointment.type'].search_read(domain, ['name', 'category']) return { 'appointment_types_info': appointment_types_info, } @route('/appointment/appointment_type/search_create_anytime', type='jsonrpc', auth='user') def appointment_type_search_create_anytime(self, context=None): return self._appointment_type_search_create_anytime(context=context) @classmethod def _appointment_type_search_create_anytime(cls, context=None, user=None): """ Return the info (id and url) of the anytime appointment type of the actual user. Search and return the anytime appointment type for the user. In case it doesn't exist yet, it creates an anytime appointment type. """ user = user or request.env.user # Check if the user is a member of group_user to avoid portal user and the like to create appointment types if not user._is_internal(): raise Forbidden() AppointmentType = request.env['appointment.type'].sudo() appointment_type = AppointmentType.search([ ('category', '=', 'anytime'), ('staff_user_ids', 'in', user.ids)]) if context: request.update_context(**context) if not appointment_type: appt_type_vals = cls._prepare_appointment_type_anytime_values(user) appointment_type = AppointmentType.with_context( AppointmentType._get_clean_appointment_context() ).create(appt_type_vals) return cls._get_staff_user_appointment_invite_info(appointment_type) # Utility Methods # ---------------------------------------------------------- @classmethod def _prepare_appointment_type_anytime_values(cls, user): return { 'name': _("Meeting with %(name)s", name=user.name), 'max_schedule_days': 15, 'category': 'anytime', 'staff_user_ids': [user.id], } @classmethod def _get_staff_user_appointment_invite_info(cls, appointment_type, user=None): user = user or request.env.user appointment_invitation_domain = cls._get_staff_user_appointment_invite_domain(appointment_type, user) appointment_invitation = request.env['appointment.invite'].sudo().search(appointment_invitation_domain, limit=1) if not appointment_invitation: invitation_values = { 'appointment_type_ids': appointment_type.ids, 'resources_choice': 'current_user', 'staff_user_ids': user.ids, 'short_code': request.env['appointment.invite']._get_unique_short_code(appointment_type=appointment_type), } if appointment_type.category == 'custom': # Custom appointment users may be edited on the spot. 'all_assigned_resources' # allows the link copied to be correctly configured with latest users. invitation_values.update({ 'resources_choice': 'all_assigned_resources', 'staff_user_ids': False, }) appointment_invitation = request.env['appointment.invite'].sudo().with_context( request.env['appointment.type']._get_clean_appointment_context() ).create(invitation_values) dialog_form_view = request.env.ref( 'appointment.appointment_type_view_form_custom_share', raise_if_not_found=False ) return { 'appointment_type_id': appointment_type.id, 'invite_url': appointment_invitation.book_url, 'view_id': dialog_form_view.id if dialog_form_view else False, } @classmethod def _get_staff_user_appointment_invite_domain(cls, appointment_type, user): """ Returns the domain used to search for an existing invitation when copying a link to any 'punctual' or 'recurring' appointment type in the calendar view. When sharing an 'anytime' appointment, we search for existing invitations (as the appointment may already exist). This prevents duplicating appointment.invite. The user can modify 'custom' appointments and share their link more than once. Indeed, the 'configure' button allows them to change staff users. They can even remove themselves from the staff users. As a new 'custom' appointment is created on using the 'select dates' feature on the fly, we want to search for the invitation that has been created by the current user. """ if appointment_type.category == 'custom': return [ ('appointment_type_ids', '=', appointment_type.id), ('create_uid', '=', user.id), ] return [ ('appointment_type_ids', '=', appointment_type.id), ('staff_user_ids', '=', user.id), ]