Files
2024-04-28 11:43:49 +08:00

437 lines
24 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from datetime import datetime, time, timedelta
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.exceptions import UserError
from odoo import fields
from odoo.exceptions import ValidationError
from odoo.tests.common import Form
from odoo.tests import 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', fields.datetime.now)
with freeze_time('2019-5-1'):
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'}),
],
})
calendar = cls.env['resource.calendar'].create({
'name': 'Classic 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Lunch', 'dayofweek': '0', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Lunch', 'dayofweek': '1', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Lunch', 'dayofweek': '2', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Lunch', 'dayofweek': '4', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
]
})
cls.env.user.company_id.resource_calendar_id = 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,
'duration': 4,
})
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.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_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'
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, 00, 0), 'It should still be the default start_datetime 0am')
self.assertEqual(defaults.get('end_datetime'), datetime(2019, 6, 26, 23, 59, 59), 'It should adjust to employee calendar: 0am -> 9pm')
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, 8, 0), 'It should adjust to employee calendar: 0am -> 9pm')
self.assertEqual(defaults.get('end_datetime'), datetime(2019, 6, 27, 17, 0), 'It should adjust to employee calendar: 0am -> 9pm')
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 pm and duration = 3 hours.
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 duration in the template.
"""
self.resource_bert.calendar_id = False
template_slot = self.env['planning.slot.template'].create({
'start_time': 23,
'duration': 3,
})
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_bert.id,
})
slot.write({
'template_id': template_slot.id,
})
self.assertEqual(slot.start_datetime, datetime(2021, 1, 1, 23, 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, 2, 2, 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, 3, 'The allocated hours of this slot should be the duration defined in the template in the resource timezone.')
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_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,
)
def test_name_long_duration(self):
""" Set an absurdly high duration to ensure we validate it and get an error """
template_slot = self.env['planning.slot.template'].create({
'start_time': 9,
'duration': 100000,
})
with self.assertRaises(ValidationError):
# only try to get the name, this triggers its compute
template_slot.name
@freeze_time("2023-11-20")
def test_shift_creation_from_role(self):
self.env.user.tz = 'Asia/Calcutta'
self.env.user.company_id.resource_calendar_id.tz = 'Asia/Calcutta'
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,
'duration': 2.0,
'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,
'duration': 4.0,
'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,
'duration': 5.1, # corresponds to 5:06
})
self.assertEqual(template.start_time + template.duration, 13.1, 'Template end time should be the start + duration')
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')