390 lines
19 KiB
Python
390 lines
19 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import datetime, timedelta
|
|
import pytz
|
|
|
|
from odoo.addons.mail.tests.common import mail_new_test_user
|
|
from odoo.tests import Form, users
|
|
from odoo.tools.intervals import Intervals
|
|
from .common import AppointmentCommon
|
|
|
|
|
|
class AppointmentGanttTestCommon(AppointmentCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.partners = cls.env['res.partner'].create([{
|
|
'name': 'gantt attendee 1'
|
|
}, {
|
|
'name': 'gantt attendee 2'
|
|
}])
|
|
|
|
# create some appointments and users to ensure they are not linked to anything else
|
|
[cls.user_bob, cls.user_john] = [mail_new_test_user(
|
|
cls.env,
|
|
company_id=cls.company_admin.id,
|
|
email='bob@aptgantt.lan',
|
|
groups='base.group_user',
|
|
name='bob',
|
|
login='bob@aptgantt.lan',
|
|
), mail_new_test_user(
|
|
cls.env,
|
|
company_id=cls.company_admin.id,
|
|
email='john@aptgantt.lan',
|
|
groups='base.group_user',
|
|
name='john',
|
|
login='john@aptgantt.lan',
|
|
)]
|
|
cls.apt_users = cls.user_bob + cls.user_john
|
|
|
|
cls.apt_resource_calendar = cls.env['resource.calendar'].create([{
|
|
'name': 'Gantt Test Apt Resource Calendar',
|
|
'company_id': False,
|
|
'hours_per_day': 24,
|
|
'attendance_ids': [
|
|
(0, 0, {
|
|
'name': f'{nday} Morning',
|
|
'dayofweek': str(nday),
|
|
'hour_from': 0,
|
|
'hour_to': 12,
|
|
'day_period': 'morning',
|
|
}) for nday in range(6)] + [
|
|
(0, 0, {
|
|
'name': f'{nday} Afternoon',
|
|
'dayofweek': str(nday),
|
|
'hour_from': 12,
|
|
'hour_to': 24,
|
|
'day_period': 'afternoon',
|
|
}) for nday in range(6)]
|
|
}])
|
|
cls.apt_resource_1, cls.apt_resource_2 = cls.env['appointment.resource'].create([{
|
|
'capacity': 1,
|
|
'name': 'Resource 1',
|
|
'resource_calendar_id': cls.apt_resource_calendar.id,
|
|
}, {
|
|
'capacity': 2,
|
|
'name': 'Resource 2',
|
|
'resource_calendar_id': cls.apt_resource_calendar.id,
|
|
}])
|
|
|
|
cls.apt_types = cls.env['appointment.type'].create([{
|
|
'appointment_tz': 'UTC',
|
|
'name': 'bob apt type',
|
|
'staff_user_ids': [(4, cls.user_bob.id)],
|
|
}, {
|
|
'appointment_tz': 'UTC',
|
|
'name': 'nouser apt type',
|
|
'staff_user_ids': [],
|
|
}, {
|
|
'appointment_tz': 'UTC',
|
|
'name': 'resource apt type',
|
|
'resource_ids': [(4, cls.apt_resource_1.id), (4, cls.apt_resource_2.id)],
|
|
'schedule_based_on': 'resources',
|
|
'manage_capacity': True,
|
|
}])
|
|
cls.user_apt_types = cls.apt_types[:2]
|
|
cls.resource_apt_types = cls.apt_types[2]
|
|
|
|
cls.gantt_context = {'appointment_booking_gantt_show_all_resources': True}
|
|
cls.gantt_domain = [('appointment_type_id', 'in', cls.apt_types.ids)]
|
|
|
|
class AppointmentGanttTest(AppointmentGanttTestCommon):
|
|
@users('apt_manager')
|
|
def test_default_assign_user_attendees(self):
|
|
"""
|
|
1> To check, Single attendee should be set as an organizer by default.
|
|
(This is typically applied when selecting a specific slot in the
|
|
appointment kanban.)
|
|
|
|
2> (special gantt case) The current user should be an attendee if
|
|
he is set as the organizer.
|
|
"""
|
|
# context while clicking the 'New' btn
|
|
no_attendees_context = {
|
|
'booking_gantt_create_record': True,
|
|
'appointment_default_assign_user_attendees': True,
|
|
'default_partner_ids': [],
|
|
}
|
|
|
|
# context while clicking the time slot of the staff user
|
|
single_attendee_context = {
|
|
**no_attendees_context,
|
|
'default_partner_ids': [self.user_bob.partner_id.id],
|
|
}
|
|
|
|
# Case 1: Specify a single attendee, simulating clicking a slot on the gantt view
|
|
event_with_partner = self.env['calendar.event'].with_context(single_attendee_context).create({
|
|
'name': 'event with partner',
|
|
'appointment_type_id': self.apt_types[0].id,
|
|
})
|
|
# Case 2: Create an appointment without specifying attendees
|
|
event_without_partner = self.env['calendar.event'].with_context(no_attendees_context).create({
|
|
'name': 'event without partner',
|
|
'appointment_type_id': self.apt_types[0].id,
|
|
})
|
|
|
|
# Case 1 check
|
|
self.assertEqual(
|
|
event_with_partner.user_id,
|
|
self.user_bob,
|
|
"Single attendee should be set as an organizer by default",
|
|
)
|
|
# Case 2 check
|
|
self.assertEqual(
|
|
event_without_partner.user_id,
|
|
self.apt_manager,
|
|
"The current user should be an organizer",
|
|
)
|
|
self.assertEqual(
|
|
event_without_partner.attendee_ids.partner_id,
|
|
self.apt_manager.partner_id,
|
|
"If we don't specify an attendee, the current user should be set as an attendee",
|
|
)
|
|
|
|
def test_gantt_calendar_unavailability(self):
|
|
"""Checks whether the Gantt view correctly excludes free events and considers only busy events for calculating unavailability."""
|
|
self._create_meetings(
|
|
self.user_john,
|
|
[(self.reference_monday, self.reference_monday + timedelta(hours=2), False)],
|
|
show_as='busy' # Event with 'busy' status
|
|
)
|
|
self._create_meetings(
|
|
self.user_bob,
|
|
[(self.reference_monday, self.reference_monday + timedelta(hours=1), False)],
|
|
show_as='free' # Event with 'free' status
|
|
)
|
|
|
|
# Call the _gantt_unavailability function
|
|
unavailabilities = self.env['calendar.event']._gantt_unavailability(
|
|
'partner_ids',
|
|
[self.user_john.partner_id.id, self.user_bob.partner_id.id],
|
|
self.reference_monday.replace(hour=0),
|
|
self.reference_monday.replace(hour=23),
|
|
'day',
|
|
)
|
|
|
|
self.assertEqual(len(unavailabilities.get(self.user_john.partner_id.id, [])), 1, "Busy events should be counted as unavailability.")
|
|
self.assertEqual(len(unavailabilities.get(self.user_bob.partner_id.id, [])), 0, "Free events should not be counted as unavailability.")
|
|
|
|
@users('apt_manager')
|
|
def test_gantt_default_calendar_event_values(self):
|
|
"""Make sure all of the defaults are sensible with respect to the appointment type used."""
|
|
quick_create_form = self.env.ref('appointment.calendar_event_view_form_gantt_booking')
|
|
base_context = self.gantt_context | {'booking_gantt_create_record': True}
|
|
user_appointment_type = self.user_apt_types[0]
|
|
resource_appointment_type = self.resource_apt_types[0]
|
|
for appointment_type, write_values, expected_capacity_reserved in [
|
|
(user_appointment_type, {'manage_capacity': False, 'user_capacity': 1}, 1),
|
|
(user_appointment_type, {'manage_capacity': False, 'user_capacity': 5}, 1),
|
|
(user_appointment_type, {'manage_capacity': True, 'user_capacity': 5}, 5),
|
|
(resource_appointment_type, {'manage_capacity': False}, 2), # 1 per resource
|
|
(resource_appointment_type, {'manage_capacity': True}, 3), # sum of capacities
|
|
]:
|
|
with self.subTest(appointment=appointment_type.name, write_values=write_values):
|
|
appointment_type.write(write_values)
|
|
appointment_context = base_context | {
|
|
'default_appointment_type_id': appointment_type.id,
|
|
}
|
|
if appointment_type.schedule_based_on == 'users':
|
|
appointment_context |= {'default_user_id': user_appointment_type.staff_user_ids[0].id}
|
|
else:
|
|
appointment_context |= {'default_resource_ids': resource_appointment_type.resource_ids.ids}
|
|
expected_default_dict = {
|
|
'appointment_type_id': appointment_type.id,
|
|
'name': appointment_type.name,
|
|
'total_capacity_reserved': expected_capacity_reserved,
|
|
}
|
|
CalendarEvent = self.env['calendar.event'].with_context(appointment_context)
|
|
# check defaultget
|
|
self.assertDictEqual(
|
|
{
|
|
field_name: value for field_name, value
|
|
in CalendarEvent.default_get(list(expected_default_dict.keys()) + ['user_id', 'resource_ids']).items()
|
|
if field_name in expected_default_dict
|
|
},
|
|
expected_default_dict
|
|
)
|
|
# check created values match up when created via the gantt view
|
|
event = Form(CalendarEvent, quick_create_form).save()
|
|
event.invalidate_recordset(['total_capacity_reserved', 'user_id', 'resource_ids'])
|
|
self.assertDictEqual(event.read(['name', 'total_capacity_reserved'])[0], {
|
|
'id': event.id,
|
|
'name': appointment_type.name,
|
|
'total_capacity_reserved': expected_capacity_reserved
|
|
})
|
|
event.unlink()
|
|
|
|
def test_gantt_empty_groups(self):
|
|
"""Check that the data sent to gantt includes the right groups in the context of appointments."""
|
|
gantt_data = self.env['calendar.event'].with_context(self.gantt_context).get_gantt_data(
|
|
self.gantt_domain, ['partner_ids'], {}
|
|
)
|
|
group_partner_ids = [group['partner_ids'][0] for group in gantt_data['groups']]
|
|
self.assertIn(self.user_bob.partner_id.id, group_partner_ids,
|
|
'Staff assigned to a user-scheduled appointment type should be shown in the default groups')
|
|
self.assertNotIn(self.user_john.partner_id.id, group_partner_ids,
|
|
'Staff not assigned to any appointment type should be hidden')
|
|
|
|
# add john as a staff user of an appointment type -> in the default groups
|
|
self.apt_types[1].staff_user_ids = self.user_john
|
|
|
|
gantt_data = self.env['calendar.event'].with_context(self.gantt_context).get_gantt_data(
|
|
self.gantt_domain, ['partner_ids'], {}
|
|
)
|
|
group_partner_ids = [group['partner_ids'][0] for group in gantt_data['groups']]
|
|
self.assertIn(self.user_bob.partner_id.id, group_partner_ids)
|
|
self.assertIn(self.user_john.partner_id.id, group_partner_ids)
|
|
|
|
# have default appointment in context -> only show staff assigned to that type
|
|
context = self.gantt_context | {'default_appointment_type_id': self.apt_types[0].id}
|
|
gantt_data = self.env['calendar.event'].with_context(context).get_gantt_data(
|
|
self.gantt_domain, ['partner_ids'], {}
|
|
)
|
|
group_partner_ids = [group['partner_ids'][0] for group in gantt_data['groups']]
|
|
self.assertIn(self.user_bob.partner_id.id, group_partner_ids)
|
|
self.assertNotIn(self.user_john.partner_id.id, group_partner_ids, 'Should only display staff assigned to the default apt type.')
|
|
|
|
def test_gantt_hide_non_staff(self):
|
|
"""Check that only the attendees that are part of the staff are used to compute the gantt data.
|
|
|
|
The other attendees, such as the website visitors that created the meeting,
|
|
are excluded and should not be displayed as gantt rows.
|
|
"""
|
|
meeting = self._create_meetings(
|
|
self.apt_users[0],
|
|
[(self.reference_monday, self.reference_monday + timedelta(hours=1), False)],
|
|
self.apt_types[0].id
|
|
)
|
|
meeting.partner_ids += self.partners[0]
|
|
gantt_data = self.env['calendar.event'].with_context(self.gantt_context).get_gantt_data(
|
|
self.gantt_domain, ['partner_ids'], {}
|
|
)
|
|
group_partner_ids = [group['partner_ids'][0] for group in gantt_data['groups']]
|
|
self.assertNotIn(self.partners[0].id, group_partner_ids, 'Attendees with no users should be hidden from the grouping.')
|
|
self.assertIn(self.user_bob.partner_id.id, group_partner_ids)
|
|
self.assertNotIn(self.user_john.partner_id.id, group_partner_ids)
|
|
self.assertEqual(gantt_data['records'], [{'id': meeting.id}])
|
|
|
|
def test_gantt_resource_unavailabilities(self):
|
|
"""Check whether the Gantt view includes already booked resource and exclude sharable resource for calculating unavailability."""
|
|
self.apt_resource_2.shareable = True
|
|
self.env['calendar.event'].sudo().create({
|
|
'appointment_type_id': self.resource_apt_types.id,
|
|
'booking_line_ids': [(0, 0, {'appointment_resource_id': self.apt_resource_2.id}), (0, 0, {'appointment_resource_id': self.apt_resource_1.id})],
|
|
'name': 'Booking 1',
|
|
'start': self.reference_monday.replace(hour=8),
|
|
'stop': self.reference_monday.replace(hour=9),
|
|
})
|
|
resource_1_calendar_unavailabilities = Intervals([
|
|
(datetime(2022, 2, 14, 8, 00, tzinfo=pytz.UTC),
|
|
datetime(2022, 2, 14, 9, 0, tzinfo=pytz.UTC), set()),
|
|
(datetime(2022, 2, 14, 22, 59, 59, 999999, tzinfo=pytz.UTC),
|
|
datetime(2022, 2, 14, 23, 0, tzinfo=pytz.UTC), set()),
|
|
])
|
|
resource_2_calendar_unavailabilities = Intervals([
|
|
(datetime(2022, 2, 14, 22, 59, 59, 999999, tzinfo=pytz.UTC),
|
|
datetime(2022, 2, 14, 23, 0, tzinfo=pytz.UTC), set()),
|
|
])
|
|
unavailabilities = self.env['calendar.event'].with_context(self.gantt_context)._gantt_unavailability(
|
|
'resource_ids',
|
|
[self.apt_resource_1.id, self.apt_resource_2.id],
|
|
self.reference_monday.replace(hour=0),
|
|
self.reference_monday.replace(hour=23),
|
|
'day',
|
|
)
|
|
self.assertListEqual(
|
|
list(Intervals([(unavailability['start'], unavailability['stop'], set())
|
|
for unavailability in unavailabilities.get(self.apt_resource_1.id, [])])),
|
|
list(resource_1_calendar_unavailabilities)
|
|
)
|
|
self.assertListEqual(
|
|
list(Intervals([(unavailability['start'], unavailability['stop'], set())
|
|
for unavailability in unavailabilities.get(self.apt_resource_2.id, [])])),
|
|
list(resource_2_calendar_unavailabilities)
|
|
)
|
|
|
|
@users('staff_user_bxls')
|
|
def test_gantt_resource_unavailabilities_multi_company(self):
|
|
"""Check that resources outside of allowed companies don't get calendar unavailabilities."""
|
|
company_a = self.env.company
|
|
company_b = self.env['res.company'].sudo().create({
|
|
'name': 'Gantt Test Company B',
|
|
})
|
|
self.env.user.sudo().company_ids += company_a + company_b
|
|
|
|
resource_calendar_unavailabilities = Intervals([
|
|
(datetime(2022, 2, 14, 11, 00, tzinfo=pytz.UTC), datetime(2022, 2, 14, 11, 0, tzinfo=pytz.UTC), set()),
|
|
(datetime(2022, 2, 14, 22, 59, 59, 999999, tzinfo=pytz.UTC), datetime(2022, 2, 14, 23, 0, tzinfo=pytz.UTC), set()),
|
|
])
|
|
|
|
for allowed_company_ids, res_1_company, res_2_company, res_1_unavailabilities, res_2_unavailabilities in [
|
|
([], False, False, resource_calendar_unavailabilities, resource_calendar_unavailabilities),
|
|
([], False, company_a, resource_calendar_unavailabilities, resource_calendar_unavailabilities),
|
|
(company_a.ids, False, company_a, resource_calendar_unavailabilities, resource_calendar_unavailabilities),
|
|
(company_b.ids, company_a, company_a, [], []),
|
|
(company_b.ids, company_a, company_b, [], resource_calendar_unavailabilities),
|
|
([company_a.id, company_b.id], company_a, company_b, resource_calendar_unavailabilities, resource_calendar_unavailabilities),
|
|
]:
|
|
with self.subTest(
|
|
allowed_company_ids=allowed_company_ids,
|
|
resource_1_company=res_1_company,
|
|
resource_2_company=res_2_company,
|
|
):
|
|
self.apt_resource_1.sudo().company_id = res_1_company
|
|
self.apt_resource_2.sudo().company_id = res_2_company
|
|
unavailabilities = self.env['calendar.event'].with_context(self.gantt_context | {
|
|
'allowed_company_ids': allowed_company_ids
|
|
})._gantt_unavailability(
|
|
'resource_ids',
|
|
[self.apt_resource_1.id, self.apt_resource_2.id],
|
|
self.reference_monday.replace(hour=0),
|
|
self.reference_monday.replace(hour=23),
|
|
'day',
|
|
)
|
|
|
|
self.assertListEqual(
|
|
list(Intervals([(unavailability['start'], unavailability['stop'], set())
|
|
for unavailability in unavailabilities.get(self.apt_resource_1.id, [])])),
|
|
list(res_1_unavailabilities)
|
|
)
|
|
self.assertListEqual(
|
|
list(Intervals([(unavailability['start'], unavailability['stop'], set())
|
|
for unavailability in unavailabilities.get(self.apt_resource_2.id, [])])),
|
|
list(res_2_unavailabilities)
|
|
)
|
|
|
|
def test_gantt_without_attendees(self):
|
|
meeting = self._create_meetings(
|
|
self.user_john[0],
|
|
[(self.reference_monday, self.reference_monday + timedelta(hours=1), False)],
|
|
self.apt_types[0].id
|
|
)
|
|
meeting.partner_ids = False
|
|
gantt_data = self.env['calendar.event'].with_context(self.gantt_context).get_gantt_data(
|
|
self.gantt_domain, ['partner_ids'], {}
|
|
)
|
|
group_partner_ids = [group['partner_ids'][0] for group in gantt_data['groups']]
|
|
self.assertIn(self.user_bob.partner_id.id, group_partner_ids)
|
|
self.assertNotIn(self.user_john.partner_id.id, group_partner_ids)
|
|
self.assertEqual(gantt_data['records'], [{'id': meeting.id}])
|
|
|
|
@users('apt_manager')
|
|
def test_gantt_read_group_resource_events_privacy(self):
|
|
""" Check that every resource events are correctly displayed in the gantt view.
|
|
|
|
Due to the events formatted_read_group privacy domain, resource events booked from the front-end
|
|
(meaning having OdooBot as user_id) weren't displayed in the gantt view.
|
|
Making sure every resource events are now visible and accessible no matter their privacy and user_id.
|
|
|
|
Using apt_manager to be sure the privacy part of the formatted_read_group domain is correctly added
|
|
as it is only included when we're not in super user.
|
|
"""
|
|
meeting = self._create_meetings(
|
|
self.env.ref('base.user_root'),
|
|
[(self.reference_monday, self.reference_monday + timedelta(hours=1), False)],
|
|
self.apt_types[2].id,
|
|
)
|
|
gantt_data = self.env['calendar.event'].with_context(self.gantt_context).get_gantt_data(self.gantt_domain, ['partner_ids'], {})
|
|
self.assertEqual(gantt_data['records'], [{'id': meeting.id}], "Should correctly retrieve the resource related event.")
|