From b31f3136c0987800f7affeb8cbfcf15c89768730 Mon Sep 17 00:00:00 2001
From: Henrik Norlin
Date: Mon, 28 Aug 2023 18:11:50 +0200
Subject: [PATCH] [MIG] resource_booking: Migration to 16.0
Changes done:
- [FIX] _get_available_slots
- [IMP] new field slot_duration
- [IMP] Button (partner -> booking)
- [IMP] combination -> bookings -> create: default combination
- [FIX] _get_intervals() when type_id is missing
- [IMP] booking: search on combination
- [FIX] _availability_is_fitting()
- [IMP] booking list view with hidden partner_ids
- [FIX] attendance hour_to 23:59 -> 24:00
- [FIX] pre-commit
- [FIX] calendar_slot_duration format
- [FIX] _get_calendar_context() with correct start / timezone
Co-authored-by: Henrik Norlin
---
resource_booking/README.rst | 20 ++--
resource_booking/__manifest__.py | 5 +-
.../migrations/16.0.1.0.0/post-migration.py | 18 +++
resource_booking/models/__init__.py | 1 +
resource_booking/models/res_partner.py | 26 ++++
resource_booking/models/resource_booking.py | 111 +++++++++++++-----
.../models/resource_booking_combination.py | 1 +
.../models/resource_booking_type.py | 60 ++--------
resource_booking/readme/CONTRIBUTORS.rst | 1 +
.../static/description/index.html | 14 ++-
resource_booking/templates/portal.xml | 18 +--
resource_booking/tests/common.py | 6 +-
resource_booking/tests/test_backend.py | 14 ++-
.../views/calendar_event_views.xml | 1 -
resource_booking/views/res_partner_views.xml | 25 ++++
.../resource_booking_combination_views.xml | 2 +-
.../views/resource_booking_type_views.xml | 3 +-
.../views/resource_booking_views.xml | 11 +-
18 files changed, 219 insertions(+), 118 deletions(-)
create mode 100644 resource_booking/migrations/16.0.1.0.0/post-migration.py
create mode 100644 resource_booking/models/res_partner.py
create mode 100644 resource_booking/views/res_partner_views.xml
diff --git a/resource_booking/README.rst b/resource_booking/README.rst
index 9daa31aa..5af42b13 100644
--- a/resource_booking/README.rst
+++ b/resource_booking/README.rst
@@ -7,7 +7,7 @@ Resource booking
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:2a68fc2648f97ac64afcd5e5c6fa3ab28bfe94008b501d0ebad818d9e9bd3140
+ !! source digest: sha256:69da76a920e2d581cfc27db05479e5705f196298cbbe67e881c92bf1ea6363af
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
@@ -17,13 +17,13 @@ Resource booking
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcalendar-lightgray.png?logo=github
- :target: https://github.com/OCA/calendar/tree/15.0/resource_booking
+ :target: https://github.com/OCA/calendar/tree/16.0/resource_booking
:alt: OCA/calendar
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/calendar-15-0/calendar-15-0-resource_booking
+ :target: https://translation.odoo-community.org/projects/calendar-16-0/calendar-16-0-resource_booking
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
- :target: https://runboat.odoo-community.org/builds?repo=OCA/calendar&target_branch=15.0
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/calendar&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -170,7 +170,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues `_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-`feedback `_.
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -186,6 +186,7 @@ Contributors
~~~~~~~~~~~~
* Jairo Llopis (https://www.tecnativa.com/)
+* Henrik Norlin (https://ows.cloud)
Maintainers
~~~~~~~~~~~
@@ -203,11 +204,14 @@ promote its widespread use.
.. |maintainer-pedrobaeza| image:: https://github.com/pedrobaeza.png?size=40px
:target: https://github.com/pedrobaeza
:alt: pedrobaeza
+.. |maintainer-ows-cloud| image:: https://github.com/ows-cloud.png?size=40px
+ :target: https://github.com/ows-cloud
+ :alt: ows-cloud
-Current `maintainer `__:
+Current `maintainers `__:
-|maintainer-pedrobaeza|
+|maintainer-pedrobaeza| |maintainer-ows-cloud|
-This module is part of the `OCA/calendar `_ project on GitHub.
+This module is part of the `OCA/calendar `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/resource_booking/__manifest__.py b/resource_booking/__manifest__.py
index 432c3d19..4b71ba26 100644
--- a/resource_booking/__manifest__.py
+++ b/resource_booking/__manifest__.py
@@ -6,12 +6,12 @@
{
"name": "Resource booking",
"summary": "Manage appointments and resource booking",
- "version": "15.0.2.0.0",
+ "version": "16.0.1.0.0",
"development_status": "Production/Stable",
"category": "Appointments",
"website": "https://github.com/OCA/calendar",
"author": "Tecnativa, Odoo Community Association (OCA)",
- "maintainers": ["pedrobaeza"],
+ "maintainers": ["pedrobaeza", "ows-cloud"],
"license": "AGPL-3",
"application": True,
"installable": True,
@@ -37,6 +37,7 @@
"templates/portal.xml",
"views/calendar_event_views.xml",
"views/mail_activity_views.xml",
+ "views/res_partner_views.xml",
"views/resource_booking_combination_views.xml",
"views/resource_booking_type_views.xml",
"views/resource_booking_views.xml",
diff --git a/resource_booking/migrations/16.0.1.0.0/post-migration.py b/resource_booking/migrations/16.0.1.0.0/post-migration.py
new file mode 100644
index 00000000..63b565e6
--- /dev/null
+++ b/resource_booking/migrations/16.0.1.0.0/post-migration.py
@@ -0,0 +1,18 @@
+from openupgradelib import openupgrade
+
+
+def _update_attendance_hour_to(env):
+ """According to the refactoring of _merge_intervals() a booking from 23:00 to 01:00
+ of the next day must be completely available, having defined hour_to=23.59 is not,
+ so it is changed to 24.0.
+ This behavior is confirmed to be correct because in the tests calendars are set
+ with full days (hour_from=0, hour_to=24)."""
+ 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)
diff --git a/resource_booking/models/__init__.py b/resource_booking/models/__init__.py
index 158a4b48..b57e4650 100644
--- a/resource_booking/models/__init__.py
+++ b/resource_booking/models/__init__.py
@@ -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
diff --git a/resource_booking/models/res_partner.py b/resource_booking/models/res_partner.py
new file mode 100644
index 00000000..180ea2aa
--- /dev/null
+++ b/resource_booking/models/res_partner.py
@@ -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
diff --git a/resource_booking/models/resource_booking.py b/resource_booking/models/resource_booking.py
index 85087983..1416386e 100644
--- a/resource_booking/models/resource_booking.py
+++ b/resource_booking/models/resource_booking.py
@@ -14,7 +14,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`.
#
@@ -480,17 +515,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),
@@ -539,30 +579,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
@@ -573,10 +622,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 = (
@@ -716,7 +767,7 @@ def action_confirm(self):
# attendee.state='accepted'
attendees_to_confirm |= attendee
attendees_to_confirm.write({"state": "accepted"})
- self.recompute()
+ self.env.flush_all() # booking.meeting_id.partner_ids and attendees_to_confirm
def action_unschedule(self):
"""Remove associated meetings."""
diff --git a/resource_booking/models/resource_booking_combination.py b/resource_booking/models/resource_booking_combination.py
index a7cf2b1b..f8b3d390 100644
--- a/resource_booking/models/resource_booking_combination.py
+++ b/resource_booking/models/resource_booking_combination.py
@@ -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):
diff --git a/resource_booking/models/resource_booking_type.py b/resource_booking/models/resource_booking_type.py
index d43f25a9..7d1597d0 100644
--- a/resource_booking/models/resource_booking_type.py
+++ b/resource_booking/models/resource_booking_type.py
@@ -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
@@ -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()
videocall_location = fields.Char(string="Meeting URL")
@@ -128,46 +128,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,
@@ -176,8 +138,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)],
diff --git a/resource_booking/readme/CONTRIBUTORS.rst b/resource_booking/readme/CONTRIBUTORS.rst
index 7ee45dc9..f7145432 100644
--- a/resource_booking/readme/CONTRIBUTORS.rst
+++ b/resource_booking/readme/CONTRIBUTORS.rst
@@ -1 +1,2 @@
* Jairo Llopis (https://www.tecnativa.com/)
+* Henrik Norlin (https://ows.cloud)
diff --git a/resource_booking/static/description/index.html b/resource_booking/static/description/index.html
index 5bc10c36..676ce85f 100644
--- a/resource_booking/static/description/index.html
+++ b/resource_booking/static/description/index.html
@@ -1,3 +1,4 @@
+
@@ -366,9 +367,9 @@ Resource booking
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:2a68fc2648f97ac64afcd5e5c6fa3ab28bfe94008b501d0ebad818d9e9bd3140
+!! source digest: sha256:69da76a920e2d581cfc27db05479e5705f196298cbbe67e881c92bf1ea6363af
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-
+
This module adds a new app to allow you to book resource combinations in given
schedules.
Example use cases:
@@ -520,7 +521,7 @@
Bugs are tracked on GitHub Issues .
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-feedback .
+feedback .
Do not contact contributors directly about support or help with technical issues.
@@ -544,9 +546,9 @@
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
-
Current maintainer :
-
-
This module is part of the OCA/calendar project on GitHub.
+
Current maintainers :
+
+
This module is part of the OCA/calendar project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute .
diff --git a/resource_booking/templates/portal.xml b/resource_booking/templates/portal.xml
index fc5165f5..4458958a 100644
--- a/resource_booking/templates/portal.xml
+++ b/resource_booking/templates/portal.xml
@@ -97,7 +97,7 @@
@@ -201,7 +201,7 @@
Cancel
@@ -478,7 +478,7 @@
@@ -505,7 +505,7 @@
Go back
@@ -543,7 +543,7 @@
×
diff --git a/resource_booking/tests/common.py b/resource_booking/tests/common.py
index 1bf5e4bb..3a57b86c 100644
--- a/resource_booking/tests/common.py
+++ b/resource_booking/tests/common.py
@@ -42,7 +42,7 @@ def create_test_data(obj):
"name": "Fridays",
"dayofweek": "4",
"hour_from": 0,
- "hour_to": 23.99,
+ "hour_to": 24,
"day_period": "morning",
},
),
@@ -53,7 +53,7 @@ def create_test_data(obj):
"name": "Saturdays",
"dayofweek": "5",
"hour_from": 0,
- "hour_to": 23.99,
+ "hour_to": 24,
"day_period": "morning",
},
),
@@ -64,7 +64,7 @@ def create_test_data(obj):
"name": "Sunday",
"dayofweek": "6",
"hour_from": 0,
- "hour_to": 23.99,
+ "hour_to": 24,
"day_period": "morning",
},
),
diff --git a/resource_booking/tests/test_backend.py b/resource_booking/tests/test_backend.py
index f91e451e..9755eb96 100644
--- a/resource_booking/tests/test_backend.py
+++ b/resource_booking/tests/test_backend.py
@@ -203,8 +203,16 @@ def test_availability_is_fitting_malformed_date_skip(self):
"""
recset = self.env["resource.booking"]
tuples = [
- (datetime(2021, 3, 1, 18, 0), datetime(2021, 3, 1, 23, 59), recset),
- (datetime(2021, 3, 2, 0, 0), datetime(2021, 3, 2, 23, 59), recset),
+ (
+ datetime(2021, 3, 1, 18, 0),
+ datetime(2021, 3, 1, 23, 59, 59, 999999),
+ recset,
+ ),
+ (
+ datetime(2021, 3, 2, 0, 0),
+ datetime(2021, 3, 2, 23, 59, 59, 999999),
+ recset,
+ ),
(datetime(2021, 3, 3, 0, 0), datetime(2021, 3, 3, 18, 0), recset),
]
available_intervals = Intervals(tuples)
@@ -769,7 +777,7 @@ def test_suggested_and_subscribed_recipients(self):
# Requester and combination must be suggested
self.assertEqual(
rb._message_get_suggested_recipients(),
- {rb.id: [(rb.partner_ids.id, "some customer", "Attendees")]},
+ {rb.id: [(rb.partner_ids.id, "some customer", None, "Attendees")]},
)
def test_creating_rbt_has_tags(self):
diff --git a/resource_booking/views/calendar_event_views.xml b/resource_booking/views/calendar_event_views.xml
index 58671981..d3a9021a 100644
--- a/resource_booking/views/calendar_event_views.xml
+++ b/resource_booking/views/calendar_event_views.xml
@@ -6,7 +6,6 @@
calendar.event.view.form.inherit
calendar.event
-