# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import pytz from babel.dates import format_datetime, format_date from datetime import datetime, timedelta from werkzeug.exceptions import Forbidden, BadRequest from werkzeug.urls import url_encode from odoo import fields, _ from odoo.addons.base.models.ir_qweb import keep_query from odoo.addons.calendar.controllers.main import CalendarController from odoo.http import content_disposition, request, route from odoo.tools.misc import get_lang from odoo.tools import consteq class AppointmentCalendarController(CalendarController): # ------------------------------------------------------------ # CALENDAR EVENT VIEW # ------------------------------------------------------------ @route() def view_meeting(self, token, id): """Redirect the internal logged in user to the form view of calendar.event, and redirect regular attendees to the website page of the calendar.event for appointments""" super(AppointmentCalendarController, self).view_meeting(token, id) attendee = request.env['calendar.attendee'].sudo().search([ ('access_token', '=', token), ('event_id', '=', int(id))]) if not attendee: return request.render("appointment.appointment_invalid", {}) # If user is internal and logged, redirect to form view of event if request.env.user._is_internal(): return request.redirect(f'/odoo/{attendee.event_id._name}/{id}?db={request.env.cr.dbname}') request.session['timezone'] = attendee.partner_id.tz if not attendee.event_id.access_token: attendee.event_id._generate_access_token() return request.redirect( f'/calendar/view/{attendee.event_id.access_token}?partner_id={attendee.partner_id.id}&attendee_token={token}' ) @route(['/calendar/view/'], type='http', auth="public", website=True) def appointment_view(self, access_token, partner_id=False, state=False, attendee_token=None, **kwargs): """ Render the validation of an appointment and display a summary of it :param access_token: the access_token of the event linked to the appointment :param partner_id: id of the partner who booked the appointment :param state: allow to display an info message, possible values: - 'new': Info message displayed when the appointment has been correctly created - other values: see _get_prevent_cancel_status """ partner_id = int(partner_id) if partner_id else False event = request.env['calendar.event'].with_context(active_test=False).sudo().search([('access_token', '=', access_token)], limit=1) if not event: return request.not_found() timezone = request.session.get('timezone') if not timezone: timezone = request.env.context.get('tz') or event.appointment_type_id.appointment_tz or event.partner_ids and event.partner_ids[0].tz or event.user_id.tz or 'UTC' request.session['timezone'] = timezone tz_session = pytz.timezone(timezone) format_func = format_datetime if not event.allday: url_date_start = fields.Datetime.from_string(event.start).strftime('%Y%m%dT%H%M%SZ') url_date_stop = fields.Datetime.from_string(event.stop).strftime('%Y%m%dT%H%M%SZ') date_start = fields.Datetime.from_string(event.start).replace(tzinfo=pytz.utc).astimezone(tz_session) else: url_date_start = fields.Date.from_string(event.start_date).strftime('%Y%m%d') url_date_stop = fields.Date.from_string(event.stop_date).strftime('%Y%m%d') date_start = fields.Date.from_string(event.start_date) format_func = format_date locale = get_lang(request.env).code day_name = format_func(date_start, 'EEE', locale=locale) date_start = f'{day_name} {format_func(date_start, locale=locale)}' params = { 'action': 'TEMPLATE', 'text': event._get_customer_summary(), 'dates': f'{url_date_start}/{url_date_stop}', 'details': event._get_customer_description(), } if event.location: params.update(location=event.location.replace('\n', ' ')) encoded_params = url_encode(params) google_url = 'https://www.google.com/calendar/render?' + encoded_params attendee = event.attendee_ids.filtered(lambda attendee: attendee.partner_id.id == int(partner_id)) attendee = attendee_token and consteq(attendee.access_token or '', attendee_token) and attendee return request.render("appointment.appointment_validated", { 'cancel_responsible': event.user_id if event.user_id.active and event.user_id._is_internal() else False, 'event': event, 'event_attendee': attendee, 'datetime_start': date_start, 'google_url': google_url, 'state': state, 'partner_id': partner_id, 'attendee_status': event.attendee_ids.filtered(lambda a: a.partner_id.id == partner_id).state if partner_id else False, 'is_cancelled': not event.active, }, headers={'Cache-Control': 'no-store'}) @route(['/calendar//add_attendees_from_emails'], type="jsonrpc", auth="public", website=True) def appointment_add_attendee(self, access_token, emails_str): """ Add the attendee at the time of the validation of an appointment page :param access_token: access_token of the event linked to the appointment :param emails_str: guest emails in the block of text """ event_sudo = request.env['calendar.event'] event_sudo = event_sudo.sudo().search([('access_token', '=', access_token)], limit=1) if not event_sudo: return request.not_found() if not event_sudo.appointment_type_id.allow_guests: raise BadRequest() if not emails_str: return [] guests = event_sudo.sudo()._find_or_create_partners(emails_str) if guests: event_sudo.write({ 'partner_ids': [(4, pid.id, False) for pid in guests] }) @route(['/calendar/cancel/', '/calendar//cancel', ], type='http', auth="public", website=True) def appointment_cancel(self, access_token, partner_id=False, **kwargs): """ Route to cancel an appointment event, this route is linked to a button in the validation page """ event = request.env['calendar.event'].sudo().search([('access_token', '=', access_token)], limit=1) appointment_type = event.appointment_type_id appointment_invite = event.appointment_invite_id if not event: return request.not_found() if cancel_status := self._get_prevent_cancel_status(event): return request.redirect(f'/calendar/view/{access_token}?state={cancel_status}&partner_id={partner_id}') event.sudo().action_cancel_meeting([int(partner_id)] if partner_id else []) if appointment_invite: redirect_url = appointment_invite.redirect_url + '&state=cancel' else: reset_params = {'state': 'cancel'} if appointment_type.schedule_based_on == 'resources': reset_params.update({ 'resource_selected_id': '', 'available_resource_ids': '', }) redirect_url = f'/appointment/{appointment_type.id}?{keep_query("*", **reset_params)}' return request.redirect(redirect_url) def _get_prevent_cancel_status(self, event): """ This method returns status corresponding to any reason preventing event cancelling. It can be overriden to add other cancelling condition checks and return their status value. """ if (fields.Datetime.from_string(event.allday and event.start_date or event.start) < datetime.now() + timedelta(hours=event.appointment_type_id.min_cancellation_hours)): return 'no_time_left' return False @route(['/calendar/ics/.ics'], type='http', auth="public", website=True) def appointment_get_ics_file(self, access_token, **kwargs): """ Route to add the appointment event in a iCal/Outlook calendar """ event = request.env['calendar.event'].sudo().search([('access_token', '=', access_token)], limit=1) if not event or not event.attendee_ids: return request.not_found() files = event._get_ics_file() content = files[event.id] return request.make_response(content, [ ('Content-Type', 'application/octet-stream'), ('Content-Length', len(content)), ('Content-Disposition', content_disposition(event._get_customer_summary() + '.ics')), ]) @route('/calendar/videocall/', type='http', auth='public') def calendar_videocall(self, access_token): if not access_token: raise Forbidden() event = request.env['calendar.event'].sudo().search([('access_token', '=', access_token)], limit=1) if not event or not event.videocall_location: return request.not_found() if event.videocall_source == 'discuss': return self.calendar_join_videocall(access_token) # custom / google_meet return request.redirect(event.videocall_location, local=False)