Files
enterprise-addons/planning/tests/test_planning.py
T

1524 lines
77 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
import re
from datetime import datetime, time, timedelta
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.exceptions import UserError, ValidationError
from odoo import Command, fields
from odoo.tests import Form, new_test_user
from odoo.addons.mail.tests.common import MockEmail
from .common import TestCommonPlanning
class TestPlanning(TestCommonPlanning, MockEmail):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.classPatch(cls.env.cr, 'now', datetime.now)
with freeze_time('2019-05-01'):
cls.setUpCalendars()
cls.setUpEmployees()
calendar_joseph = cls.env['resource.calendar'].create({
'name': 'Calendar 1',
'tz': 'UTC',
'hours_per_day': 8.0,
'attendance_ids': [
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 13, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 14, 'day_period': 'lunch'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 14, 'hour_to': 18, 'day_period': 'afternoon'}),
]
})
calendar_bert = cls.env['resource.calendar'].create({
'name': 'Calendar 2',
'tz': 'UTC',
'hours_per_day': 4,
'attendance_ids': [
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'morning'}),
],
})
cls.env.user.company_id.resource_calendar_id = cls.company_calendar
cls.employee_joseph.resource_calendar_id = calendar_joseph
cls.employee_bert.resource_calendar_id = calendar_bert
cls.slot, cls.slot2 = cls.env['planning.slot'].create([
{
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
'end_datetime': datetime(2019, 6, 27, 18, 0, 0),
},
{
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
'end_datetime': datetime(2019, 6, 28, 18, 0, 0),
}
])
cls.template = cls.env['planning.slot.template'].create({
'start_time': 11,
'end_time': 14,
'duration_days': 1,
})
cls.calendar_40h_flex = cls.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'tz': 'UTC',
'full_time_required_hours': 40.0,
'hours_per_day': 8.0,
'hours_per_week': 40.0,
'flexible_hours': True,
})
cls.flex_role = cls.env['planning.role'].create({'name': 'flex role'})
cls.flex_employee = cls.env['hr.employee'].create({
'name': 'Night employee',
'resource_calendar_id': cls.calendar_40h_flex.id,
'default_planning_role_id': cls.flex_role.id,
'tz': 'UTC',
})
def test_allocated_hours_defaults(self):
self.assertEqual(self.slot.allocated_hours, 8, "It should follow the calendar of the resource to compute the allocated hours.")
self.assertEqual(self.slot.allocated_percentage, 100, "It should have the default value")
def test_change_percentage(self):
self.slot.allocated_percentage = 60
self.assertEqual(self.slot.allocated_hours, 8 * 0.60, "It should 60%% of working hours")
self.slot2.allocated_percentage = 60
self.assertEqual(self.slot2.allocated_hours, 16 * 0.60)
def test_change_hours_more(self):
self.slot.allocated_hours = 12
self.assertEqual(self.slot.allocated_percentage, 150)
self.slot2.allocated_hours = 24
self.assertEqual(self.slot2.allocated_percentage, 150)
def test_change_hours_less(self):
self.slot.allocated_hours = 4
self.assertEqual(self.slot.allocated_percentage, 50)
self.slot2.allocated_hours = 8
self.assertEqual(self.slot2.allocated_percentage, 50)
def test_change_start(self):
self.slot.start_datetime += relativedelta(hours=2)
self.assertEqual(self.slot.allocated_percentage, 100, "It should still be 100%")
self.assertEqual(self.slot.allocated_hours, 8, "It should decreased by 2 hours")
def test_change_start_partial(self):
self.slot.allocated_percentage = 80
self.slot.start_datetime += relativedelta(hours=2)
self.slot.flush_recordset()
self.slot.invalidate_recordset()
self.assertEqual(self.slot.allocated_hours, 8 * 0.8, "It should be decreased by 2 hours and percentage applied")
self.assertEqual(self.slot.allocated_percentage, 80, "It should still be 80%")
def test_change_end(self):
self.slot.end_datetime -= relativedelta(hours=2)
self.assertEqual(self.slot.allocated_percentage, 100, "It should still be 100%")
self.assertEqual(self.slot.allocated_hours, 8, "It should decreased by 2 hours")
def test_set_template(self):
self.env.user.tz = 'Europe/Brussels'
self.slot.template_id = self.template
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 9, 0), 'It should set time from template, in user timezone (11am CET -> 9am UTC)')
def test_change_employee_with_template(self):
self.env.user.tz = 'UTC'
self.slot.template_id = self.template
self.env.flush_all()
# simulate public user (no tz)
self.env.user.tz = False
self.slot.resource_id = self.employee_janice.resource_id
self.assertEqual(self.slot.template_id, self.template, 'It should keep the template')
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 15, 0), 'It should adjust for employee timezone: 11am EDT -> 3pm UTC')
def test_change_employee(self):
""" Ensures that changing the employee does not have an impact to the shift. """
self.env.user.tz = 'UTC'
self.slot.resource_id = self.employee_joseph.resource_id
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 8, 0), 'It should not adjust to employee calendar')
self.assertEqual(self.slot.end_datetime, datetime(2019, 6, 27, 18, 0), 'It should not adjust to employee calendar')
self.slot.resource_id = self.employee_bert.resource_id
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 8, 0), 'It should not adjust to employee calendar')
self.assertEqual(self.slot.end_datetime, datetime(2019, 6, 27, 18, 0), 'It should not adjust to employee calendar')
def test_create_with_employee(self):
""" This test's objective is to mimic shift creation from the gant view and ensure that the correct behavior is met.
This test objective is to test the default values when creating a new shift for an employee when provided defaults are within employee's calendar workdays
"""
self.env.user.tz = 'UTC'
PlanningSlot = self.env['planning.slot'].with_context(
tz='UTC',
default_start_datetime='2019-06-27 00:00:00',
default_end_datetime='2019-06-27 23:59:59',
default_resource_id=self.resource_joseph.id)
defaults = PlanningSlot.default_get(['resource_id', 'start_datetime', 'end_datetime'])
self.assertEqual(defaults.get('start_datetime'), datetime(2019, 6, 27, 9, 0), 'It should be adjusted to employee calendar: 0am -> 9pm')
self.assertEqual(defaults.get('end_datetime'), datetime(2019, 6, 27, 18, 0), 'It should be adjusted to employee calendar: 0am -> 18pm')
def test_specific_time_creation(self):
self.env.user.tz = 'UTC'
PlanningSlot = self.env['planning.slot'].with_context(
tz='UTC',
default_start_datetime='2020-10-05 06:00:00',
default_end_datetime='2020-10-05 12:30:00',
planning_keep_default_datetime=True)
defaults = PlanningSlot.default_get(['start_datetime', 'end_datetime'])
self.assertEqual(defaults.get('start_datetime'), datetime(2020, 10, 5, 6, 0), 'start_datetime should not change')
self.assertEqual(defaults.get('end_datetime'), datetime(2020, 10, 5, 12, 30), 'end_datetime should not change')
def test_create_with_employee_outside_schedule(self):
""" This test objective is to test the default values when creating a new shift for an employee when provided defaults are not within employee's calendar workdays """
self.env.user.tz = 'UTC'
# Case 1: Create a planning slot on non-working days with a specific employee resource
PlanningSlot = self.env['planning.slot'].with_context(
tz='UTC',
default_start_datetime='2019-06-26 00:00:00',
default_end_datetime='2019-06-26 23:59:59',
default_resource_id=self.resource_joseph.id)
defaults = PlanningSlot.default_get(['resource_id', 'start_datetime', 'end_datetime'])
self.assertEqual(defaults.get('start_datetime'), datetime(2019, 6, 26, 8, 0), 'It should adjust to employee calendar: 0am -> 8pm')
self.assertEqual(defaults.get('end_datetime'), datetime(2019, 6, 26, 17, 0), 'It should adjust to employee calendar: 0am -> 5am')
# Case 2: Create a planning slot on non-working days without a specific employee resource
PlanningSlot = self.env['planning.slot'].with_context(
tz='UTC',
default_start_datetime='2019-12-07 00:00:00',
default_end_datetime='2019-12-08 23:59:59',
)
defaults = PlanningSlot.default_get(['resource_id', 'start_datetime', 'end_datetime'])
self.assertEqual(
defaults.get('start_datetime'),
datetime(2019, 12, 7, 8, 0),
'The start time should be adjusted to the default working hours: 8:00 AM on non-working days'
)
self.assertEqual(
defaults.get('end_datetime'),
datetime(2019, 12, 8, 17, 0),
'The end date should be adjusted to the default working hours: 17:00 on on non-working days'
)
def test_create_without_employee(self):
""" This test objective is to test the default values when creating a new shift when no employee is set """
self.env.user.tz = 'UTC'
PlanningSlot = self.env['planning.slot'].with_context(
tz='UTC',
default_start_datetime='2019-06-27 00:00:00',
default_end_datetime='2019-06-27 23:59:59',
default_resource_id=False)
defaults = PlanningSlot.default_get(['resource_id', 'start_datetime', 'end_datetime'])
self.assertEqual(defaults.get('start_datetime'), datetime(2019, 6, 27, 6, 0), 'It should adjust to employee calendar: 0am -> 6pm')
self.assertEqual(defaults.get('end_datetime'), datetime(2019, 6, 27, 15, 0), 'It should adjust to employee calendar: 0am -> 3pm')
def test_unassign_employee_with_template(self):
# we are going to put everybody in EDT, because if the employee has a different timezone from the company this workflow does not work.
self.env.user.tz = 'America/New_York'
self.env.user.company_id.resource_calendar_id.tz = 'America/New_York'
self.slot.template_id = self.template
self.env.flush_all()
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 15, 0), 'It should set time from template, in user timezone (11am EDT -> 3pm UTC)')
# simulate public user (no tz)
self.env.user.tz = False
self.slot.resource_id = self.resource_janice.id
self.env.flush_all()
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 15, 0), 'It should adjust to employee timezone')
self.slot.resource_id = None
self.assertEqual(self.slot.template_id, self.template, 'It should keep the template')
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 15, 0), 'It should reset to company calendar timezone: 11am EDT -> 3pm UTC')
def test_compute_overlap_count(self):
self.slot_6_2 = self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2019, 6, 2, 8, 0),
'end_datetime': datetime(2019, 6, 2, 17, 0),
})
self.slot_6_3 = self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2019, 6, 3, 8, 0),
'end_datetime': datetime(2019, 6, 3, 17, 0),
})
self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2019, 6, 2, 10, 0),
'end_datetime': datetime(2019, 6, 2, 12, 0),
})
self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2019, 6, 2, 16, 0),
'end_datetime': datetime(2019, 6, 2, 18, 0),
})
self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2019, 6, 2, 18, 0),
'end_datetime': datetime(2019, 6, 2, 20, 0),
})
self.assertEqual(2, self.slot_6_2.overlap_slot_count, '2 slots overlap')
self.assertEqual(0, self.slot_6_3.overlap_slot_count, 'no slot overlap')
def test_compute_datetime_with_template_slot(self):
""" Test if the start and end datetimes of a planning.slot are correctly computed with the template slot
Test Case:
=========
1) Create a planning.slot.template with start_hours = 11 am, end_hours = 2pm and duration_days = 2.
2) Create a planning.slot for one day and add the template.
3) Check if the start and end dates are on two days and not one.
4) Check if the allocating hours is equal to the working hours of the resource.
"""
self.employee_bert.resource_calendar_id = self.company_calendar
template_slot = self.env['planning.slot.template'].create({
'start_time': 11,
'end_time': 14,
'duration_days': 2,
})
slot = self.env['planning.slot'].create({
'start_datetime': datetime(2021, 1, 4, 0, 0),
'end_datetime': datetime(2021, 1, 4, 23, 59),
'resource_id': self.resource_bert.id,
})
slot.write({
'template_id': template_slot.id,
})
self.assertEqual(slot.start_datetime, datetime(2021, 1, 4, 11, 0), 'The start datetime should have the same hour and minutes defined in the template in the resource timezone.')
self.assertEqual(slot.end_datetime, datetime(2021, 1, 5, 14, 0), 'The end datetime of this slot should be 3 hours after the start datetime as mentionned in the template in the resource timezone.')
self.assertEqual(slot.allocated_hours, 10, 'The allocated hours of this slot should be the duration defined in the template in the resource timezone.')
def test_compute_datetime_template_slot_24_7_calendar(self):
""" A multi-day-span template on a 24/7 (0h-24h) calendar must not add a spurious minute.
On a 0h-24h calendar, plan_days() returns the day's end as 23:59:59.999999; the
end-datetime computation overwrote the hour and minute but kept the stale seconds,
so an 8-hour shift was stored as 8h 1min and allocated_hours showed 08:01.
"""
calendar_24_7 = self.env['resource.calendar'].create({
'name': '24/7',
'tz': 'UTC',
'attendance_ids': [
Command.create({'name': 'Day %s' % d, 'dayofweek': str(d), 'hour_from': 0, 'hour_to': 24, 'day_period': 'full_day'})
for d in range(7)
],
})
self.employee_bert.resource_calendar_id = calendar_24_7
template_slot = self.env['planning.slot.template'].create({
'start_time': 16,
'end_time': 0,
'duration_days': 2,
})
slot = self.env['planning.slot'].create({
'start_datetime': datetime(2021, 1, 4, 0, 0),
'end_datetime': datetime(2021, 1, 4, 23, 59),
'resource_id': self.resource_bert.id,
})
slot.write({'template_id': template_slot.id})
self.assertEqual(slot.end_datetime, datetime(2021, 1, 5, 0, 0), 'The end datetime should land exactly on midnight, with no leftover seconds from the 24/7 calendar.')
self.assertEqual(slot.allocated_hours, 8, 'An 8-hour shift should allocate exactly 8 hours, not 8h01.')
def test_planning_state(self):
""" The purpose of this test case is to check the planning state """
self.slot.resource_id = self.employee_bert.resource_id
self.assertEqual(self.slot.state, 'draft', 'Planning is draft mode.')
self.slot.action_send()
self.assertEqual(self.slot.state, 'published', 'Planning is published.')
def test_create_working_calendar_period(self):
""" A default dates should be calculated based on the working calendar of the company whatever the period """
test = Form(self.env['planning.slot'].with_context(
default_start_datetime=datetime(2019, 5, 27, 0, 0),
default_end_datetime=datetime(2019, 5, 27, 23, 59, 59)
))
slot = test.save()
self.assertEqual(slot.start_datetime, datetime(2019, 5, 27, 8, 0), 'It should adjust to employee calendar: 0am -> 9pm')
self.assertEqual(slot.end_datetime, datetime(2019, 5, 27, 17, 0), 'It should adjust to employee calendar: 0am -> 9pm')
# For weeks period
test_week = Form(self.env['planning.slot'].with_context(
default_start_datetime=datetime(2019, 6, 23, 0, 0),
default_end_datetime=datetime(2019, 6, 29, 23, 59, 59)
))
test_week = test_week.save()
self.assertEqual(test_week.start_datetime, datetime(2019, 6, 24, 8, 0), 'It should adjust to employee calendar: 0am -> 9pm')
self.assertEqual(test_week.end_datetime, datetime(2019, 6, 28, 17, 0), 'It should adjust to employee calendar: 0am -> 9pm')
def test_create_planing_slot_without_start_date(self):
"Test to create planning slot with template id and without start date"
planning_role = self.env['planning.role'].create({'name': 'role x'})
template = self.env['planning.slot.template'].create({
'start_time': 10,
'end_time': 15,
'duration_days': 1,
'role_id': planning_role.id,
})
with Form(self.env['planning.slot']) as slot_form:
slot_form.template_id = template
slot_form.start_datetime = False
slot_form.template_id = self.template
self.assertEqual(slot_form.template_id, self.template)
def test_shift_switching(self):
""" The purpose of this test is to check the main back-end mechanism of switching shifts between employees """
bert_user = new_test_user(self.env,
login='bert_user',
groups='planning.group_planning_user',
name='Bert User',
email='user@example.com')
self.employee_bert.user_id = bert_user.id
joseph_user = new_test_user(self.env,
login='joseph_user',
groups='planning.group_planning_user',
name='Joseph User',
email='juser@example.com')
self.employee_joseph.user_id = joseph_user.id
# Lets first try to switch a shift that is in the past - should throw an error
self.slot.resource_id = self.employee_bert.resource_id
self.assertEqual(self.slot.is_past, True, 'The shift for this test should be in the past')
with self.assertRaises(UserError):
self.slot.with_user(bert_user).action_switch_shift()
# Lets now try to switch a shift that is not ours - it should again throw an error
self.assertEqual(self.slot.resource_id, self.employee_bert.resource_id, 'The shift should be assigned to Bert')
with self.assertRaises(UserError):
self.slot.with_user(joseph_user).action_switch_shift()
# Lets now to try to switch a shift that is both in the future and is ours - this should not throw an error
test_slot = self.env['planning.slot'].create({
'start_datetime': datetime.now() + relativedelta(days=2),
'end_datetime': datetime.now() + relativedelta(days=4),
'state': 'published',
'employee_id': bert_user.employee_id.id,
'resource_id': self.employee_bert.resource_id.id,
})
with self.mock_mail_gateway():
self.assertEqual(test_slot.request_to_switch, False, 'Before requesting to switch, the request to switch should be False')
test_slot.with_user(bert_user).action_switch_shift()
self.assertEqual(test_slot.request_to_switch, True, 'After the switch action, the request to switch should be True')
# Lets now assign another user to the shift - this should remove the request to switch and assign the shift
test_slot.with_user(joseph_user).action_self_assign()
self.assertEqual(test_slot.request_to_switch, False, 'After the assign action, the request to switch should be False')
self.assertEqual(test_slot.resource_id, self.employee_joseph.resource_id, 'The shift should now be assigned to Joseph')
# Lets now create a new request and then change the start datetime of the switch - this should remove the request to switch
test_slot.with_user(joseph_user).action_switch_shift()
self.assertEqual(test_slot.request_to_switch, True, 'After the switch action, the request to switch should be True')
test_slot.write({'start_datetime': (datetime.now() + relativedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S")})
self.assertEqual(test_slot.request_to_switch, False, 'After the change, the request to switch should be False')
self.assertEqual(len(self._new_mails), 1)
self.assertMailMailWEmails(
[bert_user.partner_id.email],
None,
author=joseph_user.partner_id,
)
@freeze_time("2023-11-20")
def test_shift_creation_from_role(self):
self.env.user.tz = 'Asia/Kolkata'
self.env.user.company_id.resource_calendar_id.tz = 'Asia/Kolkata'
PlanningRole = self.env['planning.role']
PlanningTemplate = self.env['planning.slot.template']
role_a = PlanningRole.create({'name': 'role a'})
role_b = PlanningRole.create({'name': 'role b'})
template_a = PlanningTemplate.create({
'start_time': 8,
'end_time': 10,
'duration_days': 1,
'role_id': role_a.id
})
self.assertEqual(template_a.duration_days, 1, "Duration in days should be a 1 day according to resource calendar.")
self.assertEqual(template_a.end_time, 10.0, "End time should be 2 hours from start hours.")
template_b = PlanningTemplate.create({
'start_time': 8,
'end_time': 12,
'duration_days': 1,
'role_id': role_b.id
})
slot = self.env['planning.slot'].create({'template_id': template_a.id})
self.assertEqual(slot.role_id.id, slot.template_autocomplete_ids.mapped('role_id').id, "Role of the slot and shift template should be same.")
slot.template_id = template_b.id
self.assertEqual(slot.role_id.id, slot.template_autocomplete_ids.mapped('role_id').id, "Role of the slot and shift template should be same.")
def test_manage_archived_resources(self):
with freeze_time("2020-04-22"):
self.env.user.tz = 'UTC'
slot_1, slot_2, slot_3 = self.env['planning.slot'].create([
{
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2020, 4, 20, 8, 0),
'end_datetime': datetime(2020, 4, 24, 17, 0),
},
{
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2020, 4, 20, 8, 0),
'end_datetime': datetime(2020, 4, 21, 17, 0),
},
{
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2020, 4, 23, 8, 0),
'end_datetime': datetime(2020, 4, 24, 17, 0),
},
])
slot1_initial_end_date = slot_1.end_datetime
slot2_initial_end_date = slot_2.end_datetime
self.resource_bert.employee_id.action_archive()
self.assertEqual(slot_1.end_datetime, datetime.combine(fields.Date.today()+ timedelta(days=1), time.min), 'End date of the splited shift should be today')
self.assertNotEqual(slot_1.end_datetime, slot1_initial_end_date, 'End date should be updated')
self.assertEqual(slot_2.end_datetime, slot2_initial_end_date, 'End date should be the same')
self.assertFalse(slot_3.resource_id, 'Resource should be the False for archeived resource shifts')
def test_avoid_rounding_error_when_creating_template(self):
"""
Regression test: in some odd circumstances,
a floating point error during the divmod conversion from float -> hours/min can lead to incorrect minutes
5.1 after a divmod(1) gives back minutes = 0.0999999999964 instead of 1, hence the source of error
"""
template = self.env['planning.slot.template'].create({
'start_time': 8,
'end_time': 13.1,
'duration_days': 1,
})
slot = self.env['planning.slot'].create({
'start_datetime': datetime(2021, 1, 1, 0, 0),
'end_datetime': datetime(2021, 1, 1, 23, 59),
})
slot.write({
'template_id': template.id,
})
self.assertEqual(slot.end_datetime.minute, 6, 'The min should be 6, just like in the template, not 5 due to rounding error')
def test_end_time_rounding_edge_case(self):
"""
Test to ensure 0.996 doesn't round to 60,
minutes need to be between 0 and 59.
"""
shift_template = self.env['planning.slot.template'].create({
'start_time': 8.995,
'end_time': 17.996,
'duration_days': 1,
})
self.assertEqual(re.sub(r'\s+', ' ', shift_template.name), '8:59 - 17:59')
def test_copy_planning_shift(self):
""" Test state of the planning shift is only copied once we are in the planning split tool
Test Case:
=========
1) Create a planning shift with state published.
2) Copy the planning shift as we are in the planning split tool (planning_split_tool=True in the context).
3) Check the state of the new planning shift is published.
4) Copy the planning shift as we are not in the planning split tool (planning_split_tool=False in the context).
5) Check the state of the new planning shift is draft.
6) Copy the planning shift without the context (= diplicate a shift).
7) Check the state of the new planning shift is draft.
"""
self.env.user.tz = 'UTC'
slot = self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2020, 4, 20, 8, 0),
'end_datetime': datetime(2020, 4, 24, 17, 0),
'state': 'published',
})
self.assertEqual(slot.state, 'published', 'The state of the shift should be published')
slot1 = slot.with_context(planning_split_tool=True).copy()
self.assertEqual(slot1.state, 'published', 'The state of the shift should be copied')
slot2 = slot.with_context(planning_split_tool=False).copy()
self.assertEqual(slot2.state, 'draft', 'The state of the shift should not be copied')
slot3 = slot.copy()
self.assertEqual(slot3.state, 'draft', 'The state of the shift should not be copied')
def test_calculate_slot_duration_flexible_hours(self):
""" Ensures that _calculate_slot_duration function rounds up days only when there is an extra non-full day left """
employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'tz': 'UTC',
})
employee.resource_id.calendar_id = self.flex_40h_calendar
# the diff between start and end is exactly 6 days
planning_slot_1 = self.env['planning.slot'].create({
'resource_id': employee.resource_id.id,
'start_datetime': datetime(2024, 2, 23, 6, 0, 0),
'end_datetime': datetime(2024, 2, 29, 6, 0, 0),
})
self.assertEqual(planning_slot_1.allocated_hours, 54.0, "day 23, 24 and 25 belong to week 8 (8*3 = 24h) / day 26, 27, 28 (8*3 = 24h) and 29 (6 hours from 0h to 6h) belong to week 9")
# the diff between start and end is 6 days and 8 hours, hence the diff should be approximated to 7 days
planning_slot_2 = self.env['planning.slot'].create({
'resource_id': employee.resource_id.id,
'start_datetime': datetime(2024, 2, 18, 8, 0, 0),
'end_datetime': datetime(2024, 2, 24, 16, 0, 0),
})
self.assertEqual(planning_slot_2.allocated_hours, 40.0, "all days belong to same week = 8, allocated_hours is limited to 40 hours which is the week limit")
def test_auto_plan_employee_with_break_company_no_breaks(self):
""" Test auto-planning an employee with break, while company calendar without breaks
Test Case:
=========
1) Create company calendar with 24 hours per day.
2) Create employee with night shifts calendar, with 30 minutes break at midnight.
3) Create shift from 21:30 to 6:00 with 8 allocated hours.
4) Auto-plan the shift.
5) Check the shift is assigned to the employee.
6) Check the allocated hours remain the same.
"""
# Create a 24-hour company calendar
calendar_24hr = self.env['resource.calendar'].create({
'name': '24/24 Company Calendar',
'tz': 'UTC',
'hours_per_day': 24.0,
'attendance_ids': [
(0, 0, {'name': 'Morning ' + str(day), 'dayofweek': str(day), 'hour_from': 0, 'hour_to': 12, 'day_period': 'morning'})
for day in range(7)
] + [
(0, 0, {'name': 'Afternoon ' + str(day), 'dayofweek': str(day), 'hour_from': 12, 'hour_to': 24, 'day_period': 'afternoon'})
for day in range(7)
],
})
self.env.user.company_id.resource_calendar_id = calendar_24hr
night_shifts_calendar = self.env['resource.calendar'].create({
'name': 'Night Shifts Calendar',
'tz': 'UTC',
'hours_per_day': 8.0,
'attendance_ids': [
(0, 0, {'name': 'Afternoon ' + str(day), 'dayofweek': str(day), 'hour_from': 21.5, 'hour_to': 24, 'day_period': 'afternoon'})
for day in range(7)
] + [
(0, 0, {'name': 'Break ' + str(day), 'dayofweek': str(day), 'hour_from': 0, 'hour_to': 0.5, 'day_period': 'lunch'})
for day in range(7)
] + [
(0, 0, {'name': 'morning ' + str(day), 'dayofweek': str(day), 'hour_from': 0.5, 'hour_to': 6, 'day_period': 'morning'})
for day in range(7)
],
})
role = self.env['planning.role'].create({'name': 'test role'})
# Create an employee linked to this calendar
night_employee = self.env['hr.employee'].create({
'name': 'Night employee',
'resource_calendar_id': night_shifts_calendar.id,
'default_planning_role_id': role.id,
})
# Create a shift from 21:30 to 6:00 with an allocated 8 hours
night_shift = self.env['planning.slot'].create({
'name': 'Night Shift',
'start_datetime': datetime(2024, 5, 10, 21, 30),
'end_datetime': datetime(2024, 5, 11, 6, 0),
'role_id': role.id,
})
night_shift.allocated_hours = 8
# Execute auto-plan to assign the employee
night_shift.auto_plan_id()
self.assertEqual(night_shift.resource_id, night_employee.resource_id, 'The night shift should be assigned to the night employee')
self.assertEqual(night_shift.allocated_hours, 8, 'The allocated hours should remain the same')
self.assertEqual(night_shift.allocated_percentage, 100, 'The allocated percentage should be 100% as the resource will work the allocated hours')
def test_auto_plan_undo_preserves_allocated_hours(self):
"""
When undoing auto-plan, the system should preserve the original allocated_hours.
Test Case:
=========
1) Create an open shift with 6 allocated hours.
2) Auto-plan the shift to assign it to a resource.
3) Verify the shift is assigned and allocated_hours remains 6.
4) Undo the auto-plan assignment.
5) Check allocated_hours is still 6.
"""
start_dt = datetime(2025, 12, 25, 9, 0, 0)
end_dt = datetime(2025, 12, 25, 17, 0, 0)
role = self.env['planning.role'].create({'name': 'Developer Test Role'})
self.employee_joseph.default_planning_role_id = role.id
open_shift = self.env['planning.slot'].create({
'start_datetime': start_dt,
'end_datetime': end_dt,
'allocated_hours': 6.0,
'resource_id': False,
'role_id': role.id,
})
self.assertEqual(open_shift.allocated_hours, 6.0, 'Initial allocated_hours should be 6.0')
self.assertFalse(open_shift.resource_id, 'Should be an open shift')
result = self.env['planning.slot'].with_context(
default_start_datetime=start_dt,
default_end_datetime=end_dt,
).auto_plan_ids([('id', '=', open_shift.id)])
open_shift_assigned = result.get('open_shift_assigned', [])
self.assertEqual(len(open_shift_assigned), 1, 'One shift should be assigned')
open_shift.invalidate_recordset()
self.assertTrue(open_shift.resource_id, 'Shift should be assigned to a resource')
self.assertEqual(open_shift.allocated_hours, 6.0, 'allocated_hours should still be 6.0 after auto-plan')
self.env['planning.slot'].action_rollback_auto_plan_ids(result)
open_shift.invalidate_recordset()
self.assertFalse(open_shift.resource_id, 'Shift should be back to open state')
self.assertEqual(open_shift.allocated_hours, 6.0, 'allocated_hours should be preserved at 6.0 after undo')
def test_write_multiple_slots(self):
""" Test that we can write a resource_id on multiple slots at once. """
slots = self.env['planning.slot'].create([
{'start_datetime': datetime(2024, 5, 10, 8, 0), 'end_datetime': datetime(2024, 5, 10, 17, 0)},
{'start_datetime': datetime(2024, 6, 10, 8, 0), 'end_datetime': datetime(2024, 6, 10, 17, 0)},
])
slots.write({'resource_id': self.resource_bert.id})
self.assertEqual(slots.resource_id, self.resource_bert)
def test_write_without_resource(self):
slot = self.env['planning.slot'].create(
{'start_datetime': datetime(2024, 5, 10, 8, 0), 'end_datetime': datetime(2024, 5, 10, 17, 0)}
)
slot.write({
'repeat' : True,
'recurrence_update': 'all',
'start_datetime': datetime(2024, 5, 10, 9, 0),
'end_datetime': datetime(2024, 5, 10, 18, 0),
})
self.assertRecordValues(slot, [{
'repeat': True,
'start_datetime': datetime(2024, 5, 10, 9, 0),
'end_datetime': datetime(2024, 5, 10, 18, 0),
}])
def test_compute_company_planning_slot(self):
self.assertEqual(self.slot.company_id, self.env.company, "The slot's company should be the current one.")
company = self.env['res.company'].create({"name": "Test company"})
self.resource_bert.company_id = company.id
self.slot.resource_id = self.resource_bert.id
self.assertEqual(self.slot.company_id, company, "The slot's company should be the resource's one.")
def test_flexible_contract_slot(self):
"""
A flexible contract should have no constraints on the slots in terms of start/end time,
but the duration cannot exceed the hours_per_day defined in the contract.
"""
# Create a shift longer than the calendar's hours_per_day
self.employee_bert.resource_calendar_id = self.flex_50h_calendar
slot = self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2022, 1, 11, 3, 0),
'end_datetime': datetime(2022, 1, 11, 23, 0),
'state': 'published',
})
self.assertEqual(slot.allocated_hours, 10.0, 'The allocated hours should be 10.0')
self.assertEqual(slot.allocated_percentage, 100, 'The allocated percentage should be 100%%')
# Create a night shift that spans over two days, but shorter than the calendar's hours_per_day
slot = self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2022, 1, 12, 22, 0),
'end_datetime': datetime(2022, 1, 13, 4, 0),
'state': 'published',
})
self.assertEqual(slot.allocated_hours, 6.0, 'The allocated hours should be 6.0')
self.assertEqual(slot.allocated_percentage, 100, 'The allocated percentage should be 100%%')
# Create a night shift that spans over two days and is longer than the calendar's hours_per_day
slot = self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2022, 1, 13, 20, 0),
'end_datetime': datetime(2022, 1, 14, 10, 0),
'state': 'published',
})
self.assertEqual(slot.allocated_hours, 14.0, '4 hours available on day 13, 10 hours available on day 14, no daily limit excedded in both days')
self.assertEqual(slot.allocated_percentage, 100, 'The allocated percentage should be 100%%')
# Changing the allocated time percentage should be reflected in the allocated hours
slot.allocated_percentage = 50
self.assertEqual(slot.allocated_hours, 7.0, 'The allocated hours should be 5.0 after changing the allocated percentage to 50%%')
def test_fully_flexible_contract_slot(self):
"""
A fully flexible contract should not have any constraints on the slots in terms of duration and start time.
"""
self.employee_bert.resource_calendar_id = False
slot = self.env['planning.slot'].create({
'resource_id': self.resource_bert.id,
'start_datetime': datetime(2022, 1, 11, 4, 0),
'end_datetime': datetime(2022, 1, 12, 22, 0),
'state': 'published',
})
self.assertEqual(slot.allocated_hours, 42.0, 'The allocated hours should be 42.0')
self.assertEqual(slot.allocated_percentage, 100, 'The allocated percentage should be 100%%')
# Changing the allocated time percentage should be reflected in the allocated hours
slot.allocated_percentage = 50
self.assertEqual(slot.allocated_hours, 21.0, 'The allocated hours should be 21.0 after changing the allocated percentage to 50%%')
def test_open_shift_planning_slot_including_weekend(self):
"""
When an open shift is scheduled spanning between weekday and weekends (e.g. Sunday 8 AM to Monday 5 PM),
allocated time should be equal to 16h instead of 8h (same behavior as for employees working flexible hours):
"""
slot = self.env['planning.slot'].create({
'resource_id': False,
'start_datetime': datetime(2022, 1, 16, 8, 0), # Sunday 8AM
'end_datetime': datetime(2022, 1, 17, 17, 0), # Monday 5PM
'state': 'published',
})
self.assertEqual(slot.allocated_hours, 16.0, 'The allocated hours should be 16.0 for the open shift')
self.assertEqual(slot.allocated_percentage, 100, 'The allocated percentage should be 100%%')
@freeze_time('2021-01-01')
def test_allocated_hours_when_template_is_during_a_break(self):
self.resource_janice.tz = 'UTC'
template_slot = self.env['planning.slot.template'].create({
'start_time': 11,
'end_time': 16,
})
slot = self.env['planning.slot'].create({
'start_datetime': datetime(2021, 1, 1, 0, 0),
'end_datetime': datetime(2021, 1, 1, 23, 59),
'resource_id': self.resource_janice.id,
})
slot.write({
'template_id': template_slot.id,
})
self.assertEqual(slot.start_datetime, datetime(2021, 1, 1, 11, 0))
self.assertEqual(slot.end_datetime, datetime(2021, 1, 1, 16, 0))
self.assertEqual(slot.allocated_hours, 4)
def test_allocated_hours_shift_duplication(self):
self.slot.resource_id = self.resource_joseph
self.assertEqual(self.slot.allocated_hours, 8)
slot2 = self.slot.copy({'resource_id': self.resource_bert.id})
self.assertEqual(slot2.allocated_hours, 4, "The allocated hours should have been recomputed with the new resource after copying the shift.")
def test_planning_expand_resource(self):
"""
When planning_expand_resource = True and there are slots assigned to the resource in the previous or next period,
the resource is also displayed in the gantt view with value = 0.
"""
self.employee_bert.resource_calendar_id = self.flex_50h_calendar
self.slot.resource_id = self.employee_bert.resource_id
group_by = ['resource_id']
planned_dates = [
('2019-07-01 00:00:00', '2019-07-31 23:59:59'),
('2019-08-01 00:00:00', '2019-08-31 23:59:59')
]
for case, (start_date, stop_date) in enumerate(planned_dates):
result = self.env['planning.slot'].with_context(planning_expand_resource=True).get_gantt_data([
'&',
['start_datetime', '<', stop_date],
['end_datetime', '>', start_date],
], group_by, {'display_name': {}}, unavailability_fields=group_by, progress_bar_fields=group_by, start_date=start_date, stop_date = stop_date,scale='month')
if case == 0:
self.assertTrue(self.slot.resource_id.id in result['progress_bars']['resource_id'], "Resource has slots in the previous month")
self.assertEqual(result['progress_bars']['resource_id'][self.slot.resource_id.id]['value'], 0.0)
else:
self.assertFalse(self.slot.resource_id.id in result['progress_bars']['resource_id'])
def test_allocated_hours_open_shift(self):
""" Ensure that the allocated hours for an open shift are correctly computed based on the
company calendar. """
self.employee_joseph.user_id = self.env.user.id
PlanningSlot = self.env["planning.slot"]
# Create a slot NOT during the employee working hours
slot = PlanningSlot.create({
'start_datetime': datetime(2019, 5, 1, 8, 0),
'end_datetime': datetime(2019, 5, 1, 17, 0),
})
self.assertEqual(
slot.allocated_hours,
8.0,
"The allocated hours should be 8.0 for the open shift based on the company calendar",
)
# Create a slot during the employee working hours
slot = PlanningSlot.create({
'start_datetime': datetime(2019, 5, 2, 8, 0),
'end_datetime': datetime(2019, 5, 2, 17, 0),
})
self.assertEqual(
slot.allocated_hours,
8.0,
"The allocated hours should be 8.0 for the open shift based on the company calendar",
)
def test_planning_slot_default_datetime(self):
""" This test ensures that when selecting the datetime in Gantt view, the default hours are set correctly """
self.resource_joseph.tz = 'Europe/Brussels'
PlanningSlot = self.env['planning.slot'].with_user(self.env.user).with_context(
default_start_datetime='2024-07-04 12:00:00',
default_end_datetime='2024-07-04 12:59:59',
default_resource_id=self.resource_joseph.id,
)
slot = PlanningSlot.default_get(['resource_id', 'start_datetime', 'end_datetime'])
self.assertEqual(slot.get('start_datetime'), datetime(2024, 7, 4, 10, 0, 0), "The slot start datetime should be matched to the resource's timezone")
self.assertEqual(slot.get('end_datetime'), datetime(2024, 7, 4, 10, 59, 59), "The slot end datetime should be matched to the resource's timezone")
def test_copy_shift_without_archive_resource(self):
self.slot.resource_id = self.resource_joseph
self.slot2.resource_id = self.resource_bert
self.resource_joseph.action_archive()
slots = self.slot + self.slot2
slot, slot2 = slots.copy()
self.assertFalse(slot.resource_id)
self.assertEqual(slot2.resource_id, self.resource_bert)
# Exception we keep the archived resource if it is given in parameter of copy method
slot, slot2 = slots.copy({'resource_id': self.resource_joseph.id})
self.assertEqual(slot.resource_id, self.resource_joseph)
self.assertEqual(slot2.resource_id, self.resource_joseph)
# Exception we keep the archived resource if the shift is split
slot = self.slot.with_context(planning_split_tool=True).copy()
self.assertEqual(slot.resource_id, self.resource_joseph)
def test_unavailability_open_shift(self):
""" Ensure that there is no unavailabilities for open shifts. """
gantt_unavailabilities = self.env['planning.slot']._gantt_unavailability(
'resource_id',
self.resource_bert.ids,
datetime(2024, 1, 1),
datetime(2024, 1, 7),
'month',
)
self.assertEqual(gantt_unavailabilities[False], [], 'There should be no unavailability for open shifts.')
self.assertNotEqual(gantt_unavailabilities[self.resource_bert.id], [], 'There should be unavailabilities for Bert.')
def test_batch_creation_from_calendar(self):
"""
This test ensure that when planning slots are created from the "create multi" of the calendar view inconsistent slot
are not created.
employee with standard calendar : the slot is valid if it is contained at least partially in the employee's schedule.
e.a. employee with 9-17 working schedule. slot 8-12 is valid. slot 18-20 is invalid.
employee with flexible working hours : all slots are valid.
"""
template_valid, template_invalid = self.env['planning.slot.template'].create([{
'start_time': 8, 'end_time': 12, 'duration_days': 1,
}, {
'start_time': 18, 'end_time': 20, 'duration_days': 1,
}])
self.employee_bert.resource_calendar_id = False
self.employee_joseph.resource_calendar_id = self.company_calendar
slot_joseph, slot_bert = self.env['planning.slot'].create_batch_from_calendar([{
'start_datetime': '2025-04-04 08:00:00', 'end_datetime': '2025-04-04 12:00:00',
'resource_id': resource.id, 'template_id': template_valid.id,
} for resource in (self.resource_joseph, self.resource_bert)
])
self.assertEqual(slot_joseph.resource_id, self.resource_joseph)
self.assertEqual(slot_joseph.start_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-04 08:00:00')
self.assertEqual(slot_joseph.end_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-04 12:00:00')
self.assertEqual(slot_bert.resource_id, self.resource_bert)
self.assertEqual(slot_bert.start_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-04 08:00:00')
self.assertEqual(slot_bert.end_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-04 12:00:00')
slot_bert = self.env['planning.slot'].create_batch_from_calendar([{
'start_datetime': '2025-04-04 18:00:00', 'end_datetime': '2025-04-04 20:00:00',
'resource_id': resource.id, 'template_id': template_invalid.id,
} for resource in (self.resource_joseph, self.resource_bert)
])
self.assertEqual(slot_bert.resource_id, self.resource_bert)
self.assertEqual(slot_bert.start_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-04 18:00:00')
self.assertEqual(slot_bert.end_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-04 20:00:00')
def test_batch_creation_from_calendar_with_duration_days_template(self):
"""
This test ensure that when planning slots are created from the "create multi" of the calendar view with shift
template with duration days > 1, then the unavailable days are skipped.
Test case :
Create 2 new slots for Bert, flexible employee.
- weekend are ignored.
- start dates : Monday 07, Tuesday 08
- expected end dates : Friday 11, Saturday 12
Create 2 new slots for Joseph, fixed schedule 40h
- weekend are computed
- start dates : Monday 07, Tuesday 08
- expected end dates: Friday 11, Monday 14
"""
shift_template = self.env['planning.slot.template'].create({
'start_time': 8, 'end_time': 12, 'duration_days': 5
})
self.employee_bert.resource_calendar_id = False
self.employee_joseph.resource_calendar_id = self.company_calendar
slot_joseph_1, slot_bert_1, slot_joseph_2, slot_bert_2 = self.env['planning.slot'].create_batch_from_calendar([{
'start_datetime': f'2025-04-{day[0]} 08:00:00', 'end_datetime': f'2025-04-{day[1]} 12:00:00',
'resource_id': resource.id, 'template_id': shift_template.id,
} for day in [['07', '11'], ['08', '12']] for resource in (self.resource_joseph, self.resource_bert)
])
self.assertEqual(slot_joseph_1.resource_id, self.resource_joseph)
self.assertEqual(slot_joseph_2.resource_id, self.resource_joseph)
self.assertEqual(slot_bert_1.resource_id, self.resource_bert)
self.assertEqual(slot_bert_2.resource_id, self.resource_bert)
self.assertEqual(slot_joseph_1.start_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-07 08:00:00')
self.assertEqual(slot_joseph_2.start_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-08 08:00:00')
self.assertEqual(slot_bert_1.start_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-07 08:00:00')
self.assertEqual(slot_bert_2.start_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-08 08:00:00')
self.assertEqual(slot_joseph_1.end_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-11 12:00:00')
self.assertEqual(slot_joseph_2.end_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-14 12:00:00')
self.assertEqual(slot_bert_1.end_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-11 12:00:00')
self.assertEqual(slot_bert_2.end_datetime.strftime('%Y-%m-%d %H:%M:%S'), '2025-04-12 12:00:00')
def test_copy_slots_when_time_off(self):
"""
week_1: 19-01-2020 -> 25-01-2020
original slot: 20-01-2020 08:00 -> 24-01-2020 17:00 (5 days)
allocated_hours: 50 hours and allocated_percentage: 125
--------------------------------------------------------------------------------------------
week_2: 26-01-2020 -> 01-02-2020
resource on leave: 28-01-2020 8:00 -> 29-01-2020 17:00 (2 days i.e 16 hours)
copy slot: 27-01-2020 08:00 -> 31-01-2020 17:00
-------------------------------------------------------------------------------------------
Expected result:
Total 4 slots will create, 3 slot assigned to resource and 1 open slot
1) 27-01-2020 08:00 -> 27-01-2020 12:00 (4 hrs)(assigned slot)
2) 27-01-2020 13:00 -> 27-01-2020 19:00 (4 hrs)(assigned slot)
3) 28-01-2020 08:00 -> 29-01-2020 19:00 (16 hrs)(open slot)
4) 30-01-2020 08:00 -> 31-01-2020 19:00 (16 hrs)(assigned slot)
"""
employee_bert = self.env['hr.employee'].create({
'name': 'Test',
'work_email': 'test@test.in',
'tz': 'UTC',
'employee_type': 'freelance',
'create_date': '2015-01-01 00:00:00',
'resource_calendar_id': self.company_calendar.id,
})
PlanningSlot = self.env['planning.slot']
dt = datetime(2020, 1, 20, 0, 0)
slot = PlanningSlot.create({
'resource_id': employee_bert.resource_id.id,
'start_datetime': dt + relativedelta(hours=8),
'end_datetime': dt + relativedelta(days=4, hours=17),
})
self.env['resource.calendar.leaves'].create({
'name': "I go to my father-in-law's",
'calendar_id': employee_bert.resource_id.calendar_id.id,
'date_from': dt + relativedelta(weeks=1, days=1),
'date_to': dt + relativedelta(weeks=1, days=2, hours=17),
'resource_id': employee_bert.resource_id.id,
})
copied, _dummy = PlanningSlot.action_copy_previous_week(
str(dt + relativedelta(weeks=1)), [
['start_datetime', '<=', dt + relativedelta(weeks=1)],
['end_datetime', '>=', dt],
['resource_id', '=', employee_bert.resource_id.id],
]
)
copied_slot = PlanningSlot.browse(copied)
open_slot = copied_slot.filtered(lambda x: not x.resource_id)
self.assertEqual(len(open_slot), 4, "4 shift should be copied as open, as the employee is on off")
self.assertEqual(sum(open_slot.mapped('allocated_hours')), 16, "16 hours should be allocated to open slot")
self.assertEqual(slot.allocated_hours, sum(copied_slot.mapped('allocated_hours')),
"The allocated hours of slot and allocated hours of copied slots must be same")
def test_change_planning_template_start_or_end_time_to_invalid_value(self):
with self.assertRaises(ValidationError):
self.template.write({'end_time': 24})
self.template.read()
with self.assertRaises(ValidationError):
self.template.write({'start_time': 24})
self.template.read()
self.assertEqual(self.template.end_time, 14)
self.assertEqual(self.template.start_time, 11)
def test_auto_plan_flexible_employee_no_rate_no_hours_day_overload(self):
self.env.user.tz = 'UTC'
shift1, shift2 = self.env['planning.slot'].create([{
'name': 'Night Shift',
'start_datetime': datetime(2023, 7, 28, 8),
'end_datetime': datetime(2023, 7, 28, 16, 0),
'role_id': self.flex_role.id,
'allocated_hours': 4,
}, {
'name': 'Night Shift',
'start_datetime': datetime(2023, 7, 28, 8),
'end_datetime': datetime(2023, 7, 28, 16, 0),
'role_id': self.flex_role.id,
'allocated_hours': 3,
}])
res = self.env["planning.slot"].with_context(
default_start_datetime="2023-07-26 22:00:00",
default_end_datetime="2023-08-01 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2023-08-01 22:00:00'], ['end_datetime', '>', '2023-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [shift1.id, shift2.id])
self.assertEqual(shift1.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift2.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift1.allocated_hours, 4.0, "should be the same original value")
self.assertEqual(shift1.allocated_percentage, 50.0, "4 allocated hours / 8 working hours")
self.assertEqual(shift2.allocated_hours, 3.0, "should be the same original value")
self.assertEqual(shift2.allocated_percentage, 37.5, "4 allocated hours / 8 working hours")
shift3, shift4 = self.env['planning.slot'].create([{
'name': 'Night Shift',
'start_datetime': datetime(2023, 7, 28, 8),
'end_datetime': datetime(2023, 7, 28, 16),
'role_id': self.flex_role.id,
'allocated_hours': 1,
}, {
'name': 'Night Shift',
'start_datetime': datetime(2023, 7, 28, 8),
'end_datetime': datetime(2023, 7, 28, 16),
'role_id': self.flex_role.id,
'allocated_hours': 2,
}])
res = self.env["planning.slot"].with_context(
default_start_datetime="2023-07-26 22:00:00",
default_end_datetime="2023-08-01 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2023-08-01 22:00:00'], ['end_datetime', '>', '2023-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [shift3.id], "shift 4 cannot be planned as it will create an overload")
self.assertEqual(shift3.resource_id.employee_id, self.flex_employee, "allocated_hours = 4 + 3 + 1 = 8 hours / allocated_percentage = 50 + 12.5 + 37.5 = 100%")
self.assertFalse(shift4.resource_id.employee_id)
self.assertEqual(shift3.allocated_hours, 1.0, "should be the same original value")
self.assertEqual(shift3.allocated_percentage, 12.5, "1 allocated hour / 8 working hours")
shift5 = self.env['planning.slot'].create({
'name': 'Night Shift',
'start_datetime': datetime(2023, 7, 28, 6),
'end_datetime': datetime(2023, 7, 28, 8),
'role_id': self.flex_role.id,
'allocated_hours': 2,
})
res = self.env["planning.slot"].with_context(
default_start_datetime="2023-07-26 22:00:00",
default_end_datetime="2023-08-01 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2023-08-01 22:00:00'], ['end_datetime', '>', '2023-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [], "8 hours already consumed on day 28 from 8h to 16h")
self.employee_bert.write({
'resource_calendar_id': self.calendar_40h_flex.id,
'default_planning_role_id': self.flex_role.id,
'tz': 'UTC',
})
res = self.env["planning.slot"].with_context(
default_start_datetime="2023-07-26 22:00:00",
default_end_datetime="2023-08-01 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2023-08-01 22:00:00'], ['end_datetime', '>', '2023-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [shift4.id, shift5.id])
self.assertEqual(shift4.resource_id.employee_id, self.employee_bert)
self.assertEqual(shift5.resource_id.employee_id, self.employee_bert)
self.assertEqual(shift4.allocated_hours, 2.0, "should be the same original value")
self.assertEqual(shift4.allocated_percentage, 25.0, "2 allocated hours / 8 working hours")
self.assertEqual(shift5.allocated_hours, 2.0, "should be the same original value")
self.assertEqual(shift5.allocated_percentage, 100.0, "2 allocated hour / 2 working hours")
def test_auto_plan_fully_flexible_employee_no_rate_no_hours_day_overload(self):
self.env.user.tz = 'UTC'
self.flex_employee.resource_calendar_id = False
shift1, shift2, shift3, shift4 = self.env['planning.slot'].create([{
'name': 'Shift 1',
'start_datetime': datetime(2023, 7, 28, 2),
'end_datetime': datetime(2023, 7, 28, 23),
'role_id': self.flex_role.id,
'allocated_hours': 10.5,
}, {
'name': 'Shift 2',
'start_datetime': datetime(2023, 7, 28, 2),
'end_datetime': datetime(2023, 7, 28, 23),
'role_id': self.flex_role.id,
'allocated_hours': 10.5,
}, {
'name': 'Shift 3',
'start_datetime': datetime(2023, 7, 28, 0),
'end_datetime': datetime(2023, 7, 28, 2),
'role_id': self.flex_role.id,
'allocated_hours': 2,
}, {
'name': 'Shift 4',
'start_datetime': datetime(2023, 7, 28, 23),
'end_datetime': datetime(2023, 7, 29),
'role_id': self.flex_role.id,
'allocated_hours': 1,
}])
res = self.env["planning.slot"].with_context(
default_start_datetime="2023-07-26 22:00:00",
default_end_datetime="2023-08-01 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2023-08-01 22:00:00'], ['end_datetime', '>', '2023-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [shift1.id, shift2.id, shift3.id, shift4.id])
self.assertEqual(shift1.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift2.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift3.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift4.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift1.allocated_hours, 10.5, "should be the same original value")
self.assertEqual(shift1.allocated_percentage, 50.0, "10.5 allocated hours / 21 working hours from 2h to 23h")
self.assertEqual(shift2.allocated_hours, 10.5, "should be the same original value")
self.assertEqual(shift2.allocated_percentage, 50.0, "10.5 allocated hours / 21 working hours from 2h to 23h")
self.assertEqual(shift3.allocated_hours, 2.0, "should be the same original value")
self.assertEqual(shift3.allocated_percentage, 100.0, "2 allocated hours / 2 working hours from 0h to 2h")
self.assertEqual(shift4.allocated_hours, 1.0, "should be the same original value")
self.assertEqual(shift4.allocated_percentage, 100.0, "1 allocated hour / 1 working hours from 23h to 0h (next day)")
self.env['planning.slot'].create({
'name': 'Night Shift',
'start_datetime': datetime(2023, 7, 28, 8),
'end_datetime': datetime(2023, 7, 28, 16),
'role_id': self.flex_role.id,
'allocated_hours': 1,
})
res = self.env["planning.slot"].with_context(
default_start_datetime="2023-07-26 22:00:00",
default_end_datetime="2023-08-01 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2023-08-01 22:00:00'], ['end_datetime', '>', '2023-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [], "employee already busy for the 24 hours on day 28")
def test_auto_plan_flexible_employee_no_week_overload(self):
self.env.user.tz = 'UTC'
shift1, shift2, shift3, shift4, shift5 = self.env['planning.slot'].create([{
'name': 'Shift 1',
'start_datetime': datetime(2025, 7, 28, 8),
'end_datetime': datetime(2025, 7, 30, 16),
'role_id': self.flex_role.id,
'allocated_hours': 12,
}, {
'name': 'Shift 2',
'start_datetime': datetime(2025, 7, 28, 8),
'end_datetime': datetime(2025, 7, 30, 16, 0),
'role_id': self.flex_role.id,
'allocated_hours': 12,
}, {
'name': 'Shift 3',
'start_datetime': datetime(2025, 7, 31, 8),
'end_datetime': datetime(2025, 7, 31, 16),
'role_id': self.flex_role.id,
'allocated_hours': 8,
}, {
'name': 'Shift 4',
'start_datetime': datetime(2025, 8, 1, 8),
'end_datetime': datetime(2025, 8, 1, 16),
'role_id': self.flex_role.id,
'allocated_hours': 8,
}, {
'name': 'Shift 5',
'start_datetime': datetime(2025, 8, 2, 8),
'end_datetime': datetime(2025, 8, 2, 16, 0),
'role_id': self.flex_role.id,
'allocated_hours': 8,
}])
res = self.env["planning.slot"].with_context(
default_start_datetime="2025-07-26 22:00:00",
default_end_datetime="2025-08-04 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2025-08-04 22:00:00'], ['end_datetime', '>', '2025-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [shift1.id, shift2.id, shift3.id, shift4.id], "shift 5 cannot be planned as it will create an overload on the week")
self.assertEqual(shift1.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift2.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift3.resource_id.employee_id, self.flex_employee)
self.assertEqual(shift4.resource_id.employee_id, self.flex_employee)
self.assertFalse(shift5.resource_id.employee_id)
# assert _compute_allocated_hours works fine for flexible resources (triggered after setting the resource on the shift, then again when setting the allocated hours)
self.assertEqual(shift1.allocated_hours, 12.0, "should be the same original value")
self.assertEqual(shift1.allocated_percentage, 50.0, "12 allocated hours / 24 working hours from 8h day 28 to 16h day 30")
self.assertEqual(shift2.allocated_hours, 12.0, "should be the same original value")
self.assertEqual(shift2.allocated_percentage, 50.0, "12 allocated hours / 24 working hours from 8h day 28 to 16h day 30")
self.assertEqual(shift3.allocated_hours, 8.0, "should be the same original value")
self.assertEqual(shift3.allocated_percentage, 100.0, "8 allocated hours / 8 working hours from 8h to 16h day 31")
self.assertEqual(shift4.allocated_hours, 8.0, "should be the same original value")
self.assertEqual(shift4.allocated_percentage, 100.0, "8 allocated hours / 8 working hours from 8h to 16h day 01")
def test_auto_plan_fully_flexible_employee_no_hours_limit_per_week(self):
self.env.user.tz = 'UTC'
self.flex_employee.resource_calendar_id = False
shift = self.env['planning.slot'].create({
'name': 'Shift 1',
'start_datetime': datetime(2025, 7, 27),
'end_datetime': datetime(2025, 8, 2),
'role_id': self.flex_role.id,
'allocated_hours': 120.0,
})
res = self.env["planning.slot"].with_context(
default_start_datetime="2025-07-26 22:00:00",
default_end_datetime="2025-08-04 22:00:00",
).auto_plan_ids(['&', ['start_datetime', '<', '2025-08-04 22:00:00'], ['end_datetime', '>', '2025-07-26 22:00:00']])
self.assertEqual(res['open_shift_assigned'], [shift.id])
self.assertEqual(shift.resource_id.employee_id, self.flex_employee)
def test_gantt_progress_bar_split_when_flexible(self):
"""
Test if a slot is shared between two weeks the progress bar
should be split between both weeks. Not showing the whole allocated
hours in both weeks.
"""
self.employee_bert.resource_calendar_id = self.flex_40h_calendar.id
dt = datetime(2025, 8, 22, 0, 0)
self.slot.write({
'resource_id': self.employee_bert.resource_id.id,
'start_datetime': dt + relativedelta(hours=8),
'end_datetime': dt + relativedelta(days=4, hours=17),
})
planning_hours_info_1st_week = self.env['planning.slot']._gantt_progress_bar(
'resource_id', self.employee_bert.resource_id.ids, datetime(2025, 8, 16), datetime(2025, 8, 23, 23, 59)
)
self.assertEqual(self.slot.allocated_hours, 40.0)
self.assertEqual(planning_hours_info_1st_week[self.employee_bert.resource_id.id]['value'], 16)
planning_hours_info_2nd_week = self.env['planning.slot']._gantt_progress_bar(
'resource_id', self.employee_bert.resource_id.ids, datetime(2025, 8, 24), datetime(2025, 8, 30, 23, 59)
)
self.assertEqual(planning_hours_info_2nd_week[self.employee_bert.resource_id.id]['value'], 24)
def test_compute_slots_data(self):
"""Test that planning.send wizard computes slot_ids and employee_ids correctly, including active_domain."""
# Create two employees
employee_a, employee_b = self.env['hr.employee'].create([
{'name': 'Employee A'},
{'name': 'Employee B'},
])
# Create a planning role
role_dev, role_other = self.env['planning.role'].create([
{'name': 'Dev'},
{'name': 'Tester'},
])
# Create slots: one in range (role = Tester), one out of range
slot_in_range = self.env['planning.slot'].create([
{
'start_datetime': datetime(2023, 11, 20, 9, 0),
'end_datetime': datetime(2023, 11, 20, 16, 0),
'employee_id': employee_a.id,
'resource_id': employee_a.resource_id.id,
'resource_type': 'user',
'role_id': role_other.id,
},
{
'start_datetime': datetime(2023, 11, 19, 9, 0),
'end_datetime': datetime(2023, 11, 19, 16, 0),
'employee_id': employee_b.id,
'resource_id': employee_b.resource_id.id,
'resource_type': 'user',
'role_id': role_dev.id,
},
])
slot_in_range = slot_in_range[0]
# Create wizard with a time window that only includes slot_in_range
wizard = self.env['planning.send'].create({
'start_datetime': datetime(2023, 11, 20, 8, 0),
'end_datetime': datetime(2023, 11, 20, 17, 0),
})
wizard._compute_slots_data()
# Wizard should only include slot_in_range
self.assertIn(slot_in_range, wizard.slot_ids, "Wizard should include slots inside the range.")
# Employee_ids should match employee of slot_in_range
self.assertEqual(
wizard.employee_ids,
employee_a,
)
# Now test with active_domain filtering by role = Dev → should exclude slot_in_range
wizard_ctx = wizard.with_context(active_domain=[('role_id', '=', role_dev.id)])
wizard_ctx._compute_slots_data()
self.assertFalse(
wizard_ctx.slot_ids,
)
@freeze_time("2019-5-28 08:00:00")
def test_user_assign_shift_multicompany(self):
company = self.env['res.company'].create({"name": "Test company"})
self.env.user.company_ids += company
test_slot = self.env['planning.slot'].create({
'start_datetime': datetime(2019, 5, 28, 8, 0, 0),
'end_datetime': datetime(2019, 5, 28, 17, 0, 0),
'state': 'published',
'company_id': company.id,
})
with self.assertRaises(UserError):
test_slot.with_company(company).action_self_assign()
employee = self.env['hr.employee'].create({
'name': 'odoobot',
'work_email': 'odoobot@example.com',
'tz': 'UTC',
'employee_type': 'freelance',
'create_date': '2015-01-01 00:00:00',
'user_id': self.env.user.id,
'company_id': company.id,
})
test_slot.with_company(company).action_self_assign()
self.assertEqual(test_slot.employee_id, employee)
def test_avatar_card_archived_employee_info(self):
employee = self.env["hr.employee"].create({
"active": False,
"name": "Test Emp",
})
data = employee.resource_id.get_avatar_card_data(["name"])[0]
self.assertEqual(data.get("name"), "Test Emp")
def test_multi_shift_creation_excludes_non_working_days(self):
"""Ensure multi-shift creation automatically skips weekends (non-working days)."""
self.employee_bert.resource_calendar_id = self.calendar_40h_flex
slots = self.env['planning.slot'].with_context(multi_create=True).create([
{
'start_datetime': datetime(2025, 10, day, 9, 0, 0),
'end_datetime': datetime(2025, 10, day, 17, 0, 0),
'resource_id': resource.id,
'template_id': self.template.id,
} for day in range(5, 12) for resource in [self.resource_janice, self.resource_bert, self.resource_joseph]
])
slots_janice = slots.filtered(lambda slot: slot.resource_id == self.resource_janice)
slots_bert = slots.filtered(lambda slot: slot.resource_id == self.resource_bert)
self.assertEqual(len(slots_janice), 5, "Standard schedule: shifts should be created only on working days.")
self.assertEqual([slot.start_datetime.day for slot in slots_janice], [6, 7, 8, 9, 10], "Excluded 5 and 11 (Sat/Sun)")
self.assertEqual(len(slots_bert), 5, "Flexible schedule: shifts should be created only on working days.")
self.assertEqual([slot.start_datetime.day for slot in slots_bert], [5, 6, 7, 8, 9], "10 and 11 are non-working days.")
def test_planning_send_action_check_emails(self):
start_datetime = datetime(2024, 7, 1, 8, 0)
end_datetime = datetime(2024, 7, 1, 17, 0)
beth_shift, joseph_shift = self.env['planning.slot'].create([
{'start_datetime': start_datetime, 'end_datetime': end_datetime, 'resource_id': self.resource_bert.id},
{'start_datetime': start_datetime, 'end_datetime': end_datetime, 'resource_id': self.resource_joseph.id},
])
self.assertEqual(beth_shift.state, 'draft', 'The shift should be in draft state by default')
self.assertEqual(joseph_shift.state, 'draft', 'The shift should be in draft state by default')
self.employee_bert.work_email = ''
planning_send_wizard = self.env['planning.send'].create({
'start_datetime': start_datetime,
'end_datetime': end_datetime,
'slot_ids': (beth_shift + joseph_shift).ids,
})
self.assertTrue(self.env['hr.employee'].has_access('write'))
action = planning_send_wizard.action_check_emails()
self.assertEqual(action['name'], 'No Email Address for Some Employees')
self.assertEqual(action['type'], 'ir.actions.act_window', 'The action should open a form view to complete missing work email')
# since the user has no access in edit to `hr.employee`, we will not send the planning to the employee without email set.
self.assertFalse(self.env['hr.employee'].with_user(self.planning_manager_user).has_access('write'))
action = planning_send_wizard.with_user(self.planning_manager_user).action_check_emails()
self.assertEqual(action['type'], 'ir.actions.client', 'The action should return a notification')
self.assertEqual(action['tag'], 'display_notification', 'The action should return a notification')
self.assertDictEqual(action['params'], {
'type': 'info',
'message': "Shifts published — employees without a work email were skipped",
'next': {'type': 'ir.actions.act_window_close'},
})
self.assertEqual(beth_shift.state, 'draft', 'The shift should not be in published state after sending the planning since the employee set has no work_email and so no way to receive the planning')
self.assertEqual(joseph_shift.state, 'published', 'The shift should be in published state after sending the planning')
@freeze_time("2026-02-26 14:59:59")
def test_shift_template_updates_end_date(self):
"""checks that, despite an employee having a fixed schedule, the start and end hours of their shift
will correspond to the applied shift template."""
planning_role = self.env['planning.role'].create({
'name': 'role for template',
})
self.employee_joseph.write({'default_planning_role_id': planning_role.id})
template_slot = self.env['planning.slot.template'].create({
'start_time': 11,
'end_time': 19,
'role_id': planning_role.id
})
with Form(self.env['planning.slot']) as slot:
slot.role_id = planning_role
slot.resource_id = self.employee_joseph.resource_id
slot.template_id = template_slot
self.assertEqual(slot.start_datetime, datetime.now().date() + relativedelta(hour=11, minute=0, second=0))
self.assertEqual(slot.end_datetime, datetime.now().date() + relativedelta(hour=19, minute=0, second=0))
def test_planning_gantt_unavailabilities_flexible_employee(self):
employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'tz': 'UTC',
})
employee.resource_id.calendar_id = self.flex_40h_calendar
unavailabilities = self.env['planning.slot']._gantt_unavailability(
'resource_id',
[employee.resource_id.id],
datetime(2019, 1, 1),
datetime(2019, 1, 7),
'week',
)
self.assertNotIn(employee.resource_id.id, unavailabilities)
def test_set_shift_template_on_planning_slot_with_long_calendar_leave(self):
"""
Test that applying a shift template on a planning slot when the
resource calendar has a long leave.
"""
self.env['resource.calendar.leaves'].create({
'name': "Long Leaves",
'calendar_id': self.resource_bert.calendar_id.id,
'date_from': datetime(2019, 6, 5, 8, 0),
'date_to': datetime(2023, 6, 24, 18, 0),
})
self.template.duration_days = 2
self.slot.write({
'resource_id': self.resource_bert.id,
'template_id': self.template.id,
})
self.assertEqual(self.slot.start_datetime, datetime(2019, 6, 27, 11, 0))
self.assertEqual(self.slot.end_datetime, datetime(2019, 6, 28, 14, 0))