odex30_standard/odex30_base/resource_booking/tests/test_backend.py

970 lines
39 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).
from datetime import date, datetime
from unittest.mock import patch
from freezegun import freeze_time
from pytz import utc
from odoo import fields
from odoo.exceptions import ValidationError
from odoo.tests.common import Form, SavepointCase, new_test_user, users
from odoo.addons.resource.models.resource import Intervals
from odoo.addons.resource_booking.models.resource_booking import (
_availability_is_fitting,
)
from .common import create_test_data
_2dt = fields.Datetime.to_datetime
@freeze_time("2021-02-26 09:00:00", tick=True) # Last Friday of February
class BackendCase(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
create_test_data(cls)
cls.plain_user = new_test_user(cls.env, login="plain", groups="base.group_user")
@users("plain")
def test_plain_user_calendar_event(self):
"""Check that a simple user is able to handle manual calendar events."""
event = self.env["calendar.event"].create(
{
"name": "Test calendar event",
"start": "2023-01-01 00:00:00",
"stop": "2023-01-01 01:00:00",
}
)
event.write({"partner_ids": [(4, self.partner.id)]})
event.unlink()
def test_scheduling_conflict_constraints(self):
# Combination is available on Mondays and Tuesdays
rbc_montue = self.rbcs[2]
# Type is available on Mondays
cal_mon = self.r_calendars[0]
self.rbt.resource_calendar_id = cal_mon
# Booking cannot be placed next Tuesday
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-02 08:00:00",
"type_id": self.rbt.id,
"combination_id": rbc_montue.id,
"combination_auto_assign": False,
}
)
# Booking cannot be placed next Monday before 8:00
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-02 07:45:00",
"type_id": self.rbt.id,
"combination_id": rbc_montue.id,
"combination_auto_assign": False,
}
)
# Booking cannot be placed next Monday after 17:00
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-02 16:45:00",
"type_id": self.rbt.id,
"combination_id": rbc_montue.id,
"combination_auto_assign": False,
}
)
# Booking can be placed next Monday
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
"combination_id": rbc_montue.id,
"combination_auto_assign": False,
}
)
# Another event cannot collide with the same RBC
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:29:59",
"type_id": self.rbt.id,
"combination_id": rbc_montue.id,
"combination_auto_assign": False,
}
)
# Another event can collide with another RBC
rbc_mon = self.rbcs[0]
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
"combination_id": rbc_mon.id,
"combination_auto_assign": False,
}
)
def test_scheduling_constraints_span_two_days(self):
# Booking can span across two calendar days.
cal_frisun = self.r_calendars[3]
rbc_frisun = self.rbcs[3]
self.rbt.resource_calendar_id = cal_frisun
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-06 23:00:00",
"duration": 2,
"type_id": self.rbt.id,
"combination_id": rbc_frisun.id,
"combination_auto_assign": False,
}
)
# Booking cannot overlap.
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-06 22:00:00",
"duration": 4,
"type_id": self.rbt.id,
"combination_id": rbc_frisun.id,
"combination_auto_assign": False,
}
)
# Test a case where there is an overlap, but the conflict happens at
# 00:00 exactly.
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-14 00:00:00",
"duration": 1,
"type_id": self.rbt.id,
"combination_id": rbc_frisun.id,
"combination_auto_assign": False,
}
)
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-13 23:00:00",
"duration": 4,
"type_id": self.rbt.id,
"combination_id": rbc_frisun.id,
"combination_auto_assign": False,
}
)
# If there are too many minutes between the end and start of the two
# dates, the booking cannot be contiguous.
cal_frisun.attendance_ids.write({"hour_to": 23.96}) # 23:58
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-20 23:00:00",
"duration": 2,
"type_id": self.rbt.id,
"combination_id": rbc_frisun.id,
"combination_auto_assign": False,
}
)
def test_scheduling_constraints_span_three_days(self):
# Booking can span across two calendar days.
cal_frisun = self.r_calendars[3]
rbc_frisun = self.rbcs[3]
self.rbt.resource_calendar_id = cal_frisun
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-05 23:00:00",
"duration": 24 * 2,
"type_id": self.rbt.id,
"combination_id": rbc_frisun.id,
"combination_auto_assign": False,
}
)
def test_availability_is_fitting_malformed_date_skip(self):
"""Test a case for malformed data where a date is skipped in the
available_intervals list of tuples.
"""
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, 3, 0, 0), datetime(2021, 3, 3, 18, 0), recset),
]
available_intervals = Intervals(tuples)
self.assertTrue(
_availability_is_fitting(
available_intervals,
datetime(2021, 3, 1, 18, 0),
datetime(2021, 3, 3, 18, 0),
)
)
# Skip a day by removing it.
tuples.pop(1)
available_intervals = Intervals(tuples)
self.assertFalse(
_availability_is_fitting(
available_intervals,
datetime(2021, 3, 1, 18, 0),
datetime(2021, 3, 3, 18, 0),
)
)
def test_rbc_forced_calendar(self):
# Type is available on Mondays
cal_mon = self.r_calendars[0]
self.rbt.resource_calendar_id = cal_mon
# Cannot book an combination with resources that only work on Tuesdays
rbc_tue = self.rbcs[1]
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
"combination_id": rbc_tue.id,
"combination_auto_assign": False,
}
)
# However, if the combination is forced to Mondays, you can book it
rbc_tue.forced_calendar_id = cal_mon
rb = self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
"combination_auto_assign": False,
"combination_id": rbc_tue.id,
}
)
self.assertEqual(rb.combination_id, rbc_tue)
def test_booking_from_calendar_view(self):
# The type is configured by default with bookings of 30 minutes
self.assertEqual(self.rbt.duration, 0.5)
# Change it to 45 minutes
self.rbt.duration = 0.75
# Bookings smart button configures calendar with slots of 45 minutes
button_context = self.rbt.action_open_bookings()["context"]
self.assertEqual(button_context["calendar_slot_duration"], "00:45")
self.assertEqual(button_context["default_duration"], 0.75)
# When you click & drag on calendar to create an event, it adds the
# start and duration as default; we imitate that here to book a meeting
# with 2 slots next monday
button_context["default_duration"] = 1.5
booking_form = Form(
self.env["resource.booking"].with_context(
**button_context,
default_start="2021-03-01 08:00:00",
)
)
# This might seem redundant, but makes sure onchanges don't mess stuff
self.assertEqual(_2dt(booking_form.start), datetime(2021, 3, 1, 8))
self.assertEqual(booking_form.duration, 1.5)
self.assertEqual(_2dt(booking_form.stop), datetime(2021, 3, 1, 9, 30))
# If I change to next week's monday, then the stop date advances 1:30h
booking_form.start = datetime(2021, 3, 8, 8)
booking_form.partner_id = self.partner
self.assertEqual(_2dt(booking_form.start), datetime(2021, 3, 8, 8))
self.assertEqual(booking_form.duration, 1.5)
self.assertEqual(_2dt(booking_form.stop), datetime(2021, 3, 8, 9, 30))
# I can book it (which means type & combination were autofilled)
booking = booking_form.save()
self.assertTrue(booking.meeting_id)
self.assertEqual(booking.state, "scheduled")
def test_dates_inverse(self):
"""Start & stop fields are computed with inverse. Test their workflow."""
# Set type to be available only on mondays
self.rbt.resource_calendar_id = self.r_calendars[0]
# Create a booking from scratch
booking_form = Form(self.env["resource.booking"])
booking_form.type_id = self.rbt
booking_form.partner_id = self.partner
self.assertFalse(booking_form.start)
self.assertFalse(booking_form.stop)
self.assertFalse(booking_form.combination_id)
# I can save it without booking
booking = booking_form.save()
self.assertEqual(booking.state, "pending")
self.assertFalse(booking.meeting_id)
self.assertFalse(booking.start)
self.assertFalse(booking.stop)
self.assertFalse(booking.combination_id)
# I edit it again
with Form(booking) as booking_form:
# Start next Tuesday: updates stop; no combination available
booking_form.start = datetime(2021, 3, 2, 8)
self.assertEqual(_2dt(booking_form.stop), datetime(2021, 3, 2, 8, 30))
self.assertFalse(booking_form.combination_id)
# Move to Monday: updates stop; found one combination available
booking_form.start = datetime(2021, 3, 1, 8)
self.assertEqual(_2dt(booking_form.stop), datetime(2021, 3, 1, 8, 30))
self.assertTrue(booking_form.combination_id)
self.assertEqual(booking.state, "scheduled")
self.assertTrue(booking.meeting_id)
self.assertTrue(booking.start)
self.assertTrue(booking.stop)
self.assertTrue(booking.combination_id)
def test_state(self):
# I create a pending booking
booking = self.env["resource.booking"].create(
{"type_id": self.rbt.id, "partner_id": self.partner.id}
)
# Without dates, it's pending
self.assertEqual(booking.state, "pending")
self.assertTrue(booking.active)
self.assertFalse(booking.meeting_id)
self.assertFalse(booking.start)
self.assertFalse(booking.stop)
self.assertFalse(booking.combination_id)
# With a linked meeting, it's scheduled
with Form(booking) as booking_form:
booking_form.start = datetime(2021, 3, 1, 8)
meeting = booking.meeting_id
self.assertEqual(booking.state, "scheduled")
self.assertTrue(booking.active)
self.assertTrue(meeting.exists())
self.assertTrue(booking.start)
self.assertTrue(booking.stop)
self.assertTrue(booking.combination_id)
# When partner confirms attendance, it's confirmed
booker_attendance = meeting.attendee_ids.filtered(
lambda one: one.partner_id == booking.partner_id
)
self.assertTrue(booker_attendance)
booker_attendance.do_accept()
self.assertEqual(booking.state, "confirmed")
self.assertTrue(booking.active)
self.assertTrue(meeting.exists())
self.assertTrue(booking.start)
self.assertTrue(booking.stop)
self.assertTrue(booking.combination_id)
# Without dates, it's pending again
booking.action_unschedule()
self.assertEqual(booking.state, "pending")
self.assertTrue(booking.active)
self.assertFalse(meeting.exists())
self.assertFalse(booking.start)
self.assertFalse(booking.stop)
self.assertTrue(booking.combination_id)
# Archived and without dates, it's canceled
booking.action_cancel()
self.assertEqual(booking.state, "canceled")
self.assertFalse(booking.active)
self.assertFalse(meeting.exists())
self.assertFalse(booking.start)
self.assertFalse(booking.stop)
self.assertTrue(booking.combination_id)
def test_sorted_assignment(self):
"""Set sorted assignment on RBT and test it works correctly."""
rbc_mon, rbc_tue, rbc_montue, rbc_frisun = self.rbcs
with Form(self.rbt) as rbt_form:
rbt_form.combination_assignment = "sorted"
# Book next monday at 10:00
rb1_form = Form(self.env["resource.booking"])
rb1_form.type_id = self.rbt
rb1_form.partner_id = self.partner
rb1_form.start = datetime(2021, 3, 1, 10)
self.assertEqual(rb1_form.combination_id, rbc_mon)
rb1 = rb1_form.save()
self.assertEqual(rb1.combination_id, rbc_mon)
# Another booking, same time
rb2_form = Form(self.env["resource.booking"])
rb2_form.type_id = self.rbt
rb2_form.partner_id = self.partner
rb2_form.start = datetime(2021, 3, 1, 10)
self.assertEqual(rb2_form.combination_id, rbc_montue)
rb2 = rb2_form.save()
self.assertEqual(rb2.combination_id, rbc_montue)
# I'm able to alter rb1 timing
with Form(rb1) as rb1_form:
rb1_form.start = datetime(2021, 3, 2, 10)
self.assertEqual(rb1_form.combination_id, rbc_tue)
self.assertEqual(rb1.combination_id, rbc_tue)
def test_calendar_meeting_and_leave_combined(self):
"""Resource not bookable on calendar leave."""
cal_mon = self.r_calendars[0]
res_mon = self.r_users[0]
# Add leave next Monday for Mon resource
self.env["resource.calendar.leaves"].create(
{
"date_from": datetime(2021, 3, 1),
"date_to": datetime(2021, 3, 3),
"calendar_id": cal_mon.id,
"resource_id": res_mon.id,
}
)
# Add meeting same day for all resources, so no combination is available
self.env["calendar.event"].create(
{
"start": datetime(2021, 3, 1, 8),
"stop": datetime(2021, 3, 1, 10, 30),
"name": "some meeting",
"partner_ids": [(6, 0, self.users.partner_id.ids)],
}
)
# Check it's not bookable
rb_form = Form(self.env["resource.booking"])
rb_form.type_id = self.rbt
rb_form.partner_id = self.partner
# No combination found
rb_form.start = datetime(2021, 3, 1, 10)
self.assertFalse(rb_form.combination_id)
# Combination found
rb_form.start = datetime(2021, 3, 8, 10)
self.assertTrue(rb_form.combination_id)
rb_form.save()
def test_same_slot_twice_not_utc(self):
"""Scheduling the same slot twice fails, when not in UTC."""
for loop in range(2):
rb_f = Form(self.env["resource.booking"].with_context(tz="Europe/Madrid"))
rb_f.partner_id = self.partner
rb_f.type_id = self.rbt
rb_f.start = datetime(2021, 3, 1, 10)
rb_f.combination_auto_assign = False
rb_f.combination_id = self.rbcs[0]
# 1st one works
if loop == 0:
rb = rb_f.save()
self.assertEqual(rb.state, "scheduled")
else:
with self.assertRaises(ValidationError):
rb_f.save()
def test_recurring_event(self):
"""Recurrent events are considered."""
# Everyone busy past and next Mondays with a recurring meeting
ce_f = Form(self.env["calendar.event"])
ce_f.name = "recurring event past monday"
for user in self.users:
ce_f.partner_ids.add(user.partner_id)
ce_f.start = datetime(2021, 2, 22, 8)
ce_f.duration = 1
ce_f.recurrency = True
ce_f.interval = 1
ce_f.rrule_type = "weekly"
ce_f.end_type = "count"
ce_f.count = 2
ce_f.mo = True
ce_f.save()
# Cannot book next Monday at 8
rb_f = Form(self.env["resource.booking"])
rb_f.partner_id = self.partner
rb_f.type_id = self.rbt
# No RBC when starting
self.assertFalse(rb_f.combination_id)
# No RBC available next Monday at 8
rb_f.start = datetime(2021, 3, 1, 8)
self.assertFalse(rb_f.combination_id)
# Everyone's free at 9
rb_f.start = datetime(2021, 3, 1, 9)
self.assertTrue(rb_f.combination_id)
def test_change_calendar_after_bookings_exist(self):
"""Calendar changes can be done only if they introduce no conflicts."""
rbc_mon = self.rbcs[0]
cal_mon = self.r_calendars[0]
# There's a booking for last monday
past_booking = self.env["resource.booking"].create(
{
"combination_id": rbc_mon.id,
"partner_id": self.partner.id,
"start": "2021-02-22 08:00:00",
"type_id": self.rbt.id,
}
)
past_booking.action_confirm()
self.assertEqual(past_booking.duration, 0.5)
self.assertEqual(past_booking.state, "confirmed")
# There's another one for next monday, confirmed too
future_booking = self.env["resource.booking"].create(
{
"combination_id": rbc_mon.id,
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
}
)
future_booking.action_confirm()
self.assertEqual(future_booking.state, "confirmed")
# Now, it's impossible for me to change the resource calendar
with self.assertRaises(ValidationError), self.env.cr.savepoint():
with Form(cal_mon) as cal_mon_f:
with cal_mon_f.attendance_ids.edit(0) as att_mon_f:
att_mon_f.hour_from = 9
# But let's unconfirm future boooking
future_booking.action_unschedule()
with Form(future_booking) as future_booking_f:
future_booking_f.start = "2021-03-01 08:00:00"
self.assertEqual(future_booking.state, "scheduled")
# Now I should be able to change the resource calendar
with Form(cal_mon) as cal_mon_f:
with cal_mon_f.attendance_ids.edit(0) as att_mon_f:
att_mon_f.hour_from = 9
# However, now I shouldn't be able to confirm future booking
with self.assertRaises(ValidationError), self.env.cr.savepoint():
future_booking.action_confirm()
def test_notification_tz(self):
"""Mail notification TZ is the same as resource.booking.type always."""
# Configure RBT with Madrid calendar, but partner has other TZ
self.r_calendars.write({"tz": "Europe/Madrid"})
self.partner.tz = "Australia/Sydney"
rb = self.env["resource.booking"].create(
{
"combination_id": self.rbcs[0].id,
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00", # 09:00 in Madrid
"type_id": self.rbt.id,
}
)
rb.action_confirm()
invitation_mail = self.env["mail.mail"].search(
[
("state", "=", "outgoing"),
(
"subject",
"=",
"Invitation to some customer - Test resource booking type",
),
]
)
# Invitation must display Madrid TZ (CET)
self.assertIn("09:00:00 CET", invitation_mail.body)
def test_free_slots_with_different_type_and_booking_durations(self):
"""Slot and booking duration are different, and all works."""
# Type and calendar allow one slot each 30 minutes on Mondays and
# Tuesdays from 08:00 to 17:00 UTC. The booking will span for 3 slots.
rb = self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"type_id": self.rbt.id,
"duration": self.rbt.duration * 3,
}
)
self.assertEqual(rb.duration, 1.5)
slots = rb._get_available_slots(
utc.localize(datetime(2021, 3, 2, 14, 15)),
utc.localize(datetime(2021, 3, 8, 10)),
)
self.assertEqual(
slots,
{
# Thursday
date(2021, 3, 2): [
# We start searching at 14:15, so first free slot will
# start at 14:30
utc.localize(datetime(2021, 3, 2, 14, 30)),
utc.localize(datetime(2021, 3, 2, 15)),
# Booking duration is 1:30, and calendar ends at 17:00, so
# last slot starts at 15:30
utc.localize(datetime(2021, 3, 2, 15, 30)),
],
# Next Monday, because calendar only allows Mondays and Tuesdays
date(2021, 3, 8): [
# Calendar starts at 8:00
utc.localize(datetime(2021, 3, 8, 8)),
# We are searching until 10:00, so last free slot is at 8:30
utc.localize(datetime(2021, 3, 8, 8, 30)),
],
},
)
def test_location(self):
"""Location across records works as expected."""
rbt2 = self.rbt.copy({"location": "Office 2"})
rb_f = Form(self.env["resource.booking"])
rb_f.partner_id = self.partner
rb_f.type_id = self.rbt
rb = rb_f.save()
# Pending booking inherits location from type
self.assertEqual(rb.state, "pending")
self.assertEqual(rb.location, "Main office")
# Booking can change location independently now
with Form(rb) as rb_f:
rb_f.location = "Office 3"
self.assertEqual(self.rbt.location, "Main office")
self.assertEqual(rb.location, "Office 3")
# Changing booking type changes location
with Form(rb) as rb_f:
rb_f.type_id = rbt2
self.assertEqual(rb.location, "Office 2")
# Still can change it independently
with Form(rb) as rb_f:
rb_f.location = "Office 1"
self.assertEqual(rb.location, "Office 1")
self.assertEqual(rbt2.location, "Office 2")
# Schedule the booking, meeting inherits location from it
with Form(rb) as rb_f:
rb_f.start = "2021-03-01 08:00:00"
self.assertEqual(rb.state, "scheduled")
self.assertEqual(rb.location, "Office 1")
self.assertEqual(rb.meeting_id.location, "Office 1")
# Changing meeting location changes location of booking
with Form(rb.meeting_id) as meeting_f:
meeting_f.location = "Office 2"
self.assertEqual(rb.location, "Office 2")
self.assertEqual(rb.meeting_id.location, "Office 2")
# Changing booking location changes meeting location
with Form(rb) as rb_f:
rb_f.location = "Office 3"
self.assertEqual(rb.meeting_id.location, "Office 3")
self.assertEqual(rb.location, "Office 3")
# When unscheduled, it keeps location untouched
rb.action_unschedule()
self.assertFalse(rb.meeting_id)
self.assertEqual(rb.location, "Office 3")
def test_organizer_sync(self):
"""Resource booking and meeting organizers are properly synced."""
rb = self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"type_id": self.rbt.id,
"start": "2021-03-01 08:00:00",
"duration": 1.5,
}
)
self.assertEqual(rb.user_id, self.env.user)
self.assertEqual(rb.meeting_id.user_id, self.env.user)
rb.meeting_id.user_id = self.users[1]
self.assertEqual(rb.user_id, self.users[1])
self.assertEqual(rb.meeting_id.user_id, self.users[1])
def test_resource_booking_display_name(self):
# Pending booking with no name
rb = self.env["resource.booking"].create(
{"partner_id": self.partner.id, "type_id": self.rbt.id}
)
self.assertEqual(rb.display_name, "some customer - Test resource booking type")
self.assertEqual(
rb.with_context(using_portal=True).display_name, "# %d" % rb.id
)
# Pending booking with name
rb.name = "changed"
self.assertEqual(rb.display_name, "changed")
self.assertEqual(
rb.with_context(using_portal=True).display_name, "# %d - changed" % rb.id
)
# Scheduled booking with name
rb.start = "2021-03-01 08:00:00"
self.assertEqual(rb.display_name, "changed")
self.assertEqual(
rb.with_context(using_portal=True).display_name, "# %d - changed" % rb.id
)
# Scheduled booking with no name
rb.name = False
self.assertEqual(
rb.display_name,
"some customer - Test resource booking type "
"- 03/01/2021 at (08:00:00 To 08:30:00) (UTC)",
)
self.assertEqual(
rb.with_context(using_portal=True).display_name, "# %d" % rb.id
)
def test_attendee_autoassigned_not_autoconfirmed(self):
"""Meeting attendees are not autoconfirmed when combination is autoassigned."""
# Create an auto-assigned booking
rb = self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"type_id": self.rbt.id,
"start": "2021-03-01 08:00:00",
}
)
# Get attendees that belong to the combination human resource
resource_partner = rb.combination_id.resource_ids.user_id.partner_id
resource_attendees = rb.meeting_id.attendee_ids.filtered(
lambda one: one.partner_id == resource_partner
)
# Combination was auto-assigned, so resource attendees are not confirmed
self.assertEqual(resource_attendees.state, "needsAction")
def test_attendee_not_autoassigned_autoconfirmed(self):
"""Meeting attendees are auto-confirmed when assigned by hand."""
# Create a booking with handpicked combination assignment
rb = self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"type_id": self.rbt.id,
"start": "2021-03-01 08:00:00",
"combination_auto_assign": False,
"combination_id": self.rbcs[0].id,
}
)
# Get attendees that belong to the combination human resources
resource_partner = self.users[0].partner_id
resource_attendees = rb.meeting_id.attendee_ids.filtered(
lambda one: one.partner_id == resource_partner
)
# Combination was handpicked, so resource attendees are auto-confirmed
self.assertEqual(resource_attendees.state, "accepted")
def test_suggested_and_subscribed_recipients(self):
self.env = self.env(context=dict(self.env.context, tracking_disable=False))
# Create a booking as a new user
rb_user = new_test_user(
self.env, login="rbu", groups="base.group_user,resource_booking.group_user"
)
# Enable auto-subscription messaging
with patch.object(self.env.registry, "ready", True):
rb = (
self.env["resource.booking"]
.with_user(rb_user)
.sudo()
.create(
{
"partner_id": self.partner.id,
"type_id": self.rbt.id,
"combination_auto_assign": False,
"combination_id": self.rbcs[0].id,
"user_id": self.users[1].id,
}
)
)
# Organizer, combination and creator must already be following
self.assertEqual(
rb.message_partner_ids, rb_user.partner_id | self.users[:2].partner_id
)
# Requester and combination must be suggested
self.assertEqual(
rb._message_get_suggested_recipients(),
{rb.id: [(rb.partner_id.id, "some customer", "Requester")]},
)
def test_creating_rbt_has_tags(self):
"""Creating booking works if type has tags."""
categ = self.env["calendar.event.type"].create({"name": "test tag"})
self.rbt.categ_ids = categ
rb_f = Form(self.env["resource.booking"])
rb_f.partner_id = self.partner
rb_f.type_id = self.rbt
rb = rb_f.save()
self.assertEqual(rb.categ_ids, categ)
def test_event_show_as_free(self):
"""Don't mind about event owner.
Here I'll create 2 overlapping events. Since I create both, I'll be the
owner of both automatically. However, there are 2 RBC available (one is
me), so I still should be able to create 2 events.
"""
user = self.users[0]
rb_obj = self.env["resource.booking"].with_context(tracking_disable=True)
# I'm the last option
self.rbt.combination_assignment = "sorted"
self.rbt.combination_rel_ids[0].sequence = 10
# Create one long event on Monday, where there are 2 RBC available (one is me)
rb_f = Form(rb_obj)
rb_f.type_id = self.rbt
rb_f.start = "2021-03-01 09:00:00"
rb_f.duration = 1
rb_f.partner_id = self.partner
rb1 = rb_f.save()
# I'm not booked, so I'm free
self.assertEqual(rb1.combination_id, self.rbcs[2])
self.assertNotIn(user.partner_id, rb1.meeting_id.partner_ids)
# Create another event within the previous one
rb_f = Form(rb_obj)
rb_f.type_id = self.rbt
rb_f.start = "2021-03-01 09:00:00"
rb_f.duration = 1.5
rb_f.partner_id = self.partner.copy()
# Saving works because I'm free
rb2 = rb_f.save()
# I'm booked this time, so I'm busy
self.assertEqual(rb2.combination_id, self.rbcs[0])
self.assertIn(user.partner_id, rb2.meeting_id.partner_ids)
# Thus, it will fail without available resources on a next one
rb_f = Form(rb_obj)
rb_f.type_id = self.rbt
rb_f.start = "2021-03-01 09:30:00"
rb_f.duration = 0.5
rb_f.partner_id = self.partner.copy()
with self.assertRaises(AssertionError):
rb_f.save()
def test_resource_is_available(self):
"""If a resource is involved in a booking or is not active at any point
between two datetimes, then it is unavailable.
"""
rbc_montue = self.rbcs[2]
resource = rbc_montue.resource_ids[1]
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
"combination_id": rbc_montue.id,
"combination_auto_assign": False,
}
)
# Resource is available on Monday at an unoccupied time.
self.assertTrue(
resource.is_available(
utc.localize(datetime(2021, 3, 1, 10, 0)),
utc.localize(datetime(2021, 3, 1, 11, 0)),
)
)
# Resource is not available on Monday at an occupied time (longer than
# booking).
self.assertFalse(
resource.is_available(
utc.localize(datetime(2021, 3, 1, 7, 45)),
utc.localize(datetime(2021, 3, 1, 8, 45)),
)
)
# Resource is not available on Monday at an occupied time (within
# booking time).
self.assertFalse(
resource.is_available(
utc.localize(datetime(2021, 3, 1, 8, 10)),
utc.localize(datetime(2021, 3, 1, 8, 20)),
)
)
# Resource is not available on Monday at an occupied time (partially
# overlaps booking).
self.assertFalse(
resource.is_available(
utc.localize(datetime(2021, 3, 1, 8, 15)),
utc.localize(datetime(2021, 3, 1, 8, 45)),
)
)
# Resource is not available on Wednesdays.
self.assertFalse(
resource.is_available(
utc.localize(datetime(2021, 3, 3, 10, 0)),
utc.localize(datetime(2021, 3, 3, 11, 0)),
)
)
def test_resource_is_available_span_days(self):
# Correctly handle bookings that span across midnight.
cal_satsun = self.r_calendars[3]
rbc_satsun = self.rbcs[3]
resource = rbc_satsun.resource_ids[1]
self.rbt.resource_calendar_id = cal_satsun
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-06 23:00:00",
"duration": 2,
"type_id": self.rbt.id,
"combination_id": rbc_satsun.id,
"combination_auto_assign": False,
}
)
self.assertFalse(
resource.is_available(
utc.localize(datetime(2021, 3, 6, 22, 0)),
utc.localize(datetime(2021, 3, 7, 2, 0)),
)
)
# Resource is available on the next weekend.
self.assertTrue(
resource.is_available(
utc.localize(datetime(2021, 3, 13, 22, 0)),
utc.localize(datetime(2021, 3, 14, 2, 0)),
)
)
def test_assign_multiple_humans(self):
"""
It should be possible to assign multiple humans to a booking.
"""
user_4 = self.env["res.users"].create(
{
"email": "user_4@example.com",
"login": "user_4",
"name": "User 4",
}
)
r_user_4 = self.env["resource.resource"].create(
{
"calendar_id": self.r_calendars[0].id,
"name": user_4.name,
"resource_type": "user",
"tz": "UTC",
"user_id": user_4.id,
}
)
rbc = self.env["resource.booking.combination"].create(
{
"resource_ids": [(6, 0, [self.r_users[0].id, r_user_4.id])],
"type_rel_ids": [(6, 0, [self.rbt.id])],
}
)
self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
"combination_id": rbc.id,
"combination_auto_assign": False,
}
)
def test_change_combination_to_multiple_humans(self):
"""
It should be possible to change the combination of a booking to
another combination that contains multiple humans.
"""
user_4 = self.env["res.users"].create(
{
"email": "user_4@example.com",
"login": "user_4",
"name": "User 4",
}
)
r_user_4 = self.env["resource.resource"].create(
{
"calendar_id": self.r_calendars[0].id,
"name": user_4.name,
"resource_type": "user",
"tz": "UTC",
"user_id": user_4.id,
}
)
rbc_1 = self.env["resource.booking.combination"].create(
{
"resource_ids": [(6, 0, [self.r_materials[0].id])],
"type_rel_ids": [(6, 0, [self.rbt.id])],
}
)
rbc_2 = self.env["resource.booking.combination"].create(
{
"resource_ids": [(6, 0, [self.r_users[0].id, r_user_4.id])],
"type_rel_ids": [(6, 0, [self.rbt.id])],
}
)
booking = self.env["resource.booking"].create(
{
"partner_id": self.partner.id,
"start": "2021-03-01 08:00:00",
"type_id": self.rbt.id,
"combination_id": rbc_1.id,
}
)
booking.combination_id = rbc_2