Skip to content

Commit 2948af8

Browse files
committed
[IMP] appointment_capacity: support user capacity customization
implement to single ticket per slot and multiple booking per slot drop down selection and availability based on users and resources we can ask for fronted side integer number to how many times particular slot is booking when multiple booking selection also we have to add multiple seat per slot when user want to multiple seat at a time.
1 parent 4c650f3 commit 2948af8

14 files changed

+1007
-0
lines changed

appointment_capacity/__init__.py

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

appointment_capacity/__manifest__.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
'name': "Appointment Capacity",
3+
'summary': "change the capacity of the appointment based on the user and resource",
4+
'description':"capacity of the appointment is change base on the type of the appointment",
5+
'author':"sujal asodariya",
6+
'version':"1.0",
7+
'depends':[
8+
"appointment",
9+
"website",
10+
"calendar",
11+
"account_auto_transfer",
12+
],
13+
'data':[
14+
'views/appointment_template_views.xml',
15+
'views/appointment_type_views.xml',
16+
],
17+
'installable':True,
18+
'application':True,
19+
'license':"LGPL-3",
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import appointment_booking
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import json
2+
import pytz
3+
import re
4+
from dateutil.relativedelta import relativedelta
5+
from urllib.parse import unquote_plus
6+
from werkzeug.exceptions import NotFound
7+
from odoo import fields
8+
from odoo.http import request
9+
from odoo import http
10+
from odoo.addons.base.models.ir_qweb import keep_query
11+
from odoo.addons.phone_validation.tools import phone_validation
12+
from odoo.addons.appointment.controllers.appointment import AppointmentController
13+
14+
class AppointmentCapacityOverride(AppointmentController):
15+
16+
def _prepare_appointment_type_page_values(self, appointment_type, staff_user_id=False, resource_selected_id=False, **kwargs):
17+
res = super()._prepare_appointment_type_page_values(appointment_type, staff_user_id, resource_selected_id, **kwargs)
18+
19+
filter_resource_ids = json.loads(kwargs.get('filter_resource_ids') or '[]')
20+
resources_possible = self._get_possible_resources(appointment_type, filter_resource_ids)
21+
resource_default = resource_selected = request.env['appointment.resource']
22+
staff_user_id = int(staff_user_id) if staff_user_id else False
23+
resource_selected_id = int(resource_selected_id) if resource_selected_id else False
24+
manage_capacity_type = appointment_type.resource_capacity
25+
26+
max_capacity_possible = 0
27+
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+
34+
# my code changes
35+
manage_capacity_type = appointment_type.resource_capacity
36+
if manage_capacity_type == 'one_booking' or manage_capacity_type == 'multi_booking':
37+
max_capacity_possible = 1
38+
elif manage_capacity_type == 'multi_seat':
39+
if appointment_type.schedule_based_on == 'users':
40+
max_capacity_possible = appointment_type.capacity_count or 1 # Default to 1 if not set
41+
else:
42+
possible_combinations = (resource_selected or resource_default or resources_possible)._get_filtered_possible_capacity_combinations(1, {})
43+
max_capacity_possible = possible_combinations[-1][1] if possible_combinations else 1
44+
res['max_capacity'] = min(12, max_capacity_possible)
45+
return res
46+
47+
@http.route(['/appointment/<int:appointment_type_id>/submit'],
48+
type='http', auth="public", website=True, methods=["POST"])
49+
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,
50+
guest_emails_str=None, **kwargs):
51+
"""
52+
Create the event for the appointment and redirect on the validation page with a summary of the appointment.
53+
54+
:param appointment_type_id: the appointment type id related
55+
:param datetime_str: the string representing the datetime
56+
:param duration_str: the string representing the duration
57+
:param name: the name of the user sets in the form
58+
:param phone: the phone of the user sets in the form
59+
:param email: the email of the user sets in the form
60+
:param staff_user_id: the user selected for the appointment
61+
:param available_resource_ids: the resources ids available for the appointment
62+
:param asked_capacity: asked capacity for the appointment
63+
:param str guest_emails: optional line-separated guest emails. It will
64+
fetch or create partners to add them as event attendees;
65+
"""
66+
67+
domain = self._appointments_base_domain(
68+
filter_appointment_type_ids=kwargs.get('filter_appointment_type_ids'),
69+
search=kwargs.get('search'),
70+
invite_token=kwargs.get('invite_token')
71+
)
72+
available_appointments = self._fetch_and_check_private_appointment_types(
73+
kwargs.get('filter_appointment_type_ids'),
74+
kwargs.get('filter_staff_user_ids'),
75+
kwargs.get('filter_resource_ids'),
76+
kwargs.get('invite_token'),
77+
domain=domain,
78+
)
79+
appointment_type = available_appointments.filtered(lambda appt: appt.id == int(appointment_type_id))
80+
81+
if not appointment_type:
82+
raise NotFound()
83+
timezone = request.session.get('timezone') or appointment_type.appointment_tz
84+
tz_session = pytz.timezone(timezone)
85+
datetime_str = unquote_plus(datetime_str)
86+
date_start = tz_session.localize(fields.Datetime.from_string(datetime_str)).astimezone(pytz.utc).replace(tzinfo=None)
87+
duration = float(duration_str)
88+
date_end = date_start + relativedelta(hours=duration)
89+
invite_token = kwargs.get('invite_token')
90+
91+
staff_user = request.env['res.users']
92+
resources = request.env['appointment.resource']
93+
resource_ids = None
94+
asked_capacity = int(asked_capacity)
95+
resources_remaining_capacity = None
96+
users_remaining_capacity = None
97+
if appointment_type.schedule_based_on == 'resources':
98+
resource_ids = json.loads(unquote_plus(available_resource_ids))
99+
# Check if there is still enough capacity (in case someone else booked with a resource in the meantime)
100+
resources = request.env['appointment.resource'].sudo().browse(resource_ids).exists()
101+
if any(resource not in appointment_type.resource_ids for resource in resources):
102+
raise NotFound()
103+
resources_remaining_capacity = appointment_type._get_resources_remaining_capacity(resources, date_start, date_end, with_linked_resources=False)
104+
if resources_remaining_capacity['total_remaining_capacity'] < asked_capacity:
105+
return request.redirect('/appointment/%s?%s' % (appointment_type.id, keep_query('*', state='failed-resource')))
106+
# change for the users selection
107+
else:
108+
# check availability of the selected user again (in case someone else booked while the client was entering the form)
109+
staff_user = request.env['res.users'].sudo().search([('id', '=', int(staff_user_id))])
110+
if staff_user not in appointment_type.staff_user_ids:
111+
raise NotFound()
112+
users_remaining_capacity = appointment_type._get_users_remaining_capacity(staff_user, date_start, date_end)
113+
if staff_user and not staff_user.partner_id.calendar_verify_availability(date_start, date_end) or (appointment_type.resource_capacity != 'one_booking' and users_remaining_capacity['total_remaining_capacity'] < asked_capacity):
114+
return request.redirect('/appointment/%s?%s' % (appointment_type.id, keep_query('*', state='failed-staff-user')))
115+
116+
guests = None
117+
if appointment_type.allow_guests:
118+
if guest_emails_str:
119+
guests = request.env['calendar.event'].sudo()._find_or_create_partners(guest_emails_str)
120+
121+
customer = self._get_customer_partner()
122+
123+
# email is mandatory
124+
new_customer = not customer.email
125+
if not new_customer and customer.email != email and customer.email_normalized != email_normalize(email):
126+
new_customer = True
127+
if not new_customer:
128+
# phone is mandatory
129+
if not customer.phone:
130+
customer.phone = customer._phone_format(number=phone) or phone
131+
else:
132+
customer_phone_fmt = customer._phone_format(fname="phone")
133+
input_country = self._get_customer_country()
134+
input_phone_fmt = phone_validation.phone_format(phone, input_country.code, input_country.phone_code, force_format="E164", raise_exception=False)
135+
new_customer = customer.phone != phone and customer_phone_fmt != input_phone_fmt
136+
137+
if new_customer:
138+
customer = customer.sudo().create({
139+
'name': name,
140+
'phone': customer._phone_format(number=phone, country=self._get_customer_country()) or phone,
141+
'email': email,
142+
'lang': request.lang.code,
143+
})
144+
145+
# partner_inputs dictionary structures all answer inputs received on the appointment submission: key is question id, value
146+
# is answer id (as string) for choice questions, text input for text questions, array of ids for multiple choice questions.
147+
partner_inputs = {}
148+
appointment_question_ids = appointment_type.question_ids.ids
149+
for k_key, k_value in [item for item in kwargs.items() if item[1]]:
150+
question_id_str = re.match(r"\bquestion_([0-9]+)\b", k_key)
151+
if question_id_str and int(question_id_str.group(1)) in appointment_question_ids:
152+
partner_inputs[int(question_id_str.group(1))] = k_value
153+
continue
154+
checkbox_ids_str = re.match(r"\bquestion_([0-9]+)_answer_([0-9]+)\b", k_key)
155+
if checkbox_ids_str:
156+
question_id, answer_id = [int(checkbox_ids_str.group(1)), int(checkbox_ids_str.group(2))]
157+
if question_id in appointment_question_ids:
158+
partner_inputs[question_id] = partner_inputs.get(question_id, []) + [answer_id]
159+
160+
# The answer inputs will be created in _prepare_calendar_event_values from the values in answer_input_values
161+
answer_input_values = []
162+
base_answer_input_vals = {
163+
'appointment_type_id': appointment_type.id,
164+
'partner_id': customer.id,
165+
}
166+
167+
for question in appointment_type.question_ids.filtered(lambda question: question.id in partner_inputs.keys()):
168+
if question.question_type == 'checkbox':
169+
answers = question.answer_ids.filtered(lambda answer: answer.id in partner_inputs[question.id])
170+
answer_input_values.extend([
171+
dict(base_answer_input_vals, question_id=question.id, value_answer_id=answer.id) for answer in answers
172+
])
173+
elif question.question_type in ['select', 'radio']:
174+
answer_input_values.append(
175+
dict(base_answer_input_vals, question_id=question.id, value_answer_id=int(partner_inputs[question.id]))
176+
)
177+
elif question.question_type in ['char', 'text']:
178+
answer_input_values.append(
179+
dict(base_answer_input_vals, question_id=question.id, value_text_box=partner_inputs[question.id].strip())
180+
)
181+
182+
booking_line_values = []
183+
resource_capacity = appointment_type.resource_capacity
184+
if appointment_type.schedule_based_on == 'resources':
185+
capacity_to_assign = asked_capacity
186+
for resource in resources:
187+
resource_remaining_capacity = resources_remaining_capacity.get(resource)
188+
new_capacity_reserved = min(resource_remaining_capacity, capacity_to_assign, resource.capacity)
189+
capacity_to_assign -= new_capacity_reserved
190+
booking_line_values.append({
191+
'appointment_resource_id': resource.id,
192+
'capacity_reserved': new_capacity_reserved,
193+
'capacity_used': new_capacity_reserved if resource_capacity != 'one_booking' and resource.shareable else resource.capacity
194+
})
195+
# changes for the users selection
196+
else:
197+
user_remaining_capacity = users_remaining_capacity['total_remaining_capacity']
198+
new_capacity_reserved = min(user_remaining_capacity, asked_capacity, appointment_type.capacity_count)
199+
booking_line_values.append({
200+
'appointment_user_id': staff_user.id,
201+
'capacity_reserved': new_capacity_reserved,
202+
'capacity_used': new_capacity_reserved,
203+
})
204+
205+
if invite_token:
206+
appointment_invite = request.env['appointment.invite'].sudo().search([('access_token', '=', invite_token)])
207+
else:
208+
appointment_invite = request.env['appointment.invite']
209+
210+
return self._handle_appointment_form_submission(
211+
appointment_type, date_start, date_end, duration, answer_input_values, name,
212+
customer, appointment_invite, guests, staff_user, asked_capacity, booking_line_values
213+
)
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from . import appointment_type
2+
from . import appointment_booking_line
3+
from . import calender_event
4+
from . import res_partner
5+
from . import calender_booking_line
6+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from odoo import api, models,fields
2+
3+
class AppointmentBookingLine(models.Model):
4+
_inherit = "appointment.booking.line"
5+
6+
appointment_user_id = fields.Many2one('res.users', string="Appointment User", ondelete="cascade")
7+
appointment_resource_id = fields.Many2one('appointment.resource', string="Appointment Resource", ondelete="cascade",required=False)
8+
9+
@api.depends('appointment_resource_id.capacity', 'appointment_resource_id.shareable',
10+
'appointment_type_id.resource_capacity', 'capacity_reserved','appointment_type_id.capacity_count')
11+
def _compute_capacity_used(self):
12+
self.capacity_used = 0
13+
for line in self:
14+
if line.capacity_reserved == 0:
15+
line.capacity_used = 0
16+
elif line.appointment_type_id.resource_capacity == 'multi_booking':
17+
line.capacity_used = 1
18+
elif line.appointment_type_id.schedule_based_on == 'resources' and (not line.appointment_resource_id.shareable or line.appointment_type_id.resource_capacity == 'one_booking'):
19+
line.capacity_used = line.appointment_resource_id.capacity
20+
else:
21+
line.capacity_used = line.capacity_reserved

0 commit comments

Comments
 (0)