Skip to content

Commit

Permalink
[FIX] resource_booking: _get_available_slots
Browse files Browse the repository at this point in the history
[IMP] resource_booking: new field slot_duration

[IMP] resource_booking: Button (partner -> booking)

[IMP] resource_booking: combination -> bookings -> create: default combination

[IMP] resource_booking: A booking may have multiple contacts, e.g. people in a room

[FIX] resource_booking: _get_intervals() when type_id is missing

[IMP] resource_booking: booking: search on combination

[FIX] resource_booking: _availability_is_fitting()

[IMP] resource_booking: booking list view with hidden partner_ids

[FIX] resource_booking: migration: attendance hour_to 23:59 -> 24:00

[IMP] resource_booking: rename_xmlids

[FIX] resource_booking: pre-commit

[FIX] resource_booking: calendar_slot_duration format

[FIX] resource_booking: _get_calendar_context() with correct start / timezone
  • Loading branch information
norlinhenrik committed Feb 13, 2024
1 parent 3e30383 commit 3fe4807
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 88 deletions.
1 change: 1 addition & 0 deletions resource_booking/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"security/ir.model.access.csv",
"templates/portal.xml",
"views/calendar_event_views.xml",
"views/res_partner_views.xml",
"views/resource_booking_combination_views.xml",
"views/resource_booking_type_views.xml",
"views/resource_booking_views.xml",
Expand Down
14 changes: 14 additions & 0 deletions resource_booking/migrations/16.0.1.0.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from openupgradelib import openupgrade


def _update_attendance_hour_to(env):
# 23:59 -> 24:00
lines = env["resource.calendar.attendance"].search(
[("hour_to", ">=", 23 + 59 / 60)]
)
lines.hour_to = 24.0


@openupgrade.migrate()
def migrate(env, version):
_update_attendance_hour_to(env)
33 changes: 33 additions & 0 deletions resource_booking/migrations/16.0.1.0.0/pre-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from openupgradelib import openupgrade

xmlids_spec = [
(
"resource_booking.menu_resource_resource",
"resource_booking.resource_resource_menu",
),
(
"resource_booking.menu_resource_calendar",
"resource_booking.resource_calendar_menu",
),
(
"resource_booking.menu_view_resource_calendar_leaves_search",
"resource_booking.resource_calendar_leaves_menu",
),
(
"resource_booking.resource_booking_combination_form",
"resource_booking.resource_booking_combination_view_form",
),
(
"resource_booking.resource_booking_type_form",
"resource_booking.resource_booking_type_view_form",
),
(
"resource_booking.resource_booking_form",
"resource_booking.resource_booking_view_form",
),
]


@openupgrade.migrate(use_env=False)
def migrate(cr, version):
openupgrade.rename_xmlids(cr, xmlids_spec)
1 change: 1 addition & 0 deletions resource_booking/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import calendar_event
from . import res_partner
from . import resource_booking
from . import resource_booking_combination
from . import resource_booking_type
Expand Down
26 changes: 26 additions & 0 deletions resource_booking/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from odoo import fields, models


class ResPartner(models.Model):
_inherit = "res.partner"

resource_booking_count = fields.Integer(
compute="_compute_resource_booking_count", string="Resource booking count"
)
resource_booking_ids = fields.One2many(
"resource.booking", "partner_id", string="Bookings"
)

def _compute_resource_booking_count(self):
for p in self:
p.resource_booking_count = len(p.resource_booking_ids)

def action_view_resource_booking(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"resource_booking.resource_booking_action"
)
action["context"] = {
"default_partner_id": self.id,
}
return action
128 changes: 98 additions & 30 deletions resource_booking/models/resource_booking.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import calendar
from datetime import datetime, timedelta
from datetime import timedelta

from dateutil.relativedelta import relativedelta

Expand All @@ -13,7 +13,42 @@
from odoo.addons.resource.models.resource import Intervals


def _availability_is_fitting(available_intervals, start_dt, end_dt):
def _merge_intervals(intervals):
# Merge intervals where start of current interval == stop of previous interval,
# assuming that the intervals are ordererd.
intervals = [list(tup) for tup in intervals._items]
# Handle 23:59:59:99999
for i in range(len(intervals)):
stop = intervals[i][1]
if (
stop.hour == 23
and stop.minute == 59
and stop.second == 59
and stop.microsecond == 999999
):
intervals[i][1] += timedelta(microseconds=1)
# Begin with the last interval, to safely delete it if needed.
for i in range(len(intervals) - 1, 0, -1):
current_start = intervals[i][0]
current_stop = intervals[i][1]
previous_stop = intervals[i - 1][1]
if current_start == previous_stop:
intervals[i - 1][1] = current_stop
del intervals[i]
return Intervals([tuple(interval) for interval in intervals])


def _availability_is_fitting(available_intervals, start_dt, stop_dt):
available_intervals = _merge_intervals(available_intervals)
for item in available_intervals._items:
available_start, available_stop = item[0], item[1]
if start_dt >= available_start and stop_dt <= available_stop:
return True
return False


def _availability_is_fitting_legacy(available_intervals, start_dt, end_dt):
"""I keep the old method, since part of it may be needed in the new method."""
# Test whether the stretch between start_dt and end_dt is an uninterrupted
# stretch of time as determined by `available_intervals`.
#
Expand Down Expand Up @@ -131,6 +166,14 @@ class ResourceBooking(models.Model):
tracking=True,
help="Who requested this booking?",
)
partner_ids = fields.Many2many(
"res.partner",
string="Contacts",
store=True,
compute="_compute_partner_ids",
inverse="_inverse_partner_ids",
help="E.g. multiple people in a room. Used by sale_resource_booking_period",
)
user_id = fields.Many2one(
comodel_name="res.users",
default=lambda self: self._default_user_id(),
Expand Down Expand Up @@ -324,6 +367,15 @@ def _compute_stop(self):
# Either value is False: no stop date
record.stop = False

@api.depends("partner_id")
def _compute_partner_ids(self):
for record in self:
if record.partner_id:
record.partner_ids = [(6, 0, [record.partner_id.id])]

def _inverse_partner_ids(self):
pass

@api.depends("meeting_id.user_id")
def _compute_user_id(self):
"""Get user from related meeting, if available."""
Expand Down Expand Up @@ -436,17 +488,22 @@ def _get_calendar_context(self, year=None, month=None, now=None):
:param datetime now: Represents the current datetime.
"""
month1 = relativedelta(months=1)
now = now or fields.Datetime.now()
now = fields.Datetime.context_timestamp(self, now or fields.Datetime.now())
year = year or now.year
month = month or now.month
start = datetime(year, month, 1)
start, now = (
fields.Datetime.context_timestamp(self, dt) for dt in (start, now)
start = now.replace(
year=year,
month=month,
day=1,
hour=0,
minute=0,
second=0,
microsecond=0,
)
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
lang = self.env["res.lang"]._lang_get(self.env.lang or self.env.user.lang)
weekday_names = dict(lang.fields_get(["week_start"])["week_start"]["selection"])
slots = self._get_available_slots(start, start + month1)
booking_duration = timedelta(hours=self.duration)
slots = self._get_available_slots(start, start + month1 + booking_duration)
return {
"booking": self,
"calendar": calendar.Calendar(int(lang.week_start) - 1),
Expand Down Expand Up @@ -495,30 +552,39 @@ def _get_best_combination(self):
def _get_available_slots(self, start_dt, end_dt):
"""Return available slots for scheduling current booking."""
result = {}
now = fields.Datetime.context_timestamp(self, fields.Datetime.now())
slot_duration = timedelta(hours=self.type_id.duration)
slot_duration = timedelta(hours=self.type_id.slot_duration)
booking_duration = timedelta(hours=self.duration)
current = max(
now = fields.Datetime.context_timestamp(self, fields.Datetime.now())
start_dt = max(
start_dt, now + timedelta(hours=self.type_id.modifications_deadline)
)
available_intervals = self._get_intervals(current, end_dt)
while current and current < end_dt:
slot_start = self.type_id._get_next_slot_start(current)
if current != slot_start:
current = slot_start
continue
current_interval = Intervals([(current, current + booking_duration, self)])
for start, end, _meta in available_intervals & current_interval:
if end - start == booking_duration:
result.setdefault(current.date(), [])
result[current.date()].append(current)
# I actually only care about the 1st interval, if any
break
current += slot_duration
# available_intervals should start with the beginning of the work day,
# to compute each slot based on the beginning of the work day.
workday_min = start_dt.replace(hour=0, minute=0, second=0, microsecond=0)
available_intervals = self._get_intervals(workday_min, end_dt)
available_intervals = _merge_intervals(available_intervals)
# Loop through available times and append tested start/stop to the result.
test_start = False
for item in available_intervals._items:
available_start, available_stop = item[0], item[1]
test_start = available_start
while test_start and test_start < available_stop:
test_stop = test_start + booking_duration
if (
test_start >= start_dt
and test_start >= available_start
and test_stop <= available_stop
):
if not result.get(test_start.date()):
result.setdefault(test_start.date(), [])
result[test_start.date()].append(test_start)
test_start += slot_duration
return result

def _get_intervals(self, start_dt, end_dt, combination=None):
"""Get available intervals for this booking."""
"""Get available intervals for this booking,
based on the calendar of the booking type
and the calendar(s) of the relevant resource combination(s)."""
# Get all intervals except those from current booking
try:
booking_id = self.id or self._origin.id or -1
Expand All @@ -529,10 +595,12 @@ def _get_intervals(self, start_dt, end_dt, combination=None):
analyzing_booking=booking_id, exclude_public_holidays=True
)
# RBT calendar uses no resources to restrict bookings
resource = self.env["resource.resource"]
result = booking.type_id.resource_calendar_id._work_intervals_batch(
start_dt, end_dt
)[resource.id]
if booking.type_id:
result = booking.type_id.resource_calendar_id._work_intervals_batch(
start_dt, end_dt
)[False]
else:
result = Intervals([])
# Restrict with the chosen combination, or to at least one of the
# available ones
combinations = (
Expand Down
1 change: 1 addition & 0 deletions resource_booking/models/resource_booking_combination.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def action_open_bookings(self):
"res_model": "resource.booking",
"type": "ir.actions.act_window",
"view_mode": "calendar,tree,form",
"context": {"default_combination_id": self.id},
}

def action_open_resource_booking_types(self):
Expand Down
60 changes: 13 additions & 47 deletions resource_booking/models/resource_booking_type.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Copyright 2021 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from datetime import timedelta
from math import ceil
from random import random

from odoo import _, api, fields, models
Expand Down Expand Up @@ -59,10 +57,12 @@ class ResourceBookingType(models.Model):
duration = fields.Float(
required=True,
default=0.5, # 30 minutes
help=(
"Interval offered to start each resource booking. "
"Also used as booking default duration."
),
help=("Booking default duration."),
)
slot_duration = fields.Float(
required=True,
default=0.5, # 30 minutes
help=("Interval offered to start each resource booking."),
)
location = fields.Char()
modifications_deadline = fields.Float(
Expand Down Expand Up @@ -127,46 +127,8 @@ def _get_combinations_priorized(self):
combinations = rels.mapped("combination_id")
return combinations

def _get_next_slot_start(self, start_dt):
"""Slot start as it would come from the beginning of work hours.
Returns a `datetime` object indicating the next slot start (which could
be the same as `start_dt` if it matches), or `False` if no slot is
found in the next 2 weeks.
If the RBT doesn't have a calendar, it returns `start_dt`, unaltered,
because there's no way to know when a slot would start.
"""
duration_delta = timedelta(hours=self.duration)
end_dt = start_dt + duration_delta
workday_min = start_dt.replace(hour=0, minute=0, second=0, microsecond=0)
# Detached compatibility with hr_holidays_public
res_calendar = self.resource_calendar_id.with_context(
exclude_public_holidays=True
)
resource = self.env["resource.resource"]
attendance_intervals = res_calendar._attendance_intervals_batch(
workday_min, end_dt
)[resource.id]
try:
workday_start, valid_end, _meta = attendance_intervals._items[-1]
if valid_end != end_dt:
# Interval found, but without enough time; same as no interval
raise IndexError
except IndexError:
try:
# Returns `False` if no slot is found in the next 2 weeks
return (
res_calendar.plan_hours(self.duration, end_dt, compute_leaves=True)
- duration_delta
)
except TypeError:
return False
time_passed = valid_end - duration_delta - workday_start
return workday_start + duration_delta * ceil(time_passed / duration_delta)

def action_open_bookings(self):
FloatTimeParser = self.env["ir.qweb.field.float_time"]
DurationParser = self.env["ir.qweb.field.duration"]
return {
"context": dict(
self.env.context,
Expand All @@ -175,8 +137,12 @@ def action_open_bookings(self):
default_duration=self.duration,
default_type_id=self.id,
# Context used by web_calendar_slot_duration module
calendar_slot_duration=FloatTimeParser.value_to_html(
self.duration, False
calendar_slot_duration=DurationParser.value_to_html(
self.slot_duration,
{
"unit": "hour",
"digital": True,
},
),
),
"domain": [("type_id", "=", self.id)],
Expand Down
Loading

0 comments on commit 3fe4807

Please sign in to comment.