683 lines
27 KiB
Python
683 lines
27 KiB
Python
# Copyright 2021 Tecnativa - Jairo Llopis
|
|
# Copyright 2022 Tecnativa - Pedro M. Baeza
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
|
|
import calendar
|
|
from datetime import datetime, timedelta
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
|
|
from odoo.addons.resource.models.utils import Intervals
|
|
|
|
def _availability_is_fitting(available_intervals, start_dt, end_dt):
|
|
# Test whether the stretch between start_dt and end_dt is an uninterrupted
|
|
# stretch of time as determined by `available_intervals`.
|
|
#
|
|
# `available_intervals` is typically created by `_get_intervals()`, which in
|
|
# turn uses `calendar._work_intervals()`. It appears to be default upstream
|
|
# behaviour of `_work_intervals()` to create a (start_dt, end_dt, record)
|
|
# tuple for every day, where end_dt is at 23:59, and the next tuple's
|
|
# start_dt is at 00:00.
|
|
#
|
|
# Changing this upstream behaviour of `_work_intervals()` to return a
|
|
# _single_ tuple for any multi-day uninterrupted stretch of time would
|
|
# probably be preferable, but (1.) the code in `_work_intervals()` is
|
|
# unbelievably arcane, and (2.) changing this behaviour is extremely likely
|
|
# to cause bugs elsewhere. So instead, we account for the upstream behaviour
|
|
# here.
|
|
start_date = start_dt.date()
|
|
end_date = end_dt.date()
|
|
# Booking is uninterrupted on the same calendar day.
|
|
if (
|
|
len(available_intervals) == 1
|
|
and available_intervals._items[0][0] <= start_dt
|
|
and available_intervals._items[0][1] >= end_dt
|
|
):
|
|
return True
|
|
# Booking spans more than one calendar day, e.g. from 23:00 to 1:00
|
|
# the next day.
|
|
elif available_intervals and start_date != end_date:
|
|
tally_date = start_date
|
|
for item in available_intervals:
|
|
item0_date = item[0].date()
|
|
item1_date = item[1].date()
|
|
# FIXME: Really weird workaround for when available_intervals has
|
|
# nonsensical items in it where item1_date is before item0_date.
|
|
# Just ignore those items and pretend they don't exist; all the
|
|
# other items appear to make sense.
|
|
if item1_date < item0_date:
|
|
continue
|
|
# Intervals that aren't on the running tally date break the streak.
|
|
# This check is for malformed data in `available_intervals` where a
|
|
# day is skipped.
|
|
if item0_date != tally_date or item1_date != tally_date:
|
|
break
|
|
# Intervals that aren't on the end date should end at 23:59 (and any
|
|
# number of seconds).
|
|
if item1_date != end_date and (item[1].hour != 23 or item[1].minute != 59):
|
|
break
|
|
# Intervals that aren't on the start date should start at 00:00 (and
|
|
# any number of seconds).
|
|
if item0_date != start_date and (item[0].hour != 0 or item[0].minute != 0):
|
|
break
|
|
# The next interval should be on the next day.
|
|
tally_date += timedelta(days=1)
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
|
|
class ResourceBooking(models.Model):
|
|
_name = "resource.booking"
|
|
_inherit = ["mail.thread", "mail.activity.mixin", "portal.mixin"]
|
|
_description = "Resource Booking"
|
|
_order = "start DESC"
|
|
_sql_constraints = [
|
|
(
|
|
"combination_required_if_event",
|
|
"CHECK(meeting_id IS NULL OR combination_id IS NOT NULL)",
|
|
"Missing resource booking combination.",
|
|
),
|
|
(
|
|
"unique_meeting_id",
|
|
"UNIQUE(meeting_id)",
|
|
"Only one event per resource booking can exist.",
|
|
),
|
|
]
|
|
|
|
active = fields.Boolean(default=True)
|
|
meeting_id = fields.Many2one(
|
|
comodel_name="calendar.event",
|
|
string="Meeting",
|
|
context={"default_res_id": False, "default_res_model": False},
|
|
copy=False,
|
|
index=True,
|
|
ondelete="set null",
|
|
help="Meeting confirmed for this booking.",
|
|
)
|
|
categ_ids = fields.Many2many(string="Tags", comodel_name="calendar.event.type")
|
|
combination_id = fields.Many2one(
|
|
comodel_name="resource.booking.combination",
|
|
string="Resources combination",
|
|
compute="_compute_combination_id",
|
|
copy=False,
|
|
domain="[('type_rel_ids.type_id', 'in', [type_id])]",
|
|
index=True,
|
|
readonly=False,
|
|
states={"scheduled": [("required", True)], "confirmed": [("required", True)]},
|
|
store=True,
|
|
tracking=True,
|
|
)
|
|
combination_auto_assign = fields.Boolean(
|
|
string="Auto assigned",
|
|
default=True,
|
|
help=(
|
|
"When checked, resource combinations will be (un)assigned automatically "
|
|
"based on their availability during the booking dates."
|
|
),
|
|
)
|
|
name = fields.Char(index=True, help="Leave empty to autogenerate a booking name.")
|
|
description = fields.Html("Description")
|
|
partner_id = fields.Many2one(
|
|
"res.partner",
|
|
string="Requester",
|
|
index=True,
|
|
ondelete="cascade",
|
|
required=True,
|
|
tracking=True,
|
|
help="Who requested this booking?",
|
|
)
|
|
user_id = fields.Many2one(
|
|
"res.users",
|
|
default=lambda self: self._default_user_id(),
|
|
store=True,
|
|
readonly=False,
|
|
compute="_compute_user_id",
|
|
string="Organizer",
|
|
index=True,
|
|
tracking=True,
|
|
help=(
|
|
"Who organized this booking? Usually whoever created the record. "
|
|
"It will appear as the calendar event organizer, when scheduled, "
|
|
"and calendar notifications will be sent in his/her name."
|
|
),
|
|
)
|
|
location = fields.Char(
|
|
compute="_compute_location",
|
|
readonly=False,
|
|
store=True,
|
|
)
|
|
requester_advice = fields.Text(related="type_id.requester_advice", readonly=True)
|
|
is_modifiable = fields.Boolean(compute="_compute_is_modifiable")
|
|
is_overdue = fields.Boolean(compute="_compute_is_overdue")
|
|
state = fields.Selection(
|
|
[
|
|
("pending", "Pending"),
|
|
("scheduled", "Scheduled"),
|
|
("confirmed", "Confirmed"),
|
|
("canceled", "Canceled"),
|
|
],
|
|
compute="_compute_state",
|
|
store=True,
|
|
default="pending",
|
|
index=True,
|
|
tracking=True,
|
|
help=(
|
|
"Pending: No meeting scheduled.\n"
|
|
"Scheduled: The requester has not confirmed attendance yet.\n"
|
|
"Confirmed: Meeting scheduled, and requester attendance confirmed.\n"
|
|
"Canceled: Meeting removed, booking archived."
|
|
),
|
|
)
|
|
start = fields.Datetime(
|
|
compute="_compute_start",
|
|
copy=False,
|
|
index=True,
|
|
readonly=False,
|
|
store=True,
|
|
tracking=True,
|
|
)
|
|
duration = fields.Float(
|
|
compute="_compute_duration",
|
|
readonly=False,
|
|
store=True,
|
|
tracking=True,
|
|
help="Amount of time that the resources will be booked and unavailable for others.",
|
|
)
|
|
stop = fields.Datetime(
|
|
compute="_compute_stop",
|
|
copy=False,
|
|
index=True,
|
|
store=True,
|
|
tracking=True,
|
|
)
|
|
type_id = fields.Many2one(
|
|
comodel_name="resource.booking.type",
|
|
string="Type",
|
|
index=True,
|
|
ondelete="cascade",
|
|
required=True,
|
|
tracking=True,
|
|
)
|
|
|
|
@api.model
|
|
def _default_user_id(self):
|
|
return self.env.user
|
|
|
|
def _compute_access_url(self):
|
|
result = super()._compute_access_url()
|
|
for one in self:
|
|
one.access_url = "/my/bookings/%d" % one.id
|
|
return result
|
|
|
|
@api.onchange("type_id")
|
|
def _onchange_type_set_categ_ids(self):
|
|
"""Copy default tags from RBT when changing it."""
|
|
for one in self:
|
|
if one.type_id:
|
|
one.categ_ids = one.type_id.categ_ids
|
|
|
|
@api.depends("start", "type_id", "combination_auto_assign")
|
|
def _compute_combination_id(self):
|
|
"""Select best combination candidate when changing booking dates."""
|
|
for one in self:
|
|
# Useless without the interval
|
|
if one.start and one.combination_auto_assign:
|
|
one.combination_id = one._get_best_combination()
|
|
|
|
@api.depends("start")
|
|
def _compute_is_overdue(self):
|
|
"""Indicate if booking is overdue."""
|
|
now = fields.Datetime.now()
|
|
for one in self:
|
|
# You can always modify it if there's no meeting yet
|
|
if not one.start:
|
|
one.is_overdue = False
|
|
continue
|
|
anticipation = timedelta(hours=one.type_id.modifications_deadline)
|
|
deadline = one.start - anticipation
|
|
one.is_overdue = now > deadline
|
|
|
|
@api.depends("is_overdue")
|
|
@api.depends_context("uid", "using_portal")
|
|
def _compute_is_modifiable(self):
|
|
"""Indicate if the booking is modifiable."""
|
|
self.is_modifiable = True
|
|
is_manager = not self.env.context.get(
|
|
"using_portal"
|
|
) and self.env.user.has_group("resource_booking.group_manager")
|
|
# Managers can always modify overdue bookings
|
|
if not is_manager:
|
|
overdue = self.filtered("is_overdue")
|
|
overdue.is_modifiable = False
|
|
|
|
@api.depends("name", "partner_id", "type_id", "meeting_id")
|
|
@api.depends_context("uid", "using_portal")
|
|
def _compute_display_name(self):
|
|
"""Overridden just for dependencies; see `name_get()` for implementation."""
|
|
return super()._compute_display_name()
|
|
|
|
@api.depends("meeting_id.location", "type_id")
|
|
def _compute_location(self):
|
|
"""Get location from meeting or type."""
|
|
for record in self:
|
|
# Get location from type when changing it or creating from ORM
|
|
# HACK https://github.com/odoo/odoo/issues/74152
|
|
if not record.location or record._origin.type_id != record.type_id:
|
|
record.location = record.type_id.location
|
|
# Get it from meeting only when available
|
|
elif record.meeting_id:
|
|
record.location = record.meeting_id.location
|
|
|
|
@api.depends("active", "meeting_id.attendee_ids.state")
|
|
def _compute_state(self):
|
|
"""Obtain request state."""
|
|
to_check = self.browse()
|
|
for one in self:
|
|
if not one.active:
|
|
one.state = "canceled"
|
|
continue
|
|
confirmed = False
|
|
for attendee in one.meeting_id.attendee_ids:
|
|
if attendee.partner_id == one.partner_id:
|
|
confirmed = attendee.state == "accepted"
|
|
break
|
|
if confirmed:
|
|
one.state = "confirmed"
|
|
to_check |= one
|
|
continue
|
|
one.state = "scheduled" if one.meeting_id else "pending"
|
|
to_check._check_scheduling()
|
|
|
|
@api.depends("meeting_id.start")
|
|
def _compute_start(self):
|
|
"""Get start date from related meeting, if available."""
|
|
for record in self:
|
|
if record.id:
|
|
record.start = record.meeting_id.start
|
|
|
|
@api.depends("meeting_id.duration", "type_id")
|
|
def _compute_duration(self):
|
|
"""Compute duration for each booking."""
|
|
for record in self:
|
|
# Special case when creating record from UI
|
|
if not record.id:
|
|
record.duration = self.default_get(["duration"]).get(
|
|
"duration", record.type_id.duration
|
|
)
|
|
# Get duration from type only when changing type or when creating from ORM
|
|
elif not record.duration or record._origin.type_id != record.type_id:
|
|
record.duration = record.type_id.duration
|
|
# Get it from meeting only when available
|
|
elif record.meeting_id:
|
|
record.duration = record.meeting_id.duration
|
|
|
|
@api.depends("start", "duration")
|
|
def _compute_stop(self):
|
|
"""Get stop date from start date and duration."""
|
|
for record in self:
|
|
try:
|
|
record.stop = record.start + timedelta(hours=record.duration)
|
|
except TypeError:
|
|
# Either value is False: no stop date
|
|
record.stop = False
|
|
|
|
@api.depends("meeting_id.user_id")
|
|
def _compute_user_id(self):
|
|
"""Get user from related meeting, if available."""
|
|
for record in self:
|
|
if record.meeting_id.user_id:
|
|
record.user_id = record.meeting_id.user_id
|
|
|
|
def _sync_meeting(self):
|
|
"""Lazy-create or destroy calendar.event."""
|
|
# Notify changed dates to attendees
|
|
_self = self.with_context(syncing_booking_ids=self.ids)
|
|
# Avoid sync recursion
|
|
_self -= self.browse(self.env.context.get("syncing_booking_ids"))
|
|
to_create, to_delete = [], _self.env["calendar.event"]
|
|
for one in _self:
|
|
if one.start:
|
|
resource_partners = one.combination_id.resource_ids.filtered(
|
|
lambda res: res.resource_type == "user"
|
|
).mapped("user_id.partner_id")
|
|
meeting_vals = dict(
|
|
alarm_ids=[(6, 0, one.type_id.alarm_ids.ids)],
|
|
categ_ids=[(6, 0, one.categ_ids.ids)],
|
|
description=one.type_id.requester_advice,
|
|
duration=one.duration,
|
|
location=one.location,
|
|
name=one.name
|
|
or one._get_name_formatted(one.partner_id, one.type_id),
|
|
partner_ids=[
|
|
(4, partner.id, 0)
|
|
for partner in one.partner_id | resource_partners
|
|
],
|
|
resource_booking_ids=[(6, 0, one.ids)],
|
|
start=one.start,
|
|
stop=one.stop,
|
|
user_id=one.user_id.id,
|
|
show_as="busy",
|
|
# These 2 avoid creating event as activity
|
|
res_model_id=False,
|
|
res_id=False,
|
|
)
|
|
if one.meeting_id:
|
|
meeting = one.meeting_id
|
|
if not all(
|
|
(
|
|
one.meeting_id.start == one.start,
|
|
one.meeting_id.stop == one.stop,
|
|
one.meeting_id.duration == one.duration,
|
|
)
|
|
):
|
|
# Context to notify scheduling change
|
|
meeting = meeting.with_context(from_ui=True)
|
|
meeting.write(meeting_vals)
|
|
else:
|
|
to_create.append(meeting_vals)
|
|
else:
|
|
to_delete |= one.meeting_id
|
|
to_delete.unlink()
|
|
_self.env["calendar.event"].create(to_create)
|
|
|
|
@api.constrains("combination_id", "meeting_id", "type_id")
|
|
def _check_scheduling(self):
|
|
"""Scheduled bookings must have no conflicts."""
|
|
# Nothing to do if no bookings are scheduled
|
|
has_meeting = self.filtered("meeting_id")
|
|
if not has_meeting:
|
|
return
|
|
# Ensure all scheduled bookings have booked some resources
|
|
has_rbc = self.filtered("combination_id.resource_ids")
|
|
missing_rbc = has_meeting - has_rbc
|
|
if missing_rbc:
|
|
raise ValidationError(
|
|
_(
|
|
"Cannot schedule these bookings because no resources "
|
|
"are selected for them:\n\n- %s"
|
|
)
|
|
% ("\n- ".join(missing_rbc.mapped("display_name")))
|
|
)
|
|
# Ensure all bookings fit in their type and resources calendars
|
|
unfitting_bookings = has_meeting
|
|
now = fields.Datetime.now()
|
|
for booking in has_meeting:
|
|
# Ignore if the event already happened
|
|
already_happened = booking.stop and booking.stop < now
|
|
if already_happened:
|
|
unfitting_bookings -= booking
|
|
continue
|
|
start_dt = fields.Datetime.context_timestamp(self, booking["start"])
|
|
end_dt = fields.Datetime.context_timestamp(self, booking["stop"])
|
|
available_intervals = booking._get_intervals(start_dt, end_dt)
|
|
if _availability_is_fitting(available_intervals, start_dt, end_dt):
|
|
unfitting_bookings -= booking
|
|
# Explain which bookings failed validation
|
|
if unfitting_bookings:
|
|
raise ValidationError(
|
|
_(
|
|
"Cannot schedule these bookings because they do not fit "
|
|
"in their type or resources calendars, or because "
|
|
"all resources are busy:\n\n- %s"
|
|
)
|
|
% "\n- ".join(unfitting_bookings.mapped("display_name"))
|
|
)
|
|
|
|
def _get_calendar_context(self, year=None, month=None, now=None):
|
|
"""Get the required context for the calendar view in the portal.
|
|
|
|
See the `resource_booking.scheduling_calendar` view.
|
|
|
|
:param int year: Year of the calendar to be displayed.
|
|
:param int month: Month of the calendar to be displayed.
|
|
:param datetime now: Represents the current datetime.
|
|
"""
|
|
month1 = relativedelta(months=1)
|
|
now = 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 = 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)
|
|
return {
|
|
"booking": self,
|
|
"calendar": calendar.Calendar(int(lang.week_start) - 1),
|
|
"now": now,
|
|
"res_lang": lang,
|
|
"slots": slots,
|
|
"start": start,
|
|
"weekday_names": weekday_names,
|
|
}
|
|
|
|
@api.model
|
|
def _get_name_formatted(self, partner, type_, meeting=None):
|
|
"""Produce a beautifully formatted name."""
|
|
values = {"partner": partner.display_name, "type": type_.display_name}
|
|
if meeting:
|
|
values["time"] = meeting.display_time
|
|
return _("%(partner)s - %(type)s - %(time)s") % values
|
|
return _("%(partner)s - %(type)s") % values
|
|
|
|
def _get_best_combination(self):
|
|
"""Pick best combination based on current booking state."""
|
|
# No dates? Then return whatever is already selected (can be empty)
|
|
if not self.start:
|
|
return self.combination_id
|
|
# If there's a combination already, put it 1st (highest priority)
|
|
sorted_combinations = self.combination_id + (
|
|
self.type_id._get_combinations_priorized() - self.combination_id
|
|
)
|
|
start_dt = fields.Datetime.context_timestamp(self, self.start)
|
|
end_dt = fields.Datetime.context_timestamp(self, self.stop)
|
|
# Get 1st combination available in the desired interval
|
|
for combination in sorted_combinations:
|
|
available_intervals = self._get_intervals(start_dt, end_dt, combination)
|
|
if _availability_is_fitting(available_intervals, start_dt, end_dt):
|
|
return combination
|
|
# Tell portal user there's no combination available
|
|
if self.env.context.get("using_portal"):
|
|
hours = (self.stop - self.start).total_seconds() / 3600
|
|
raise ValidationError(
|
|
_("No resource combinations available on %s")
|
|
% self.env["calendar.event"]._get_display_time(
|
|
self.start, self.stop, hours, False
|
|
)
|
|
)
|
|
|
|
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)
|
|
booking_duration = timedelta(hours=self.duration)
|
|
current = 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
|
|
return result
|
|
|
|
def _get_intervals(self, start_dt, end_dt, combination=None):
|
|
"""Get available intervals for this booking."""
|
|
# Get all intervals except those from current booking
|
|
try:
|
|
booking_id = self.id or self._origin.id or -1
|
|
except AttributeError:
|
|
booking_id = -1
|
|
# Detached compatibility with hr_holidays_public
|
|
booking = self.with_context(
|
|
analyzing_booking=booking_id, exclude_public_holidays=True
|
|
)
|
|
# RBT calendar uses no resources to restrict bookings
|
|
result = booking.type_id.resource_calendar_id._work_intervals(start_dt, end_dt)
|
|
# Restrict with the chosen combination, or to at least one of the
|
|
# available ones
|
|
combinations = (
|
|
combination
|
|
or booking.combination_id
|
|
or booking.mapped("type_id.combination_rel_ids.combination_id")
|
|
).with_context(analyzing_booking=booking_id)
|
|
result &= combinations._get_intervals(start_dt, end_dt)
|
|
return result
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
"""Sync booking with meeting if needed."""
|
|
result = super().create(vals_list)
|
|
result._sync_meeting()
|
|
return result
|
|
|
|
def write(self, vals):
|
|
"""Sync booking with meeting if needed."""
|
|
# On a database with lots of recurrent calendar events we could get serious
|
|
# performance downgrades. As we'll be computing them with _sync_meeting later
|
|
# we'll be avoiding triggering on the super call (i.e. compute methods)
|
|
result = super(ResourceBooking, self.with_context(virtual_id=False)).write(vals)
|
|
self._sync_meeting()
|
|
return result
|
|
|
|
def unlink(self):
|
|
"""Unlink meeting if needed."""
|
|
self.meeting_id.unlink()
|
|
return super().unlink()
|
|
|
|
def name_get(self):
|
|
"""Autogenerate booking name if none is provided."""
|
|
old = super().name_get()
|
|
new = []
|
|
for id_, name in old:
|
|
record = self.browse(id_)
|
|
if self.env.context.get("using_portal"):
|
|
# ID optionally suffixed with custom name for portal users
|
|
template = _("# %(id)d - %(name)s") if record.name else _("# %(id)d")
|
|
name = template % {"id": id_, "name": name}
|
|
elif not record.name:
|
|
# Automatic name for backend users
|
|
name = self._get_name_formatted(
|
|
record.partner_id, record.type_id, record.meeting_id
|
|
)
|
|
new.append((id_, name))
|
|
return new
|
|
|
|
def _message_auto_subscribe_followers(self, updated_values, default_subtype_ids):
|
|
"""Auto-subscribe and notify resource partners."""
|
|
result = super()._message_auto_subscribe_followers(
|
|
updated_values, default_subtype_ids
|
|
)
|
|
combination = (
|
|
self.env["resource.booking.combination"]
|
|
.sudo()
|
|
.browse(updated_values.get("combination_id"))
|
|
)
|
|
resource_partners = combination.mapped(
|
|
"resource_ids.user_id.partner_id"
|
|
).filtered("active")
|
|
for partner in resource_partners:
|
|
result.append(
|
|
(
|
|
partner.id,
|
|
default_subtype_ids,
|
|
"resource_booking.message_combination_assigned"
|
|
if partner != self.env.user.partner_id
|
|
else False,
|
|
)
|
|
)
|
|
return result
|
|
|
|
def _message_get_suggested_recipients(self):
|
|
"""Suggest related partners."""
|
|
recipients = super()._message_get_suggested_recipients()
|
|
for record in self:
|
|
record._message_add_suggested_recipient(
|
|
recipients,
|
|
partner=record.partner_id,
|
|
reason=self._fields["partner_id"].string,
|
|
)
|
|
return recipients
|
|
|
|
def action_schedule(self):
|
|
"""Redirect user to a simpler way to schedule this booking."""
|
|
FloatTimeParser = self.env["ir.qweb.field.float_time"]
|
|
return {
|
|
"context": dict(
|
|
self.env.context,
|
|
# These 2 avoid creating event as activity
|
|
default_res_model_id=False,
|
|
default_res_id=False,
|
|
# Context used by web_calendar_slot_duration module
|
|
calendar_slot_duration=FloatTimeParser.value_to_html(
|
|
self.duration, False
|
|
),
|
|
default_resource_booking_ids=[(6, 0, self.ids)],
|
|
default_name=self.name,
|
|
),
|
|
"name": _("Schedule booking"),
|
|
"res_model": "calendar.event",
|
|
"target": "self",
|
|
"type": "ir.actions.act_window",
|
|
"view_mode": "calendar,list,form",
|
|
}
|
|
|
|
def action_confirm(self):
|
|
"""Confirm own and requesting partner's attendance."""
|
|
attendees_to_confirm = self.env["calendar.attendee"]
|
|
confirm_always = self.env["res.partner"]
|
|
if self.env.context.get("confirm_own_attendance"):
|
|
confirm_always |= self.env.user.partner_id
|
|
# Avoid wasted state recomputes
|
|
for booking in self:
|
|
if not booking.meeting_id:
|
|
continue
|
|
# Make sure requester and user resources are meeting attendees
|
|
booking.meeting_id.partner_ids |= booking.partner_id | booking.mapped(
|
|
"combination_id.resource_ids.user_id.partner_id"
|
|
)
|
|
# Find meeting attendees that should be confirmed
|
|
partners_to_confirm = confirm_always | booking.partner_id
|
|
for attendee in booking.meeting_id.attendee_ids:
|
|
if attendee.partner_id & partners_to_confirm:
|
|
# attendee.state='accepted'
|
|
attendees_to_confirm |= attendee
|
|
attendees_to_confirm.write({"state": "accepted"})
|
|
|
|
def action_unschedule(self):
|
|
"""Remove associated meetings."""
|
|
self.mapped("meeting_id").unlink()
|
|
# Force recomputing, in case meeting_id is not visible in the form
|
|
self.write({"meeting_id": False})
|
|
|
|
def action_cancel(self):
|
|
"""Cancel this booking."""
|
|
# Remove related meeting
|
|
self.action_unschedule()
|
|
# Archive and reset access token
|
|
self.write({"active": False, "access_token": False})
|
|
|
|
def action_open_portal(self):
|
|
return {
|
|
"target": "self",
|
|
"type": "ir.actions.act_url",
|
|
"url": self.get_portal_url(),
|
|
}
|