Skip to content

Commit 478a9bd

Browse files
committed
[ADD] appointment_update: add appointment support capacity
In this commit: - added track_capacity selection field for users and resources; previously, only manage_resource_capacity boolean field was available. - overridden several functions to accommodate the new capacity selection field.
1 parent df4b6e6 commit 478a9bd

14 files changed

+325
-64
lines changed

appointment_update/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import controllers
2+
from . import models

appointment_update/__manifest__.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
'name': 'Appointment update',
3+
'version': '1.0',
4+
'description': """
5+
Allow clients to Schedule Appointments through the Portal and book multiple appointment
6+
""",
7+
'depends': ['appointment', 'appointment_account_payment', 'website_appointment'],
8+
'data': [
9+
'views/appointment_update_type_views.xml',
10+
],
11+
'installable': True,
12+
'license': 'OEEL-1',
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import appointment_update
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import json
2+
import pytz
3+
import re
4+
5+
from dateutil.relativedelta import relativedelta
6+
from urllib.parse import unquote_plus
7+
from werkzeug.exceptions import NotFound
8+
9+
from odoo import fields
10+
from odoo.http import request
11+
from odoo.tools import email_normalize
12+
from odoo.addons.base.models.ir_qweb import keep_query
13+
from odoo.addons.phone_validation.tools import phone_validation
14+
from odoo.addons.appointment.controllers.appointment import AppointmentController
15+
16+
class AppointmentUpdateController(AppointmentController):
17+
18+
def _prepare_appointment_type_page_values(self, appointment_type, staff_user_id=False, resource_selected_id=False, **kwargs):
19+
result = super()._prepare_appointment_type_page_values(appointment_type, staff_user_id, resource_selected_id, **kwargs)
20+
21+
filter_resource_ids = json.loads(kwargs.get('filter_resource_ids') or '[]')
22+
resources_possible = self._get_possible_resources(appointment_type, filter_resource_ids)
23+
resource_default = resource_selected = request.env['appointment.resource']
24+
staff_user_id = int(staff_user_id) if staff_user_id else False
25+
resource_selected_id = int(resource_selected_id) if resource_selected_id else False
26+
track_capacity = appointment_type.track_capacity
27+
max_capacity_possible = 0
28+
if resources_possible:
29+
if resource_selected_id and resource_selected_id in resources_possible.ids and appointment_type.assign_method != 'time_resource':
30+
resource_selected = request.env['appointment.resource'].sudo().browse(resource_selected_id)
31+
elif appointment_type.assign_method == 'resource_time':
32+
resource_default = resources_possible[0]
33+
# my code changes
34+
if track_capacity == 'one_booking_per_slot' or track_capacity == 'multiple_booking_per_slot':
35+
max_capacity_possible = 1
36+
elif track_capacity == 'multiple_seat_per_slot':
37+
if appointment_type.schedule_based_on == 'users':
38+
max_capacity_possible = appointment_type.total_booking
39+
else:
40+
possible_combinations = (resource_selected or resource_default or resources_possible)._get_filtered_possible_capacity_combinations(1, {})
41+
max_capacity_possible = possible_combinations[-1][1] if possible_combinations else 1
42+
result['max_capacity'] = min(12, max_capacity_possible)
43+
return result
44+
45+
def appointment_form_submit(self, appointment_type_id, datetime_str, duration_str, name, phone, email, staff_user_id=None, available_resource_ids=None, asked_capacity=1,
46+
guest_emails_str=None, **kwargs):
47+
"""
48+
Create the event for the appointment and redirect on the validation page with a summary of the appointment.
49+
50+
:param appointment_type_id: the appointment type id related
51+
:param datetime_str: the string representing the datetime
52+
:param duration_str: the string representing the duration
53+
:param name: the name of the user sets in the form
54+
:param phone: the phone of the user sets in the form
55+
:param email: the email of the user sets in the form
56+
:param staff_user_id: the user selected for the appointment
57+
:param available_resource_ids: the resources ids available for the appointment
58+
:param asked_capacity: asked capacity for the appointment
59+
:param str guest_emails: optional line-separated guest emails. It will
60+
fetch or create partners to add them as event attendees;
61+
"""
62+
# res = super().appointment_form_submit(appointment_type_id, datetime_str, duration_str, name, phone, email, staff_user_id, available_resource_ids, asked_capacity, guest_emails_str, **kwargs)
63+
64+
domain = self._appointments_base_domain(
65+
filter_appointment_type_ids=kwargs.get('filter_appointment_type_ids'),
66+
search=kwargs.get('search'),
67+
invite_token=kwargs.get('invite_token')
68+
)
69+
70+
available_appointments = self._fetch_and_check_private_appointment_types(
71+
kwargs.get('filter_appointment_type_ids'),
72+
kwargs.get('filter_staff_user_ids'),
73+
kwargs.get('filter_resource_ids'),
74+
kwargs.get('invite_token'),
75+
domain=domain,
76+
)
77+
appointment_type = available_appointments.filtered(lambda appt: appt.id == int(appointment_type_id))
78+
79+
if not appointment_type:
80+
raise NotFound()
81+
timezone = request.session.get('timezone') or appointment_type.appointment_tz
82+
tz_session = pytz.timezone(timezone)
83+
datetime_str = unquote_plus(datetime_str)
84+
date_start = tz_session.localize(fields.Datetime.from_string(datetime_str)).astimezone(pytz.utc).replace(tzinfo=None)
85+
duration = float(duration_str)
86+
date_end = date_start + relativedelta(hours=duration)
87+
invite_token = kwargs.get('invite_token')
88+
89+
staff_user = request.env['res.users']
90+
resources = request.env['appointment.resource']
91+
resource_ids = None
92+
asked_capacity = int(asked_capacity)
93+
resources_remaining_capacity = None
94+
if appointment_type.schedule_based_on == 'resources':
95+
resource_ids = json.loads(unquote_plus(available_resource_ids))
96+
# Check if there is still enough capacity (in case someone else booked with a resource in the meantime)
97+
resources = request.env['appointment.resource'].sudo().browse(resource_ids).exists()
98+
if any(resource not in appointment_type.resource_ids for resource in resources):
99+
raise NotFound()
100+
resources_remaining_capacity = appointment_type._get_resources_remaining_capacity(resources, date_start, date_end, with_linked_resources=False)
101+
if resources_remaining_capacity['total_remaining_capacity'] < asked_capacity:
102+
return request.redirect('/appointment/%s?%s' % (appointment_type.id, keep_query('*', state='failed-resource')))
103+
104+
guests = None
105+
if appointment_type.allow_guests:
106+
if guest_emails_str:
107+
guests = request.env['calendar.event'].sudo()._find_or_create_partners(guest_emails_str)
108+
109+
customer = self._get_customer_partner()
110+
111+
# email is mandatory
112+
new_customer = not customer.email
113+
if not new_customer and customer.email != email and customer.email_normalized != email_normalize(email):
114+
new_customer = True
115+
if not new_customer:
116+
# phone is mandatory
117+
if not customer.phone:
118+
customer.phone = customer._phone_format(number=phone) or phone
119+
else:
120+
customer_phone_fmt = customer._phone_format(fname="phone")
121+
input_country = self._get_customer_country()
122+
input_phone_fmt = phone_validation.phone_format(phone, input_country.code, input_country.phone_code, force_format="E164", raise_exception=False)
123+
new_customer = customer.phone != phone and customer_phone_fmt != input_phone_fmt
124+
if new_customer:
125+
customer = customer.sudo().create({
126+
'name': name,
127+
'phone': customer._phone_format(number=phone, country=self._get_customer_country()) or phone,
128+
'email': email,
129+
'lang': request.lang.code,
130+
})
131+
# partner_inputs dictionary structures all answer inputs received on the appointment submission: key is question id, value
132+
# is answer id (as string) for choice questions, text input for text questions, array of ids for multiple choice questions.
133+
partner_inputs = {}
134+
appointment_question_ids = appointment_type.question_ids.ids
135+
for k_key, k_value in [item for item in kwargs.items() if item[1]]:
136+
question_id_str = re.match(r"\bquestion_([0-9]+)\b", k_key)
137+
if question_id_str and int(question_id_str.group(1)) in appointment_question_ids:
138+
partner_inputs[int(question_id_str.group(1))] = k_value
139+
continue
140+
checkbox_ids_str = re.match(r"\bquestion_([0-9]+)_answer_([0-9]+)\b", k_key)
141+
if checkbox_ids_str:
142+
question_id, answer_id = [int(checkbox_ids_str.group(1)), int(checkbox_ids_str.group(2))]
143+
if question_id in appointment_question_ids:
144+
partner_inputs[question_id] = partner_inputs.get(question_id, []) + [answer_id]
145+
146+
# The answer inputs will be created in _prepare_calendar_event_values from the values in answer_input_values
147+
answer_input_values = []
148+
base_answer_input_vals = {
149+
'appointment_type_id': appointment_type.id,
150+
'partner_id': customer.id,
151+
}
152+
for question in appointment_type.question_ids.filtered(lambda question: question.id in partner_inputs.keys()):
153+
if question.question_type == 'checkbox':
154+
answers = question.answer_ids.filtered(lambda answer: answer.id in partner_inputs[question.id])
155+
answer_input_values.extend([dict(base_answer_input_vals, question_id=question.id, value_answer_id=answer.id) for answer in answers])
156+
elif question.question_type in ['select', 'radio']:
157+
answer_input_values.append(dict(base_answer_input_vals, question_id=question.id, value_answer_id=int(partner_inputs[question.id])))
158+
elif question.question_type in ['char', 'text']:
159+
answer_input_values.append(dict(base_answer_input_vals, question_id=question.id, value_text_box=partner_inputs[question.id].strip()))
160+
booking_line_values = []
161+
track_capacity = appointment_type.track_capacity
162+
if appointment_type.schedule_based_on == 'resources':
163+
capacity_to_assign = asked_capacity
164+
for resource in resources:
165+
resource_remaining_capacity = resources_remaining_capacity.get(resource)
166+
new_capacity_reserved = min(resource_remaining_capacity, capacity_to_assign, resource.capacity)
167+
capacity_to_assign -= new_capacity_reserved
168+
booking_line_values.append({
169+
'appointment_resource_id': resource.id,
170+
'capacity_reserved': new_capacity_reserved,
171+
# my code changes
172+
'capacity_used': new_capacity_reserved if track_capacity != 'one_booking_per_slot' and resource.shareable else resource.capacity
173+
})
174+
if invite_token:
175+
appointment_invite = request.env['appointment.invite'].sudo().search([('access_token', '=', invite_token)])
176+
else:
177+
appointment_invite = request.env['appointment.invite']
178+
179+
return self._handle_appointment_form_submission(
180+
appointment_type, date_start, date_end, duration, answer_input_values, name,
181+
customer, appointment_invite, guests, staff_user, asked_capacity, booking_line_values)

appointment_update/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import appointment_update_type
2+
from . import appointment_update_booking_line
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from odoo import api, models
2+
3+
class AppointmentBookingLine(models.Model):
4+
_inherit = "appointment.booking.line"
5+
6+
@api.depends('appointment_resource_id.capacity', 'appointment_resource_id.shareable','appointment_type_id.track_capacity', 'capacity_reserved', 'appointment_type_id.total_booking')
7+
def _compute_capacity_used(self):
8+
self.capacity_used = 0
9+
for line in self:
10+
if line.capacity_reserved == 0:
11+
line.capacity_used = 0
12+
elif line.appointment_type_id.track_capacity == 'multiple_booking_per_slot':
13+
line.capacity_used = 1
14+
elif line.appointment_type_id.schedule_based_on == 'resources' and (not line.appointment_resource_id.shareable or line.appointment_type_id.track_capacity == 'one_booking_per_slot'):
15+
line.capacity_used = line.appointment_resource_id.capacity
16+
else:
17+
line.capacity_used = line.capacity_reserved
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from odoo import api, fields, models
2+
3+
class AppointmentType(models.Model):
4+
_inherit = "appointment.type"
5+
6+
track_capacity = fields.Selection(
7+
[
8+
("one_booking_per_slot", "One Booking Per Slot"),
9+
("multiple_booking_per_slot", "Multiple Booking Per Slot"),
10+
("multiple_seat_per_slot", "Multiple Seat Per Slot"),
11+
],
12+
string="Capacities",
13+
store=True,
14+
default="one_booking_per_slot",
15+
compute="_compute_resource_manage_capacity",
16+
required=True,
17+
readonly=False,
18+
help="""Manage the maximum number of bookings that seats and resources can handle. "One booking per slot" means each user can book a slot exclusively,
19+
while "multiple bookings per slot" allows multiple bookings based on user input.
20+
"Multiple seat per slot" indicates that bookings can be made according to the user's capacity.""",
21+
)
22+
total_booking = fields.Integer("Total Booking", default=1)
23+
24+
_sql_constraints = [
25+
("check_total_booking", "check(total_booking >= 1)", "total booking/seats should be at least 1."),
26+
]
27+
28+
@api.onchange("track_capacity")
29+
def _onchange_track_capacity(self):
30+
if (self.track_capacity == "one_booking_per_slot"):
31+
self.resource_manage_capacity = False
32+
else:
33+
self.resource_manage_capacity = True
34+
35+
def _slot_availability_is_resource_available(self, slot, resource, availability_values):
36+
result = super()._slot_availability_is_resource_available(slot,resource, availability_values)
37+
38+
slot_start_dt_utc, slot_end_dt_utc = slot['UTC'][0], slot['UTC'][1]
39+
resource_to_bookings = availability_values.get('resource_to_bookings')
40+
41+
if resource_to_bookings.get(resource):
42+
if resource_to_bookings[resource].filtered(lambda bl: bl.event_start < slot_end_dt_utc and bl.event_stop > slot_start_dt_utc):
43+
if self.track_capacity == "multiple_booking_per_slot":
44+
result = True
45+
elif self.track_capacity == "multiple_seat_per_slot":
46+
result = resource.shareable
47+
return result
48+
49+
def _slot_availability_select_best_resources(self, capacity_info, asked_capacity):
50+
result = super()._slot_availability_select_best_resources(capacity_info, asked_capacity)
51+
available_resources = self.env['appointment.resource'].concat(*capacity_info.keys()).sorted('sequence')
52+
53+
if self.track_capacity == "one_booking_per_slot":
54+
return available_resources[0] if self.assign_method != "time_resource" else available_resources
55+
56+
return result
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<record id="appointment_update_type_view_form" model="ir.ui.view">
4+
<field name="name">appointment.type.view.form.inherit.appointment.update.type.view</field>
5+
<field name="model">appointment.type</field>
6+
<field name="inherit_id" ref="appointment.appointment_type_view_form"/>
7+
<field name="arch" type="xml">
8+
<field name="assign_method" position="after">
9+
<label for="track_capacity" />
10+
<div>
11+
<field name="track_capacity" class="oe_inline"/>
12+
<!-- Availability based on 'user' -->
13+
<span invisible="track_capacity == 'one_booking_per_slot' or schedule_based_on == 'resources'">
14+
Total: <field name="total_booking" class="o_field_integer oe_inline o_input_5ch me-1 form-control form-control-sm w-auto border border-primary rounded"/>
15+
<span class="badge bg-info" invisible="track_capacity == 'multiple_booking_per_slot'">seats</span>
16+
<span class="badge bg-info" invisible="track_capacity == 'multiple_seat_per_slot'">bookings</span>
17+
</span>
18+
<!-- Availability based on 'resource' -->
19+
<span invisible="track_capacity == 'one_booking_per_slot' or schedule_based_on == 'users'">
20+
Total: <field name="resource_total_capacity" class="oe_inline o_input_5ch me-1 form-control form-control-sm w-auto border border-primary rounded"/>
21+
<span class="badge bg-secondary" invisible="track_capacity == 'multiple_booking_per_slot'">seats</span>
22+
<span class="badge bg-secondary" invisible="track_capacity == 'multiple_seat_per_slot'">bookings</span>
23+
<a type="object" name="action_appointment_resources" class="btn btn-sm btn-link d-inline-flex align-items-center px-2 py-1 ms-2" role="button">
24+
<i class="oi oi-fw o_button_icon oi-arrow-right me-1"></i> Manage Resources
25+
</a>
26+
</span>
27+
</div>
28+
</field>
29+
<xpath expr="//field[@name='resource_manage_capacity']/.." position="attributes">
30+
<attribute name="invisible">1</attribute>
31+
</xpath>
32+
</field>
33+
</record>
34+
<record id="appointment_update_type_view_form_1" model="ir.ui.view">
35+
<field name="name">appointment.type.view.form.inherit.appointment.update.type.view</field>
36+
<field name="model">appointment.type</field>
37+
<field name="inherit_id" ref="appointment_account_payment.appointment_type_view_form"/>
38+
<field name="arch" type="xml">
39+
<xpath expr="//span[@class='ms-1'][1]" position="replace">
40+
<span invisible="track_capacity == 'multiple_seat_per_slot'" class="ms-1">per booking</span>
41+
</xpath>
42+
<xpath expr="//span[@class='ms-1'][2]" position="replace">
43+
<span invisible="track_capacity != 'multiple_seat_per_slot'" class="ms-1">per seat</span>
44+
</xpath>
45+
</field>
46+
</record>
47+
48+
<template id="appointment_update_appointment_templates_appointments" inherit_id="appointment.appointment_info">
49+
<xpath expr="//div[@t-if='not based_on_users and appointment_type.resource_manage_capacity']" position="attributes">
50+
<attribute name="t-if">appointment_type.track_capacity != 'one_booking_per_slot' and appointment_type.track_capacity != 'multiple_booking_per_slot'</attribute>
51+
</xpath>
52+
</template>
53+
</odoo>

pos_update/__init__.py

-1
This file was deleted.

pos_update/__manifest__.py

-16
This file was deleted.

pos_update/models/__init__.py

-1
This file was deleted.

pos_update/models/pos_update_product.py

-11
This file was deleted.

0 commit comments

Comments
 (0)