diff --git a/resource_booking/models/resource_calendar.py b/resource_booking/models/resource_calendar.py index fba48da7..3e1206ca 100644 --- a/resource_booking/models/resource_calendar.py +++ b/resource_booking/models/resource_calendar.py @@ -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 @@ -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) ) @@ -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 @@ -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"], ) ) diff --git a/resource_booking/tests/test_backend.py b/resource_booking/tests/test_backend.py index 0b758067..42f02ff9 100644 --- a/resource_booking/tests/test_backend.py +++ b/resource_booking/tests/test_backend.py @@ -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."""