279 lines
14 KiB
Python
279 lines
14 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||
|
||
from datetime import datetime
|
||
|
||
from odoo.exceptions import UserError
|
||
from odoo.tests import tagged
|
||
|
||
from odoo.addons.planning.tests.common import TestCommonPlanning
|
||
|
||
|
||
@tagged("post_install", "-at_install")
|
||
class TestPlanningReport(TestCommonPlanning):
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super().setUpClass()
|
||
cls.setUpEmployees()
|
||
cls.setUpDates()
|
||
cls.setUpCalendars()
|
||
|
||
cls.planning_role = cls.env["planning.role"].create({
|
||
"name": "Developer",
|
||
})
|
||
|
||
cls.slot_1, cls.slot_2 = cls.env["planning.slot"].create([{
|
||
"start_datetime": "2024-01-01 08:00:00",
|
||
"end_datetime": "2024-01-01 17:00:00",
|
||
"resource_id": cls.resource_joseph.id,
|
||
"role_id": cls.planning_role.id,
|
||
}, {
|
||
"start_datetime": "2024-01-02 08:00:00",
|
||
"end_datetime": "2024-01-02 17:00:00",
|
||
"resource_id": cls.resource_bert.id,
|
||
"role_id": cls.planning_role.id,
|
||
}])
|
||
|
||
def test_planning_report_create_action_blocked(self):
|
||
"""Test that adding the planning report to the print menu is blocked."""
|
||
planning_report = self.env.ref("planning.report_planning_slot")
|
||
with self.assertRaises(UserError) as capture:
|
||
planning_report.create_action()
|
||
|
||
self.assertEqual(
|
||
capture.exception.args[0],
|
||
"The Planning report cannot be added to the print menu. "
|
||
"Please use the Print action available in the Planning calendar and Gantt views."
|
||
)
|
||
|
||
def test_planning_report_without_data_raises_error(self):
|
||
"""Test that printing from list/form view (without required context) raises UserError.
|
||
|
||
This simulates the scenario where a user already added the report to the Print
|
||
menu (before the fix) and tries to print from list/form view.
|
||
"""
|
||
with self.assertRaises(UserError) as capture:
|
||
self.env["ir.actions.report"].with_context(force_report_rendering=True)._render_qweb_pdf(
|
||
"planning.report_planning_slot", res_ids=[self.slot_1.id, self.slot_2.id]
|
||
)
|
||
|
||
self.assertEqual(
|
||
capture.exception.args[0],
|
||
"The Planning report cannot be printed from here. "
|
||
"Please use the Print action available in the Planning calendar and Gantt views."
|
||
)
|
||
|
||
def test_planning_report_with_proper_data_works(self):
|
||
"""Test that the correct workflow (Print button in calendar view) still works."""
|
||
result = self.env["planning.slot"].action_print_plannings(
|
||
"2024-01-01 00:00:00",
|
||
"2024-01-07 23:59:59",
|
||
["resource_id"],
|
||
[("id", "in", [self.slot_1.id, self.slot_2.id])],
|
||
)
|
||
|
||
self.assertEqual(result.get("type"), "ir.actions.report")
|
||
self.assertDictEqual(
|
||
{key: key in result.get("data", {}) for key in ("weeks", "group_by_slots_per_day_per_week", "group_by_name_by_id")},
|
||
{"weeks": True, "group_by_slots_per_day_per_week": True, "group_by_name_by_id": True},
|
||
)
|
||
|
||
def test_print_planning(self):
|
||
"""
|
||
In this test, we make sure that the split works well for:
|
||
1- slots starting before the week first day: we split the pill, and eliminate the part before the week start day
|
||
2- slots ending before the week end day: we split the pill, and eliminate the part after the week end day
|
||
3- the remaining part (inside the week) is splitted into many pills (pill per day), allocated hours, start_datetime and end_datetime
|
||
are computed for each pill based on the resource availabilities
|
||
"""
|
||
flexEmployee, standardEmployee = self.env['hr.employee'].create([{
|
||
'name': 'Flex Employee',
|
||
'tz': 'UTC',
|
||
}, {
|
||
'name': 'Standard Employee',
|
||
'tz': 'UTC',
|
||
}])
|
||
|
||
flexEmployee.resource_id.calendar_id = self.flex_40h_calendar
|
||
standardEmployee.resource_id.calendar_id = self.company_calendar
|
||
slots_count = self.env['planning.slot'].search_count([])
|
||
|
||
# the diff between start and end is exactly 6 days
|
||
self.env.user.tz = 'UTC'
|
||
# Case 1: Create a planning slot on non-working days with a specific employee resource
|
||
slot1, slot2, slot3 = self.env['planning.slot'].with_context(tz='UTC').create([{
|
||
'resource_id': flexEmployee.resource_id.id,
|
||
'start_datetime': datetime(2025, 5, 16, 8, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 20, 17, 0, 0),
|
||
}, {
|
||
'resource_id': standardEmployee.resource_id.id,
|
||
'start_datetime': datetime(2025, 5, 21, 8, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 26, 17, 0, 0),
|
||
}, {
|
||
'start_datetime': datetime(2025, 5, 16, 8, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 19, 17, 0, 0),
|
||
}])
|
||
|
||
slots = slot1 | slot2 | slot3
|
||
current_slots_count = self.env['planning.slot'].search_count([])
|
||
self.assertEqual(current_slots_count, len(slots) + slots_count, "3 slots should be created")
|
||
|
||
field_involved_in_fake_pill_creating_and_updating = slots._print_planning_get_fields_to_copy()
|
||
|
||
def get_slots_values(slots):
|
||
values = {}
|
||
for slot in slots:
|
||
values[slot.id] = {
|
||
field: slot[field]
|
||
for field in field_involved_in_fake_pill_creating_and_updating
|
||
}
|
||
|
||
return values
|
||
|
||
original_values = get_slots_values(slots)
|
||
|
||
action = self.env['planning.slot'].with_context(discard_logo_check=True).action_print_plannings(
|
||
date_start='2025-05-18 00:00:00',
|
||
date_end='2025-05-24 23:59:59',
|
||
group_bys=['resource_id'],
|
||
domain=[['start_datetime', '<', '2025-05-25 00:00:00'], ['end_datetime', '>', '2025-05-18 00:00:00']]
|
||
)
|
||
|
||
# make sure fake slots are not created in db
|
||
self.assertEqual(current_slots_count, self.env['planning.slot'].search_count([]), "no additional slots should be created")
|
||
all_slots = self.env['planning.slot'].search([['start_datetime', '<', '2025-05-25 00:00:00'], ['end_datetime', '>', '2025-05-18 00:00:00']])
|
||
|
||
# make sure existing slots are not updated when manipulating fake slots
|
||
slots_after_printing = all_slots & slots
|
||
self.assertDictEqual(original_values, get_slots_values(slots_after_printing))
|
||
|
||
# OPEN SHIFTS: from 18 to 19, other are eliminated because they're outside the week period,
|
||
# each slot is from 00:00 to 23:59 (because there is no calendar to follow) and has 8 allocated hours following the company work schedule
|
||
# except day 19 as it takes the slot end_datetime
|
||
self.assertEqual(len(action['data']['group_by_slots_per_day_per_week']), 1, "one week")
|
||
self.assertEqual(len(action['data']['group_by_slots_per_day_per_week'][0]), 3)
|
||
|
||
# resources should be sorted as (False, display named in non DESC order)
|
||
self.assertEqual((action['data']['group_by_slots_per_day_per_week'][0][0][0], dict(action['data']['group_by_slots_per_day_per_week'][0][0][1])), (False, {
|
||
'05/18/2025': [
|
||
{'title': '12:00 AM – 11:59 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
'05/19/2025': [
|
||
{'title': '12:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
}))
|
||
|
||
# FLEX EMPLOYEE: from 18 to 20, other are eliminated because they're outside the week period,
|
||
# each slot is from 00:00 to 23:59 and has 8 allocated hours following the flex_40h_calendar
|
||
# except day 20 as it takes the slot end_datetime
|
||
|
||
self.assertEqual(action['data']['group_by_slots_per_day_per_week'][0][1][0], flexEmployee.resource_id.id)
|
||
self.assertDictEqual(action['data']['group_by_slots_per_day_per_week'][0][1][1], {
|
||
'05/18/2025': [
|
||
{'title': '12:00 AM – 11:59 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
'05/19/2025': [
|
||
{'title': '12:00 AM – 11:59 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
'05/20/2025': [
|
||
{'title': '12:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
})
|
||
|
||
# STANDARD EMPLOYEE: from 22 to 23, other are eliminated because they're outside the week period and 24 is part of the weekend
|
||
# each slot is from 08:00 to 17:00 and has 8 allocated hours, except day 22 (from 6 to 15) following exaclty company_calendar
|
||
self.assertEqual(action['data']['group_by_slots_per_day_per_week'][0][2][0], standardEmployee.resource_id.id)
|
||
self.assertDictEqual(dict(action['data']['group_by_slots_per_day_per_week'][0][2][1]), {
|
||
'05/21/2025': [
|
||
{'title': '08:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
'05/22/2025': [
|
||
{'title': '06:00 AM – 03:00 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
'05/23/2025': [
|
||
{'title': '08:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'}
|
||
],
|
||
})
|
||
|
||
self.assertEqual(action['data']['weeks'][0], (0, [
|
||
'05/18/2025',
|
||
'05/19/2025',
|
||
'05/20/2025',
|
||
'05/21/2025',
|
||
'05/22/2025',
|
||
'05/23/2025',
|
||
'05/24/2025'
|
||
], 'Week from 05/18/2025 to 05/24/2025'))
|
||
|
||
def test_order_of_planning_slots_in_report(self):
|
||
self.employee_bert.resource_id.calendar_id = self.company_calendar
|
||
self.env.user.tz = 'UTC'
|
||
slots = self.env['planning.slot'].with_context(tz='UTC').create([{
|
||
'resource_id': self.employee_bert.resource_id.id,
|
||
'start_datetime': datetime(2025, 5, 20, 8, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 20, 9, 0, 0),
|
||
}, {
|
||
'resource_id': self.employee_bert.resource_id.id,
|
||
'start_datetime': datetime(2025, 5, 20, 9, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 20, 10, 0, 0),
|
||
}, {
|
||
'resource_id': self.employee_bert.resource_id.id,
|
||
'start_datetime': datetime(2025, 5, 20, 10, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 22, 17, 0, 0),
|
||
}, {
|
||
'resource_id': self.employee_bert.resource_id.id,
|
||
'start_datetime': datetime(2025, 5, 21, 1, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 21, 17, 0, 0),
|
||
}, {
|
||
'resource_id': self.employee_bert.resource_id.id,
|
||
'start_datetime': datetime(2025, 5, 20, 8, 0, 0),
|
||
'end_datetime': datetime(2025, 5, 20, 9, 0, 0),
|
||
'role_id': self.planning_role.id,
|
||
}])
|
||
|
||
action = self.env['planning.slot'].with_context(discard_logo_check=True).action_print_plannings(
|
||
date_start='2025-05-18 00:00:00',
|
||
date_end='2025-05-24 23:59:59',
|
||
group_bys=['resource_id'],
|
||
domain=[['start_datetime', '<', '2025-05-25 00:00:00'], ['end_datetime', '>', '2025-05-18 00:00:00']]
|
||
)
|
||
|
||
"""
|
||
Expected slots
|
||
+---------------------------+---------------------------+---------------------------+
|
||
| Tue 20 | Wed 21 | Thu 22 |
|
||
| 05/20/2025 | 05/21/2025 | 05/22/2025 |
|
||
+---------------------------+---------------------------+---------------------------+
|
||
| Slot 1 | Slot 3 | Slot 3 |
|
||
| 8:00 AM – 9:00 AM | 8:00 AM – 5:00 PM | 6:00 AM – 5:00 PM |
|
||
+---------------------------+---------------------------+---------------------------+
|
||
| Slot 5 (flex role) | Slot 4 | |
|
||
| 8:00 AM – 9:00 AM | 1:00 AM – 5:00 PM | |
|
||
+---------------------------+---------------------------+---------------------------+
|
||
| Slot 2 | | |
|
||
| 9:00 AM – 10:00 AM | | |
|
||
+---------------------------+---------------------------+---------------------------+
|
||
| Slot 3 | | |
|
||
| 10:00 AM – 5:00 PM | | |
|
||
+---------------------------+---------------------------+---------------------------+
|
||
"""
|
||
|
||
weekly_slot_data = dict(action['data']['group_by_slots_per_day_per_week'][0][0][1])
|
||
slot5_color = slots[4].role_id._get_light_color(0.5, not slots[4].resource_id)
|
||
self.assertDictEqual(weekly_slot_data, {
|
||
# All the shits follow time order
|
||
'05/20/2025': [
|
||
{'title': '08:00 AM – 09:00 AM', 'style': 'background-color: #80c3c2;'},
|
||
{'title': '08:00 AM – 09:00 AM Developer', 'style': f'background-color: {slot5_color};'},
|
||
{'title': '09:00 AM – 10:00 AM', 'style': 'background-color: #80c3c2;'},
|
||
{'title': '10:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'},
|
||
],
|
||
'05/21/2025': [
|
||
{'title': '08:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'},
|
||
{'title': '01:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'},
|
||
],
|
||
'05/22/2025': [
|
||
{'title': '06:00 AM – 05:00 PM', 'style': 'background-color: #80c3c2;'},
|
||
],
|
||
})
|