Skip to content

Commit

Permalink
[MIG] resource_booking: Migration to 16.0
Browse files Browse the repository at this point in the history
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
  • Loading branch information
norlinhenrik authored and victoralmau committed Mar 12, 2024
1 parent cbae059 commit b31f313
Show file tree
Hide file tree
Showing 18 changed files with 219 additions and 118 deletions.
20 changes: 12 additions & 8 deletions resource_booking/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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|
Expand Down Expand Up @@ -170,7 +170,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/calendar/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 <https://github.com/OCA/calendar/issues/new?body=module:%20resource_booking%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/calendar/issues/new?body=module:%20resource_booking%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Expand All @@ -186,6 +186,7 @@ Contributors
~~~~~~~~~~~~

* Jairo Llopis <[email protected]> (https://www.tecnativa.com/)
* Henrik Norlin (https://ows.cloud)

Maintainers
~~~~~~~~~~~
Expand All @@ -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 <https://odoo-community.org/page/maintainer-role>`__:
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-pedrobaeza|
|maintainer-pedrobaeza| |maintainer-ows-cloud|

This module is part of the `OCA/calendar <https://github.com/OCA/calendar/tree/15.0/resource_booking>`_ project on GitHub.
This module is part of the `OCA/calendar <https://github.com/OCA/calendar/tree/16.0/resource_booking>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
5 changes: 3 additions & 2 deletions resource_booking/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
18 changes: 18 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,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)
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
111 changes: 81 additions & 30 deletions resource_booking/models/resource_booking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
#
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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 = (
Expand Down Expand Up @@ -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."""
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()
videocall_location = fields.Char(string="Meeting URL")
Expand Down Expand Up @@ -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,
Expand All @@ -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)],
Expand Down
1 change: 1 addition & 0 deletions resource_booking/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* Jairo Llopis <[email protected]> (https://www.tecnativa.com/)
* Henrik Norlin (https://ows.cloud)
Loading

0 comments on commit b31f313

Please sign in to comment.