forked from Mapan/odoo17e
277 lines
14 KiB
Python
277 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import re
|
|
import uuid
|
|
from markupsafe import Markup
|
|
from werkzeug.urls import url_encode, url_join
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import ValidationError
|
|
|
|
SHORT_CODE_PATTERN = re.compile(r"^[\w-]+$")
|
|
|
|
|
|
class AppointmentShare(models.Model):
|
|
_name = 'appointment.invite'
|
|
_description = 'Appointment Invite'
|
|
_order = 'create_date DESC, id DESC'
|
|
_rec_name = 'short_code'
|
|
|
|
access_token = fields.Char('Token', default=lambda s: uuid.uuid4(), required=True, copy=False, readonly=True)
|
|
short_code = fields.Char('Short Code', default=lambda s: s._get_unique_short_code(), required=True)
|
|
short_code_format_warning = fields.Boolean('Short Code Format Warning', compute="_compute_short_code_warning")
|
|
short_code_unique_warning = fields.Boolean('Short Code Unique Warning', compute="_compute_short_code_warning")
|
|
disable_save_button = fields.Boolean('Computes if alert is present', compute='_compute_disable_save_button')
|
|
|
|
base_book_url = fields.Char('Base Link URL', compute="_compute_base_book_url")
|
|
book_url = fields.Char('Link URL', compute='_compute_book_url')
|
|
redirect_url = fields.Char('Redirect URL', compute='_compute_redirect_url')
|
|
|
|
# Put active_test to False because we always want to be able to check all appointment types from an invitation.
|
|
# In case the appointment type is archived, we still want old links to work and display with a message telling
|
|
# that it's no longer available.
|
|
appointment_type_ids = fields.Many2many('appointment.type', string='Appointment Types',
|
|
domain="[('category', 'in', ['punctual', 'recurring'])]", context={"active_test": False})
|
|
appointment_type_info_msg = fields.Html('No User Assigned Message', compute='_compute_appointment_type_info_msg')
|
|
appointment_type_count = fields.Integer('Selected Appointments Count', compute='_compute_appointment_type_count', store=True)
|
|
schedule_based_on = fields.Char('Schedule Based On', compute="_compute_schedule_based_on")
|
|
suggested_resource_ids = fields.Many2many('appointment.resource', related="appointment_type_ids.resource_ids", string="Possible resources")
|
|
suggested_resource_count = fields.Integer('# Resources', compute='_compute_suggested_resource_count')
|
|
suggested_staff_user_ids = fields.Many2many(
|
|
'res.users', related='appointment_type_ids.staff_user_ids', string='Possible users',
|
|
help="Get the users linked to the appointment type selected to apply a domain on the users that can be selected")
|
|
suggested_staff_user_count = fields.Integer('# Staff Users', compute='_compute_suggested_staff_user_count')
|
|
resources_choice = fields.Selection(
|
|
selection=[
|
|
('current_user', 'Me (only with Users)'),
|
|
('all_assigned_resources', 'Any User/Resource'),
|
|
('specific_resources', 'Specific Users/Resources')],
|
|
string='Assign to', compute='_compute_resources_choice', store=True, readonly=False)
|
|
resource_ids = fields.Many2many('appointment.resource', string='Resources', domain="[('id', 'in', suggested_resource_ids)]",
|
|
compute='_compute_resource_ids', store=True, readonly=False)
|
|
staff_user_ids = fields.Many2many('res.users', string='Users', domain="[('id', 'in', suggested_staff_user_ids)]",
|
|
compute='_compute_staff_user_ids', store=True, readonly=False)
|
|
|
|
calendar_event_ids = fields.One2many('calendar.event', 'appointment_invite_id', string="Booked Appointments", readonly=True)
|
|
calendar_event_count = fields.Integer('# Bookings', compute="_compute_calendar_event_count")
|
|
|
|
_sql_constraints = [
|
|
('short_code_uniq', 'UNIQUE (short_code)', 'The URL is already taken, please pick another code.')
|
|
]
|
|
|
|
@api.depends('short_code_format_warning',
|
|
'short_code_unique_warning',
|
|
'appointment_type_count',
|
|
'suggested_resource_count',
|
|
'suggested_staff_user_ids',
|
|
'resources_choice')
|
|
def _compute_disable_save_button(self):
|
|
for invite in self:
|
|
conditions = [
|
|
invite.short_code_format_warning,
|
|
invite.short_code_unique_warning,
|
|
invite.appointment_type_count == 1 and invite.resources_choice == 'current_user' and self.env.user.id not in invite.suggested_staff_user_ids.ids,
|
|
not invite.suggested_staff_user_ids and invite.appointment_type_count == 1 and invite.suggested_resource_count < 1,
|
|
]
|
|
invite.disable_save_button = any(conditions)
|
|
|
|
@api.constrains('short_code')
|
|
def _check_short_code_format(self):
|
|
invalid_invite = next((invite for invite in self if invite.short_code_format_warning), False)
|
|
if invalid_invite:
|
|
raise ValidationError(_(
|
|
"Only letters, numbers, underscores and dashes are allowed in your links. You need to adapt %s.", invalid_invite.short_code
|
|
))
|
|
|
|
@api.depends('appointment_type_ids')
|
|
def _compute_schedule_based_on(self):
|
|
""" Get the schedule_based_on value when selecting one appointment type.
|
|
This allows to personalize the warning or info message based on this value. """
|
|
for invite in self:
|
|
invite.schedule_based_on = invite.appointment_type_ids.schedule_based_on if len(invite.appointment_type_ids) == 1 else False
|
|
|
|
@api.depends('appointment_type_ids', 'appointment_type_count')
|
|
def _compute_appointment_type_info_msg(self):
|
|
'''
|
|
When there is more than one appointment type selected to be shared and at least one doesn't have any staff user or resource assigned,
|
|
display an alert info to tell the current user that, without staff users or resources, an appointment type won't be published.
|
|
'''
|
|
for invite in self:
|
|
appt_without_staff_user = invite.appointment_type_ids.filtered_domain([('schedule_based_on', '=', 'users'), ('staff_user_ids', '=', False)])
|
|
appt_without_resource = invite.appointment_type_ids.filtered_domain([('schedule_based_on', '=', 'resources'), ('resource_ids', '=', False)])
|
|
appointment_type_info_msg = Markup()
|
|
if appt_without_staff_user and invite.appointment_type_count > 1:
|
|
appointment_type_info_msg += _(
|
|
'The following appointment type(s) have no staff assigned: %s.',
|
|
', '.join(appt_without_staff_user.mapped('name'))
|
|
) + Markup('<br/>')
|
|
if appt_without_resource and invite.appointment_type_count > 1:
|
|
appointment_type_info_msg += _(
|
|
'The following appointment type(s) have no resource assigned: %s.',
|
|
', '.join(appt_without_resource.mapped('name'))
|
|
)
|
|
invite.appointment_type_info_msg = appointment_type_info_msg if appointment_type_info_msg else False
|
|
|
|
@api.depends('appointment_type_ids')
|
|
def _compute_appointment_type_count(self):
|
|
appointment_data = self.env['appointment.type']._read_group(
|
|
[('appointment_invite_ids', 'in', self.ids)],
|
|
['appointment_invite_ids'],
|
|
['__count'],
|
|
)
|
|
mapped_data = {appointment_invite.id: count for appointment_invite, count in appointment_data}
|
|
for invite in self:
|
|
if isinstance(invite.id, models.NewId):
|
|
invite.appointment_type_count = len(invite.appointment_type_ids)
|
|
else:
|
|
invite.appointment_type_count = mapped_data.get(invite.id, 0)
|
|
|
|
@api.depends('short_code')
|
|
def _compute_base_book_url(self):
|
|
for invite in self:
|
|
invite.base_book_url = url_join(invite.get_base_url(), '/book/')
|
|
|
|
@api.depends('calendar_event_ids')
|
|
def _compute_calendar_event_count(self):
|
|
appointment_invite_data = self.env['calendar.event']._read_group(
|
|
[('appointment_invite_id', 'in', self.ids)],
|
|
['appointment_invite_id'],
|
|
['__count'],
|
|
)
|
|
mapped_data = {invite.id: count for invite, count in appointment_invite_data}
|
|
for invite in self:
|
|
invite.calendar_event_count = mapped_data.get(invite.id, 0)
|
|
|
|
@api.depends('short_code')
|
|
def _compute_short_code_warning(self):
|
|
for invite in self:
|
|
invite.short_code_format_warning = not bool(re.match(SHORT_CODE_PATTERN, invite.short_code)) if invite.short_code else False
|
|
invite.short_code_unique_warning = bool(self.env['appointment.invite'].search_count([
|
|
('id', '!=', invite._origin.id), ('short_code', '=', invite.short_code)]))
|
|
|
|
@api.depends('appointment_type_ids')
|
|
def _compute_resources_choice(self):
|
|
for invite in self:
|
|
if len(invite.appointment_type_ids) != 1:
|
|
invite.resources_choice = False
|
|
elif invite.appointment_type_ids.schedule_based_on == 'users' and self.env.user in invite.appointment_type_ids._origin.staff_user_ids:
|
|
invite.resources_choice = 'current_user'
|
|
else:
|
|
invite.resources_choice = 'all_assigned_resources'
|
|
|
|
@api.depends('appointment_type_ids')
|
|
def _compute_resource_ids(self):
|
|
for invite in self:
|
|
if len(invite.appointment_type_ids) > 1 or invite.appointment_type_ids.schedule_based_on != 'resources':
|
|
invite.resource_ids = False
|
|
|
|
@api.depends('appointment_type_ids', 'resources_choice')
|
|
def _compute_staff_user_ids(self):
|
|
for invite in self:
|
|
if invite.resources_choice == "current_user" and \
|
|
self.env.user.id in invite.appointment_type_ids.staff_user_ids.ids:
|
|
invite.staff_user_ids = self.env.user
|
|
else:
|
|
invite.staff_user_ids = False
|
|
|
|
@api.depends('suggested_resource_ids')
|
|
def _compute_suggested_resource_count(self):
|
|
for invite in self:
|
|
invite.suggested_resource_count = len(invite.suggested_resource_ids)
|
|
|
|
@api.depends('suggested_staff_user_ids')
|
|
def _compute_suggested_staff_user_count(self):
|
|
for invite in self:
|
|
invite.suggested_staff_user_count = len(invite.suggested_staff_user_ids)
|
|
|
|
@api.depends('base_book_url', 'short_code')
|
|
def _compute_book_url(self):
|
|
"""
|
|
Compute a short link linked to an appointment invitation.
|
|
"""
|
|
for invite in self:
|
|
invite.book_url = url_join(invite.base_book_url, invite.short_code) if invite.short_code else False
|
|
|
|
@api.depends('appointment_type_ids', 'staff_user_ids', 'resource_ids')
|
|
def _compute_redirect_url(self):
|
|
"""
|
|
Compute a link that will be share for the user depending on the appointment types and users
|
|
selected. We allow to preselect a group of them if there is only one appointment type selected.
|
|
Indeed, it would be too complex to manage ones with multiple appointment types.
|
|
Three possible params can be generated with the link:
|
|
- filter_resource_ids: which allows the user to select a resource between the ones selected
|
|
- filter_staff_user_ids: which allows the user to select an user between the ones selected
|
|
- filter_appointment_type_ids: which display a selection of appointment types to user from which
|
|
they can choose
|
|
"""
|
|
for invite in self:
|
|
if len(invite.appointment_type_ids) == 1:
|
|
base_redirect_url = url_join("/appointment/", str(invite.appointment_type_ids.id))
|
|
else:
|
|
base_redirect_url = '/appointment'
|
|
|
|
invite.redirect_url = '%s?%s' % (
|
|
base_redirect_url,
|
|
url_encode(invite._get_redirect_url_parameters()),
|
|
)
|
|
|
|
@api.model
|
|
def _get_invitation_url_parameters(self):
|
|
""" Returns invitation-related url parameters we want to keep between the different steps of booking """
|
|
return {'filter_appointment_type_ids', 'filter_resource_ids', 'filter_staff_user_ids', 'invite_token'}
|
|
|
|
def _get_meeting_categories_for_appointment(self):
|
|
""" Get the categories that will be linked to the meetings created from the invite
|
|
:return <calendar.event.type> recordset:
|
|
"""
|
|
return self.env.ref('appointment.calendar_event_type_data_online_appointment', raise_if_not_found=False)
|
|
|
|
def _get_redirect_url_parameters(self):
|
|
self.ensure_one()
|
|
url_param = {
|
|
'invite_token': self.access_token,
|
|
}
|
|
if self.appointment_type_ids:
|
|
url_param.update({
|
|
'filter_appointment_type_ids': str(self.appointment_type_ids.ids),
|
|
})
|
|
if self.staff_user_ids:
|
|
url_param.update({
|
|
'filter_staff_user_ids': str(self.staff_user_ids.ids)
|
|
})
|
|
elif self.resource_ids:
|
|
url_param.update({
|
|
'filter_resource_ids': str(self.resource_ids.ids)
|
|
})
|
|
return url_param
|
|
|
|
def _check_appointments_params(self, appointment_types, users, resources):
|
|
"""
|
|
Check if the param receive through the URL match with the appointment invite info
|
|
:param recordset appointment_types: the appointment types representing the filter_appointment_type_ids
|
|
:param recordset users: the staff users representing the filter_staff_user_ids
|
|
:param recordset resources: the resources representing the filter_resource_ids
|
|
"""
|
|
self.ensure_one()
|
|
if (self.appointment_type_ids and self.appointment_type_ids != appointment_types) or self.staff_user_ids != users or self.resource_ids != resources:
|
|
return False
|
|
return True
|
|
|
|
def _get_unique_short_code(self, short_code=False):
|
|
short_access_token = self.access_token[:8] if self.access_token else uuid.uuid4().hex[:8]
|
|
short_code = short_code or self.short_code or short_access_token
|
|
nb_short_code = self.env['appointment.invite'].search_count([('id', '!=', self._origin.id), ('short_code', '=', short_code)])
|
|
if bool(nb_short_code):
|
|
short_code = "%s_%s" % (short_code, nb_short_code)
|
|
return short_code
|
|
|
|
@api.autovacuum
|
|
def _gc_appointment_invite(self):
|
|
limit_dt = fields.Datetime.subtract(fields.Datetime.now(), months=6)
|
|
|
|
invites = self.env['appointment.invite'].search([('create_date', '<=', limit_dt)])
|
|
|
|
to_remove = invites.filtered(lambda invite: not invite.calendar_event_ids or max([event.stop for event in invite.calendar_event_ids]) < limit_dt)
|
|
to_remove.unlink()
|