Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] appointment_capacity: added capacity management for users and r… #663

Draft
wants to merge 5 commits into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions appointment_capacity/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
18 changes: 18 additions & 0 deletions appointment_capacity/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
'name': 'Appointment Capacity Management',
'version': '1.0',
'summary': 'Enhance appointment booking with multiple bookings and seats per slot.',
'description': """
- Allows multiple bookings per time slot.
- Supports multiple seats per slot.
""",
'author': 'Darshan Patel',
'depends': ['appointment','appointment_account_payment', 'calendar'],
'data': [
'views/appointment_type_views.xml',
'views/appointment_template_appointment.xml',
'views/calendar_event_views.xml',
],
'installable': True,
'license': 'LGPL-3',
}
1 change: 1 addition & 0 deletions appointment_capacity/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import appointment
201 changes: 201 additions & 0 deletions appointment_capacity/controllers/appointment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import json
import pytz
import re

from dateutil.relativedelta import relativedelta
from urllib.parse import unquote_plus
from werkzeug.exceptions import NotFound

from odoo import http, fields
from odoo.http import request
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as email_normalize
from odoo.addons.appointment.controllers.appointment import AppointmentController
from odoo.addons.base.models.ir_qweb import keep_query
from odoo.addons.phone_validation.tools import phone_validation


class AppointmentCapacityController(AppointmentController):


def _prepare_appointment_type_page_values(self, appointment_type, staff_user_id=False, resource_selected_id=False, skip_resource_selection=False, **kwargs):
"""
Overrides `_prepare_appointment_type_page_values` to include capacity management based on `capacity_type`.
"""
values = super()._prepare_appointment_type_page_values(appointment_type, staff_user_id, resource_selected_id, **kwargs)

if appointment_type.capacity_type == 'multiple_seats':
if appointment_type.schedule_based_on == 'users':
max_capacity_possible = appointment_type.user_capacity_count
else:
possible_combinations = (values['resource_selected'] or values['resource_default'] or values['resources_possible'])._get_filtered_possible_capacity_combinations(1, {})
max_capacity_possible = possible_combinations[-1][1] if possible_combinations else 1
else:
max_capacity_possible = 1

values['max_capacity'] = min(12, max_capacity_possible)
return values


@http.route()
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,
guest_emails_str=None, **kwargs):
"""
Create the event for the appointment and redirect on the validation page with a summary of the appointment.

:param appointment_type_id: the appointment type id related
:param datetime_str: the string representing the datetime
:param duration_str: the string representing the duration
:param name: the name of the user sets in the form
:param phone: the phone of the user sets in the form
:param email: the email of the user sets in the form
:param staff_user_id: the user selected for the appointment
:param available_resource_ids: the resources ids available for the appointment
:param asked_capacity: asked capacity for the appointment
:param str guest_emails: optional line-separated guest emails. It will
fetch or create partners to add them as event attendees;
"""
domain = self._appointments_base_domain(
filter_appointment_type_ids=kwargs.get('filter_appointment_type_ids'),
search=kwargs.get('search'),
invite_token=kwargs.get('invite_token')
)

available_appointments = self._fetch_and_check_private_appointment_types(
kwargs.get('filter_appointment_type_ids'),
kwargs.get('filter_staff_user_ids'),
kwargs.get('filter_resource_ids'),
kwargs.get('invite_token'),
domain=domain,
)
appointment_type = available_appointments.filtered(lambda appt: appt.id == int(appointment_type_id))

if not appointment_type:
raise NotFound()
timezone = request.session.get('timezone') or appointment_type.appointment_tz
tz_session = pytz.timezone(timezone)
datetime_str = unquote_plus(datetime_str)
date_start = tz_session.localize(fields.Datetime.from_string(datetime_str)).astimezone(pytz.utc).replace(tzinfo=None)
duration = float(duration_str)
date_end = date_start + relativedelta(hours=duration)
invite_token = kwargs.get('invite_token')

staff_user = request.env['res.users']
resources = request.env['appointment.resource']
resource_ids = None
asked_capacity = int(asked_capacity)
resources_remaining_capacity = None
users_remaining_capacity = None
if appointment_type.schedule_based_on == 'resources':
resource_ids = json.loads(unquote_plus(available_resource_ids))
# Check if there is still enough capacity (in case someone else booked with a resource in the meantime)
resources = request.env['appointment.resource'].sudo().browse(resource_ids).exists()
if any(resource not in appointment_type.resource_ids for resource in resources):
raise NotFound()
resources_remaining_capacity = appointment_type._get_resources_remaining_capacity(resources, date_start, date_end, with_linked_resources=False)
if resources_remaining_capacity['total_remaining_capacity'] < asked_capacity:
return request.redirect('/appointment/%s?%s' % (appointment_type.id, keep_query('*', state='failed-resource')))
else:
# check availability of the selected user again (in case someone else booked while the client was entering the form)
staff_user = request.env['res.users'].sudo().search([('id', '=', int(staff_user_id))])
if staff_user not in appointment_type.staff_user_ids:
raise NotFound()
if staff_user and not staff_user.partner_id.calendar_verify_availability(date_start, date_end):
return request.redirect('/appointment/%s?%s' % (appointment_type.id, keep_query('*', state='failed-staff-user')))
# check if there is still enough capacity
users_remaining_capacity = appointment_type._get_users_remaining_capacity(staff_user, date_start, date_end)
if users_remaining_capacity['total_remaining_capacity'] < asked_capacity and appointment_type.capacity_type != 'one_booking':
return request.redirect('/appointment/%s?%s' % (appointment_type.id, keep_query('*', state='failed-staff-user')))


guests = None
if appointment_type.allow_guests:
if guest_emails_str:
guests = request.env['calendar.event'].sudo()._find_or_create_partners(guest_emails_str)

customer = self._get_customer_partner()

# considering phone and email are mandatory
new_customer = not (customer.email) or not (customer.phone)
if not new_customer and customer.email != email and customer.email_normalized != email_normalize(email):
new_customer = True
if not new_customer and not customer.phone:
new_customer = True
if not new_customer:
customer_phone_fmt = customer._phone_format(fname="phone")
input_country = self._get_customer_country()
input_phone_fmt = phone_validation.phone_format(phone, input_country.code, input_country.phone_code, force_format="E164", raise_exception=False)
new_customer = customer.phone != phone and customer_phone_fmt != input_phone_fmt

if new_customer:
customer = customer.sudo().create({
'name': name,
'phone': customer._phone_format(number=phone, country=self._get_customer_country()) or phone,
'email': email,
'lang': request.lang.code,
})

# partner_inputs dictionary structures all answer inputs received on the appointment submission: key is question id, value
# is answer id (as string) for choice questions, text input for text questions, array of ids for multiple choice questions.
partner_inputs = {}
appointment_question_ids = appointment_type.question_ids.ids
for k_key, k_value in [item for item in kwargs.items() if item[1]]:
question_id_str = re.match(r"\bquestion_([0-9]+)\b", k_key)
if question_id_str and int(question_id_str.group(1)) in appointment_question_ids:
partner_inputs[int(question_id_str.group(1))] = k_value
continue
checkbox_ids_str = re.match(r"\bquestion_([0-9]+)_answer_([0-9]+)\b", k_key)
if checkbox_ids_str:
question_id, answer_id = [int(checkbox_ids_str.group(1)), int(checkbox_ids_str.group(2))]
if question_id in appointment_question_ids:
partner_inputs[question_id] = partner_inputs.get(question_id, []) + [answer_id]

# The answer inputs will be created in _prepare_calendar_event_values from the values in answer_input_values
answer_input_values = []
base_answer_input_vals = {
'appointment_type_id': appointment_type.id,
'partner_id': customer.id,
}

for question in appointment_type.question_ids.filtered(lambda question: question.id in partner_inputs.keys()):
if question.question_type == 'checkbox':
answers = question.answer_ids.filtered(lambda answer: answer.id in partner_inputs[question.id])
answer_input_values.extend([
dict(base_answer_input_vals, question_id=question.id, value_answer_id=answer.id) for answer in answers
])
elif question.question_type in ['select', 'radio']:
answer_input_values.append(
dict(base_answer_input_vals, question_id=question.id, value_answer_id=int(partner_inputs[question.id]))
)
elif question.question_type in ['char', 'text']:
answer_input_values.append(
dict(base_answer_input_vals, question_id=question.id, value_text_box=partner_inputs[question.id].strip())
)

booking_line_values = []
if appointment_type.schedule_based_on == 'resources':
capacity_to_assign = asked_capacity
for resource in resources:
resource_remaining_capacity = resources_remaining_capacity.get(resource)
new_capacity_reserved = min(resource_remaining_capacity, capacity_to_assign, resource.capacity)
capacity_to_assign -= new_capacity_reserved
booking_line_values.append({
'appointment_resource_id': resource.id,
'capacity_reserved': new_capacity_reserved,
'capacity_used': new_capacity_reserved if resource.shareable and appointment_type.capacity_type != 'one_booking' else resource.capacity,
})
else:
user_remaining_capacity = users_remaining_capacity['total_remaining_capacity']
new_capacity_reserved = min(user_remaining_capacity, asked_capacity, appointment_type.user_capacity_count)
booking_line_values.append({
'appointment_user_id': staff_user.id,
'capacity_reserved': new_capacity_reserved,
'capacity_used': new_capacity_reserved,
})
if invite_token:
appointment_invite = request.env['appointment.invite'].sudo().search([('access_token', '=', invite_token)])
else:
appointment_invite = request.env['appointment.invite']
return self._handle_appointment_form_submission(
appointment_type, date_start, date_end, duration, answer_input_values, name,
customer, appointment_invite, guests, staff_user, asked_capacity, booking_line_values
)
5 changes: 5 additions & 0 deletions appointment_capacity/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import appointment_type
from . import appointment_booking
from . import calender_event
from . import calendar_booking_line
from . import res_partner
29 changes: 29 additions & 0 deletions appointment_capacity/models/appointment_booking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from odoo import api, fields, models


class AppointmentBookingLine(models.Model):
_inherit = "appointment.booking.line"

appointment_user_id = fields.Many2one('res.users', string="Appointment user", ondelete="cascade")
appointment_resource_id = fields.Many2one('appointment.resource', string="Appointment Resource",ondelete="cascade",required=False)

@api.depends(
"appointment_resource_id.capacity",
"appointment_resource_id.shareable",
"appointment_type_id.capacity_type",
"appointment_type_id.user_capacity_count",
"capacity_reserved",
)
def _compute_capacity_used(self):
self.capacity_used = 0
for line in self:
if line.capacity_reserved == 0:
line.capacity_used = 0
elif line.appointment_type_id.capacity_type == 'multiple_bookings':
line.capacity_used = 1
elif line.appointment_type_id.capacity_type == 'multiple_seats':
line.capacity_used = line.capacity_reserved
elif (not line.appointment_resource_id.shareable or line.appointment_type_id.capacity_type == 'one_booking') and line.appointment_type_id.schedule_based_on == 'resources':
line.capacity_used = line.appointment_resource_id.capacity
else:
line.capacity_used = line.capacity_reserved
Loading