Skip to content
Open
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
47 changes: 38 additions & 9 deletions resource_booking/models/resource_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
# Copyright 2022 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from datetime import datetime, time, timedelta

from pytz import UTC

from odoo import api, fields, models
from odoo.osv import expression

from odoo.addons.resource.models.utils import Intervals

Expand Down Expand Up @@ -41,6 +44,9 @@ def _calendar_event_busy_intervals(
"""Get busy meeting intervals."""
assert start_dt.tzinfo
assert end_dt.tzinfo
interval_tz = start_dt.tzinfo
start_local_date = start_dt.date()
end_local_date = end_dt.date()
start_dt, end_dt = (
fields.Datetime.to_string(dt.astimezone(UTC)) for dt in (start_dt, end_dt)
)
Expand All @@ -56,8 +62,19 @@ def _calendar_event_busy_intervals(
return Intervals(intervals)
# Simple domain to get all possibly conflicting events in a single
# query; this reduces DB calls and helps the underlying recurring
# system (in calendar.event) to work smoothly
domain = [("start", "<=", end_dt), ("stop", ">=", start_dt)]
# system (in calendar.event) to work smoothly. All-day events are
# stored without start/stop timestamps in some flows, so OR in a
# date-based predicate to catch them too.
domain = expression.OR(
[
[("start", "<=", end_dt), ("stop", ">=", start_dt)],
[
("allday", "=", True),
("start_date", "<=", end_local_date),
("stop_date", ">=", start_local_date),
],
]
)
# Anyway up to this version, is more performant to restrict as much as possible
# the events to avoid recurrent events.
# TODO: in v14 we should test which approach remains the most performant
Expand Down Expand Up @@ -89,15 +106,27 @@ def _calendar_event_busy_intervals(
):
raise Busy
except Busy:
# Add the matched event as a busy interval
# Add the matched event as a busy interval. All-day events
# have no start/stop timestamps in some flows, so derive the
# interval from start_date/stop_date in the analyzer's tz.
if event.allday and event.start_date and event.stop_date:
event_start = interval_tz.localize(
datetime.combine(event.start_date, time.min)
)
event_stop = interval_tz.localize(
datetime.combine(event.stop_date + timedelta(days=1), time.min)
)
else:
event_start = fields.Datetime.context_timestamp(
event, fields.Datetime.to_datetime(event.start)
)
event_stop = fields.Datetime.context_timestamp(
event, fields.Datetime.to_datetime(event.stop)
)
intervals.append(
(
fields.Datetime.context_timestamp(
event, fields.Datetime.to_datetime(event.start)
),
fields.Datetime.context_timestamp(
event, fields.Datetime.to_datetime(event.stop)
),
event_start,
event_stop,
self.env["resource.calendar.leaves"],
)
)
Expand Down
31 changes: 31 additions & 0 deletions resource_booking/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,37 @@ def test_recurring_event(self):
rb_f.start = datetime(2021, 3, 1, 9)
self.assertTrue(rb_f.combination_id)

def test_allday_event_blocks_booking_slot(self):
"""All-day calendar events block booking slots that overlap their day.

Without all-day handling, the date-only event is invisible to the
scheduling search (which queries on start/stop datetimes), so the
booking is incorrectly accepted.
"""
user = self.users[0]
self.env["calendar.event"].create(
{
"name": "PTO",
"allday": True,
"start_date": "2021-03-01",
"stop_date": "2021-03-01",
"user_id": user.id,
"partner_ids": [Command.set([user.partner_id.id])],
}
)
rb_f = Form(self.env["resource.booking"])
rb_f.partner_ids.add(self.partner)
rb_f.type_id = self.rbt
# Force the user-resource combination so the all-day event has to block it
rb_f.combination_auto_assign = False
rb_f.combination_id = self.rbcs[0]
rb_f.start = datetime(2021, 3, 1, 9)
with self.assertRaises(ValidationError):
rb_f.save()
# Following Monday is fine
rb_f.start = datetime(2021, 3, 8, 9)
rb_f.save()

@mute_logger("odoo.models.unlink")
def test_change_calendar_after_bookings_exist(self):
"""Calendar changes can be done only if they introduce no conflicts."""
Expand Down
Loading