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

18.0 appointment update drat #652

Draft
wants to merge 1 commit 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_update/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
13 changes: 13 additions & 0 deletions appointment_update/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
'name': 'Appointment update',
'version': '1.0',
'description': """
Allow clients to Schedule Appointments through the Portal and book multiple appointment
""",
'depends': ['appointment', 'appointment_account_payment', 'website_appointment'],
'data': [
'views/appointment_update_type_views.xml',
],
'installable': True,
'license': 'OEEL-1',
}
1 change: 1 addition & 0 deletions appointment_update/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import appointment_update
181 changes: 181 additions & 0 deletions appointment_update/controllers/appointment_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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 fields
from odoo.http import request
from odoo.tools import email_normalize
from odoo.addons.base.models.ir_qweb import keep_query
from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.appointment.controllers.appointment import AppointmentController

class AppointmentUpdateController(AppointmentController):

def _prepare_appointment_type_page_values(self, appointment_type, staff_user_id=False, resource_selected_id=False, **kwargs):
result = super()._prepare_appointment_type_page_values(appointment_type, staff_user_id, resource_selected_id, **kwargs)

filter_resource_ids = json.loads(kwargs.get('filter_resource_ids') or '[]')
resources_possible = self._get_possible_resources(appointment_type, filter_resource_ids)
resource_default = resource_selected = request.env['appointment.resource']
staff_user_id = int(staff_user_id) if staff_user_id else False
resource_selected_id = int(resource_selected_id) if resource_selected_id else False
track_capacity = appointment_type.track_capacity
max_capacity_possible = 0
if resources_possible:
if resource_selected_id and resource_selected_id in resources_possible.ids and appointment_type.assign_method != 'time_resource':
resource_selected = request.env['appointment.resource'].sudo().browse(resource_selected_id)
elif appointment_type.assign_method == 'resource_time':
resource_default = resources_possible[0]
# my code changes
if track_capacity == 'one_booking_per_slot' or track_capacity == 'multiple_booking_per_slot':
max_capacity_possible = 1
elif track_capacity == 'multiple_seat_per_slot':
if appointment_type.schedule_based_on == 'users':
max_capacity_possible = appointment_type.total_booking
else:
possible_combinations = (resource_selected or resource_default or resources_possible)._get_filtered_possible_capacity_combinations(1, {})
max_capacity_possible = possible_combinations[-1][1] if possible_combinations else 1
result['max_capacity'] = min(12, max_capacity_possible)
return result

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;
"""
# 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)

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
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')))

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()

# email is mandatory
new_customer = not customer.email
if not new_customer and customer.email != email and customer.email_normalized != email_normalize(email):
new_customer = True
if not new_customer:
# phone is mandatory
if not customer.phone:
customer.phone = customer._phone_format(number=phone) or phone
else:
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 = []
track_capacity = appointment_type.track_capacity
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,
# my code changes
'capacity_used': new_capacity_reserved if track_capacity != 'one_booking_per_slot' and resource.shareable else resource.capacity
})
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)
2 changes: 2 additions & 0 deletions appointment_update/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import appointment_update_type
from . import appointment_update_booking_line
17 changes: 17 additions & 0 deletions appointment_update/models/appointment_update_booking_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from odoo import api, models

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

@api.depends('appointment_resource_id.capacity', 'appointment_resource_id.shareable','appointment_type_id.track_capacity', 'capacity_reserved', 'appointment_type_id.total_booking')
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.track_capacity == 'multiple_booking_per_slot':
line.capacity_used = 1
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'):
line.capacity_used = line.appointment_resource_id.capacity
else:
line.capacity_used = line.capacity_reserved
56 changes: 56 additions & 0 deletions appointment_update/models/appointment_update_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from odoo import api, fields, models

class AppointmentType(models.Model):
_inherit = "appointment.type"

track_capacity = fields.Selection(
[
("one_booking_per_slot", "One Booking Per Slot"),
("multiple_booking_per_slot", "Multiple Booking Per Slot"),
("multiple_seat_per_slot", "Multiple Seat Per Slot"),
],
string="Capacities",
store=True,
default="one_booking_per_slot",
compute="_compute_resource_manage_capacity",
required=True,
readonly=False,
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,
while "multiple bookings per slot" allows multiple bookings based on user input.
"Multiple seat per slot" indicates that bookings can be made according to the user's capacity.""",
)
total_booking = fields.Integer("Total Booking", default=1)

_sql_constraints = [
("check_total_booking", "check(total_booking >= 1)", "total booking/seats should be at least 1."),
]

@api.onchange("track_capacity")
def _onchange_track_capacity(self):
if (self.track_capacity == "one_booking_per_slot"):
self.resource_manage_capacity = False
else:
self.resource_manage_capacity = True

def _slot_availability_is_resource_available(self, slot, resource, availability_values):
result = super()._slot_availability_is_resource_available(slot,resource, availability_values)

slot_start_dt_utc, slot_end_dt_utc = slot['UTC'][0], slot['UTC'][1]
resource_to_bookings = availability_values.get('resource_to_bookings')

if resource_to_bookings.get(resource):
if resource_to_bookings[resource].filtered(lambda bl: bl.event_start < slot_end_dt_utc and bl.event_stop > slot_start_dt_utc):
if self.track_capacity == "multiple_booking_per_slot":
result = True
elif self.track_capacity == "multiple_seat_per_slot":
result = resource.shareable
return result

def _slot_availability_select_best_resources(self, capacity_info, asked_capacity):
result = super()._slot_availability_select_best_resources(capacity_info, asked_capacity)
available_resources = self.env['appointment.resource'].concat(*capacity_info.keys()).sorted('sequence')

if self.track_capacity == "one_booking_per_slot":
return available_resources[0] if self.assign_method != "time_resource" else available_resources

return result
53 changes: 53 additions & 0 deletions appointment_update/views/appointment_update_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="appointment_update_type_view_form" model="ir.ui.view">
<field name="name">appointment.type.view.form.inherit.appointment.update.type.view</field>
<field name="model">appointment.type</field>
<field name="inherit_id" ref="appointment.appointment_type_view_form"/>
<field name="arch" type="xml">
<field name="assign_method" position="after">
<label for="track_capacity" />
<div>
<field name="track_capacity" class="oe_inline"/>
<!-- Availability based on 'user' -->
<span invisible="track_capacity == 'one_booking_per_slot' or schedule_based_on == 'resources'">
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"/>
<span class="badge bg-info" invisible="track_capacity == 'multiple_booking_per_slot'">seats</span>
<span class="badge bg-info" invisible="track_capacity == 'multiple_seat_per_slot'">bookings</span>
</span>
<!-- Availability based on 'resource' -->
<span invisible="track_capacity == 'one_booking_per_slot' or schedule_based_on == 'users'">
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"/>
<span class="badge bg-secondary" invisible="track_capacity == 'multiple_booking_per_slot'">seats</span>
<span class="badge bg-secondary" invisible="track_capacity == 'multiple_seat_per_slot'">bookings</span>
<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">
<i class="oi oi-fw o_button_icon oi-arrow-right me-1"></i> Manage Resources
</a>
</span>
</div>
</field>
<xpath expr="//field[@name='resource_manage_capacity']/.." position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record id="appointment_update_type_view_form_1" model="ir.ui.view">
<field name="name">appointment.type.view.form.inherit.appointment.update.type.view</field>
<field name="model">appointment.type</field>
<field name="inherit_id" ref="appointment_account_payment.appointment_type_view_form"/>
<field name="arch" type="xml">
<xpath expr="//span[@class='ms-1'][1]" position="replace">
<span invisible="track_capacity == 'multiple_seat_per_slot'" class="ms-1">per booking</span>
</xpath>
<xpath expr="//span[@class='ms-1'][2]" position="replace">
<span invisible="track_capacity != 'multiple_seat_per_slot'" class="ms-1">per seat</span>
</xpath>
</field>
</record>

<template id="appointment_update_appointment_templates_appointments" inherit_id="appointment.appointment_info">
<xpath expr="//div[@t-if='not based_on_users and appointment_type.resource_manage_capacity']" position="attributes">
<attribute name="t-if">appointment_type.track_capacity != 'one_booking_per_slot' and appointment_type.track_capacity != 'multiple_booking_per_slot'</attribute>
</xpath>
</template>
</odoo>