Skip to content

Commit f81910f

Browse files
committed
[IMP] appointment_capacity: added test cases for usr/resource remaining capacity
1 parent 5aab045 commit f81910f

12 files changed

+649
-107
lines changed

appointment_capacity/__manifest__.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
{
2-
'name': 'Appointment Capacity Management',
3-
'version': '1.0',
4-
'summary': 'Enhance appointment booking with multiple bookings and seats per slot.',
5-
'description': """
2+
"name": "Appointment Capacity Management",
3+
"version": "1.0",
4+
"summary": "Enhance appointment booking with multiple bookings and seats per slot.",
5+
"description": """
66
- Allows multiple bookings per time slot.
77
- Supports multiple seats per slot.
88
""",
9-
'author': 'Darshan Patel',
10-
'depends': ['appointment','appointment_account_payment', 'calendar'],
11-
'data': [
12-
'views/appointment_type_views.xml',
13-
'views/appointment_template_appointment.xml',
14-
'views/calendar_event_views.xml',
9+
"author": "Darshan Patel",
10+
"depends": ["appointment", "appointment_account_payment", "calendar"],
11+
"data": [
12+
"views/appointment_type_views.xml",
13+
"views/appointment_template_appointment.xml",
14+
"views/calendar_event_views.xml",
1515
],
16-
'installable': True,
17-
'license': 'LGPL-3',
16+
"installable": True,
17+
"license": "LGPL-3",
1818
}

appointment_capacity/controllers/appointment.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616

1717
class AppointmentCapacityController(AppointmentController):
1818

19-
2019
def _prepare_appointment_type_page_values(self, appointment_type, staff_user_id=False, resource_selected_id=False, skip_resource_selection=False, **kwargs):
2120
"""
2221
Overrides `_prepare_appointment_type_page_values` to include capacity management based on `capacity_type`.
2322
"""
2423
values = super()._prepare_appointment_type_page_values(appointment_type, staff_user_id, resource_selected_id, **kwargs)
25-
24+
2625
if appointment_type.capacity_type == 'multiple_seats':
2726
if appointment_type.schedule_based_on == 'users':
2827
max_capacity_possible = appointment_type.user_capacity_count
@@ -31,10 +30,9 @@ def _prepare_appointment_type_page_values(self, appointment_type, staff_user_id=
3130
max_capacity_possible = possible_combinations[-1][1] if possible_combinations else 1
3231
else:
3332
max_capacity_possible = 1
34-
35-
values['max_capacity_possible'] = min(12, max_capacity_possible)
36-
return values
3733

34+
values['max_capacity'] = min(12, max_capacity_possible)
35+
return values
3836

3937
@http.route()
4038
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,
@@ -106,7 +104,6 @@ def appointment_form_submit(self, appointment_type_id, datetime_str, duration_st
106104
if users_remaining_capacity['total_remaining_capacity'] < asked_capacity and appointment_type.capacity_type != 'one_booking':
107105
return request.redirect('/appointment/%s?%s' % (appointment_type.id, keep_query('*', state='failed-staff-user')))
108106

109-
110107
guests = None
111108
if appointment_type.allow_guests:
112109
if guest_emails_str:
@@ -156,7 +153,7 @@ def appointment_form_submit(self, appointment_type_id, datetime_str, duration_st
156153
'partner_id': customer.id,
157154
}
158155

159-
for question in appointment_type.question_ids.filtered(lambda question: question.id in partner_inputs.keys()):
156+
for question in appointment_type.question_ids.filtered(lambda question: question.id in partner_inputs):
160157
if question.question_type == 'checkbox':
161158
answers = question.answer_ids.filtered(lambda answer: answer.id in partner_inputs[question.id])
162159
answer_input_values.extend([
@@ -181,7 +178,7 @@ def appointment_form_submit(self, appointment_type_id, datetime_str, duration_st
181178
booking_line_values.append({
182179
'appointment_resource_id': resource.id,
183180
'capacity_reserved': new_capacity_reserved,
184-
'capacity_used': new_capacity_reserved if resource.shareable and appointment_type.capacity_type != 'single_booking' else resource.capacity,
181+
'capacity_used': new_capacity_reserved if resource.shareable and appointment_type.capacity_type != 'one_booking' else resource.capacity,
185182
})
186183
else:
187184
user_remaining_capacity = users_remaining_capacity['total_remaining_capacity']
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from . import appointment_type
22
from . import appointment_booking
3-
from . import calender_event
3+
from . import calendar_booking
44
from . import calendar_booking_line
5+
from . import calender_event
56
from . import res_partner

appointment_capacity/models/appointment_booking.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class AppointmentBookingLine(models.Model):
55
_inherit = "appointment.booking.line"
66

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

1010
@api.depends(
1111
"appointment_resource_id.capacity",
@@ -23,7 +23,7 @@ def _compute_capacity_used(self):
2323
line.capacity_used = 1
2424
elif line.appointment_type_id.capacity_type == 'multiple_seats':
2525
line.capacity_used = line.capacity_reserved
26-
elif (not line.appointment_resource_id.shareable or line.appointment_type_id.capacity_type == 'single_booking') and line.appointment_type_id.schedule_based_on == 'resources':
26+
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':
2727
line.capacity_used = line.appointment_resource_id.capacity
2828
else:
2929
line.capacity_used = line.capacity_reserved

appointment_capacity/models/appointment_type.py

Lines changed: 19 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,20 @@ class AppointmentType(models.Model):
1818

1919
capacity_type = fields.Selection(
2020
[
21-
("single_booking", "Single Booking"),
21+
("one_booking", "One Booking"),
2222
("multiple_bookings", "Multiple Bookings"),
2323
("multiple_seats", "Multiple Seats"),
2424
],
2525
string="Capacity Type",
2626
required=True,
27-
default="single_booking",
27+
default="one_booking",
2828
)
29-
3029
user_capacity_count = fields.Integer(
3130
"User cpacity count",
3231
default=1,
3332
help="Maximum number of users bookings per slot (for multiple bookings or multiple seats).",
3433
)
3534

36-
3735
@api.constrains("user_capacity_count")
3836
def _check_user_capacity_count(self):
3937
for record in self:
@@ -91,7 +89,7 @@ def _slot_availability_is_resource_available(self, slot, resource, availability_
9189
slot_start_dt_utc, slot_end_dt_utc = slot['UTC'][0], slot['UTC'][1]
9290
resource_to_bookings = availability_values.get('resource_to_bookings')
9391
# Check if there is already a booking line for the time slot and make it available
94-
# only if the resource is shareable and the capacity_type is not single_booking.
92+
# only if the resource is shareable and the capacity_type is not one_booking.
9593
# This avoid to mark the resource as "available" and compute unnecessary remaining capacity computation
9694
# because of potential linked resources.
9795
if resource_to_bookings.get(resource):
@@ -108,7 +106,6 @@ def _slot_availability_is_resource_available(self, slot, resource, availability_
108106

109107
return True
110108

111-
112109
def _slot_availability_select_best_resources(self, capacity_info, asked_capacity):
113110
""" Check and select the best resources for the capacity needed
114111
:params main_resources_remaining_capacity <dict>: dict containing remaining capacities of resources available
@@ -120,7 +117,7 @@ def _slot_availability_select_best_resources(self, capacity_info, asked_capacity
120117
available_resources = self.env['appointment.resource'].concat(*capacity_info.keys()).sorted('sequence')
121118
if not available_resources:
122119
return self.env['appointment.resource']
123-
if self.capacity_type == 'single_booking':
120+
if self.capacity_type == 'one_booking':
124121
return available_resources[0] if self.assign_method != 'time_resource' else available_resources
125122

126123
perfect_matches = available_resources.filtered(
@@ -209,10 +206,9 @@ def _slots_fill_users_availability(self, slots, start_dt, end_dt, filter_users=N
209206
else:
210207
slot['staff_user_id'] = available_staff_users
211208

212-
213-
214209
def _slot_availability_is_user_available(self, slot, staff_user, availability_values, asked_capacity=1):
215-
""" This method verifies if the user is available on the given slot.
210+
"""
211+
This method verifies if the user is available on the given slot.
216212
It checks whether the user has calendar events clashing and if he
217213
is included in slot's restricted users.
218214
@@ -235,8 +231,7 @@ def _slot_availability_is_user_available(self, slot, staff_user, availability_va
235231
return False
236232

237233
partner_to_events = availability_values.get('partner_to_events') or {}
238-
users_remaining_capacity = self._get_users_remaining_capacity(staff_user, slot['UTC'][0], slot['UTC'][1],
239-
availability_values=availability_values)
234+
users_remaining_capacity = self._get_users_remaining_capacity(staff_user, slot['UTC'][0], slot['UTC'][1])
240235
if partner_to_events.get(staff_user.partner_id):
241236
for day_dt in rrule.rrule(freq=rrule.DAILY,
242237
dtstart=slot_start_dt_utc,
@@ -247,7 +242,7 @@ def _slot_availability_is_user_available(self, slot, staff_user, availability_va
247242
# return False
248243
for event in day_events:
249244
if not event.allday and (event.start < slot_end_dt_utc and event.stop > slot_start_dt_utc):
250-
if self.capacity_type != "single_booking" and self == event.appointment_type_id:
245+
if self.capacity_type != "one_booking" and self == event.appointment_type_id:
251246
if users_remaining_capacity['total_remaining_capacity'] >= asked_capacity:
252247
continue
253248
return False
@@ -260,13 +255,12 @@ def _slot_availability_is_user_available(self, slot, staff_user, availability_va
260255
return False
261256
return True
262257

263-
def _get_users_remaining_capacity(self, users, slot_start_utc, slot_stop_utc, availability_values=None):
264-
"""
258+
def _get_users_remaining_capacity(self, users, slot_start_utc, slot_stop_utc):
259+
"""
265260
Compute the remaining capacity for users in a specific time slot.
266261
:param <res.users> users : record containing one or a multiple of user
267262
:param datetime slot_start_utc: start of slot (in naive UTC)
268263
:param datetime slot_stop_utc: end of slot (in naive UTC)
269-
:param dict availability_values: dict of data used for availability check.
270264
271265
:return remaining_capacity:
272266
"""
@@ -277,17 +271,12 @@ def _get_users_remaining_capacity(self, users, slot_start_utc, slot_stop_utc, av
277271
if not all_users:
278272
return {'total_remaining_capacity': 0}
279273

280-
booking_lines = self.env['appointment.booking.line'].sudo()
281-
if availability_values is None:
282-
availability_values = self._slot_availability_prepare_users_values(all_users, slot_start_utc, slot_stop_utc)
283-
users_to_bookings = availability_values.get('users_to_bookings', {})
284-
274+
booking_lines = self.env['appointment.booking.line'].sudo().search([
275+
('appointment_user_id', 'in', all_users.ids),
276+
('event_start', '<', slot_stop_utc),
277+
('event_stop', '>', slot_start_utc),
278+
])
285279
users_remaining_capacity = {}
286-
for user, booking_lines_ids in users_to_bookings.items():
287-
if user in all_users:
288-
booking_lines |= booking_lines_ids
289-
booking_lines = booking_lines.filtered(lambda bl: bl.event_start < slot_stop_utc and bl.event_stop > slot_start_utc)
290-
291280
users_booking_lines = booking_lines.grouped('appointment_user_id')
292281

293282
for user in all_users:
@@ -296,50 +285,9 @@ def _get_users_remaining_capacity(self, users, slot_start_utc, slot_stop_utc, av
296285
users_remaining_capacity.update(total_remaining_capacity=sum(users_remaining_capacity.values()))
297286
return users_remaining_capacity
298287

299-
def _slot_availability_prepare_users_values(self, staff_users, start_dt, end_dt):
300-
"""
301-
Override to add booking values.
302-
303-
:return: update ``super()`` values with users booking vaues, formatted like
304-
{
305-
'users_to_bookings': dict giving their corresponding bookings within the given time range
306-
(see ``_slot_availability_prepare_users_bookings_values()``);
307-
}
308-
"""
309-
users_values = super()._slot_availability_prepare_users_values(staff_users, start_dt, end_dt)
310-
users_values.update(self._slot_availability_prepare_users_bookings_values(staff_users, start_dt, end_dt))
311-
return users_values
312-
313-
def _slot_availability_prepare_users_bookings_values(self, users, start_dt_utc, end_dt_utc):
314-
"""
315-
This method retrieves and organizes bookings for the given users within the specified time range.
316-
Users may handle multiple appointment types, so all overlapping bookings must be considered
317-
to prevent double booking.
318-
319-
:param <res.users> users: A recordset of staff users for whom availability is being checked.
320-
:param datetime start_dt_utc: The start of the appointment check boundary in UTC.
321-
:param datetime end_dt_utc: The end of the appointment check boundary in UTC.
322-
323-
:return: A dict containing booking data, formatted as:
324-
{
325-
'users_to_bookings': A dict mapping user IDs to their booking records,
326-
}
327-
"""
328-
users_to_bookings = {}
329-
if users:
330-
booking_lines = self.env['appointment.booking.line'].sudo().search([
331-
('appointment_user_id', 'in', users.ids),
332-
('event_start', '<', end_dt_utc),
333-
('event_stop', '>', start_dt_utc),
334-
])
335-
336-
users_to_bookings = booking_lines.grouped('appointment_user_id')
337-
return {
338-
'users_to_bookings': users_to_bookings,
339-
}
340-
341288
def _get_appointment_slots(self, timezone, filter_users=None, filter_resources=None, asked_capacity=1, reference_date=None):
342-
""" Fetch available slots to book an appointment.
289+
"""
290+
Fetch available slots to book an appointment.
343291
344292
:param str timezone: timezone string e.g.: 'Europe/Brussels' or 'Etc/GMT+1'
345293
:param <res.users> filter_users: filter available slots for those users (can be a singleton
@@ -390,13 +338,13 @@ def _get_appointment_slots(self, timezone, filter_users=None, filter_resources=N
390338
if self.category == 'custom' and unique_slots:
391339
# Custom appointment type, the first day should depend on the first slot datetime
392340
start_first_slot = unique_slots[0].start_datetime
393-
first_day_utc = start_first_slot if reference_date > start_first_slot else reference_date
341+
first_day_utc = min(reference_date, start_first_slot)
394342
first_day = requested_tz.fromutc(first_day_utc + relativedelta(hours=self.min_schedule_hours))
395343
appointment_duration_days = (unique_slots[-1].end_datetime.date() - reference_date.date()).days
396344
last_day = requested_tz.fromutc(reference_date + relativedelta(days=appointment_duration_days))
397345
elif self.category == 'punctual':
398346
# Punctual appointment type, the first day is the start_datetime if it is in the future, else the first day is now
399-
first_day = requested_tz.fromutc(self.start_datetime if self.start_datetime > now else now)
347+
first_day = requested_tz.fromutc(max(now, self.start_datetime))
400348
last_day = requested_tz.fromutc(self.end_datetime)
401349
else:
402350
# Recurring appointment type
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from odoo import models, Command
2+
3+
4+
class CalendarBookingLine(models.Model):
5+
_inherit = 'calendar.booking'
6+
_description = "Meeting Booking"
7+
8+
def _make_event_from_paid_booking(self):
9+
""" This method is called when the booking is considered as paid. We create a calendar event from the booking values. """
10+
if not self:
11+
return
12+
todo = self.filtered(lambda booking: not booking.calendar_event_id)
13+
unavailable_bookings = todo._filter_unavailable_bookings()
14+
15+
for booking in todo - unavailable_bookings:
16+
booking_line_values = [{
17+
'appointment_user_id': line.appointment_user_id.id,
18+
'appointment_resource_id': line.appointment_resource_id.id,
19+
'capacity_reserved': line.capacity_reserved,
20+
'capacity_used': line.capacity_used,
21+
} for line in booking.booking_line_ids]
22+
23+
calendar_event_values = booking.appointment_type_id._prepare_calendar_event_values(
24+
booking.asked_capacity, booking_line_values, booking.duration, booking.appointment_invite_id,
25+
booking.guest_ids, booking.name, booking.partner_id, booking.staff_user_id, booking.start, booking.stop
26+
)
27+
calendar_event_values['appointment_answer_input_ids'] = [Command.set(booking.appointment_answer_input_ids.ids)]
28+
29+
meeting = self.env['calendar.event'].with_context(
30+
mail_create_nolog=True,
31+
mail_create_nosubscribe=True,
32+
mail_notify_author=True,
33+
allowed_company_ids=booking.staff_user_id.company_ids.ids,
34+
).sudo().create(calendar_event_values)
35+
booking.calendar_event_id = meeting
36+
37+
unavailable_bookings.not_available = True
38+
unavailable_bookings._log_booking_collisions()

0 commit comments

Comments
 (0)