1323 lines
63 KiB
Python
1323 lines
63 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
|
|
|
from datetime import datetime, timedelta
|
|
from freezegun import freeze_time
|
|
|
|
from .common import TestCommonPlanning
|
|
from .common import TestPlanningContractCommon
|
|
|
|
import unittest
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests import Form
|
|
|
|
|
|
class TestRecurrencySlotGeneration(TestCommonPlanning):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestRecurrencySlotGeneration, cls).setUpClass()
|
|
cls.setUpEmployees()
|
|
cls.setUpDates()
|
|
cls.setUpCalendars()
|
|
|
|
def configure_recurrency_span(self, span_qty):
|
|
self.env.company.write({
|
|
'planning_generation_interval': span_qty,
|
|
})
|
|
|
|
# ---------------------------------------------------------
|
|
# Repeat "until" mode
|
|
# ---------------------------------------------------------
|
|
|
|
def test_repeat_until(self):
|
|
""" Normal case: Test slots get repeated at the right time with company limit
|
|
Soft limit should be the earliest between `(now + planning_generation_interval)` and `repeat_until`
|
|
In this case, task repeats forever so soft limit is `(now + planning_generation_interval)`
|
|
planning_generation_interval: 1 month
|
|
|
|
first run:
|
|
now : 2019-06-27
|
|
initial_start : 2019-06-27
|
|
generated slots:
|
|
2019-06-27
|
|
2019-07-04
|
|
2019-07-11
|
|
2019-07-18
|
|
2019-07-25
|
|
NOT 2019-08-01 because it hits the soft limit
|
|
1st cron
|
|
now : 2019-07-11 2 weeks later
|
|
last generated start : 2019-07-25
|
|
repeat_until : 2022-06-27
|
|
generated_slots:
|
|
2019-08-01
|
|
2019-08-08
|
|
NOT 2019-08-15 because it hits the soft limit
|
|
"""
|
|
with freeze_time('2019-06-27 08:00:00'):
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
|
|
|
# since repeat span is 1 month, we should have 5 slots
|
|
slot = self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
})
|
|
generated_slots = self.get_by_employee(self.employee_joseph)
|
|
first_generated_slots_dates = set(map(lambda slot: slot.start_datetime, generated_slots))
|
|
expected_slot_dates = {
|
|
datetime(2019, 6, 27, 8, 0, 0),
|
|
datetime(2019, 7, 4, 8, 0, 0),
|
|
datetime(2019, 7, 11, 8, 0, 0),
|
|
datetime(2019, 7, 18, 8, 0, 0),
|
|
datetime(2019, 7, 25, 8, 0, 0),
|
|
}
|
|
self.assertTrue(first_generated_slots_dates == expected_slot_dates, 'Initial run should have created expected slots')
|
|
# TODO JEM: check same employee, attached to same reccurrence, same role, ...
|
|
|
|
slot.write({
|
|
'repeat_unit': 'week',
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2019, 9, 27, 15, 0, 0),
|
|
})
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'The slot count for recurring shift who have repeat type until should be 5')
|
|
|
|
# now run cron two weeks later, should yield two more slots
|
|
# because the repeat_interval is 1 week
|
|
with freeze_time('2019-07-11 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
generated_slots = self.get_by_employee(self.employee_joseph)
|
|
all_slots_dates = set(map(lambda slot: slot.start_datetime, generated_slots))
|
|
new_expected_slots_dates = expected_slot_dates | {
|
|
datetime(2019, 8, 1, 8, 0, 0),
|
|
datetime(2019, 8, 8, 8, 0, 0),
|
|
}
|
|
self.assertTrue(all_slots_dates == new_expected_slots_dates, 'first cron run should have generated 2 more slots')
|
|
|
|
@freeze_time('2019-06-27 08:00:00')
|
|
def test_repeat_until_no_repeat(self):
|
|
"""create a recurrency with repeat until set which is less than next cron span, should
|
|
stop repeating upon creation
|
|
company_span: 1 month
|
|
first run:
|
|
now : 2019-06-27
|
|
initial_start : 2019-06-27
|
|
repeat_until : 2019-06-29
|
|
generated slots:
|
|
2019-06-27
|
|
NOT 2019-07-04 because it's after the recurrency's repeat_until
|
|
"""
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
|
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_interval': 1,
|
|
'repeat_until': datetime(2019, 6, 29, 8, 0, 0),
|
|
})
|
|
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 1, 'first run should only have created 1 slot since repeat until is set at 1 week')
|
|
|
|
@freeze_time('2019-06-27 08:00:00')
|
|
def test_repeat_until_cron_idempotent(self):
|
|
"""Create a recurrency with repeat_until set, it allows a full first run, but not on next cron
|
|
first run:
|
|
now : 2019-06-27
|
|
initial_start : 2019-06-27
|
|
repeat_end : 2019-07-10 recurrency's repeat_until
|
|
generated slots:
|
|
2019-06-27
|
|
2019-07-04
|
|
NOT 2019-07-11 because it hits the recurrency's repeat_until
|
|
first cron:
|
|
now: 2019-07-12
|
|
last generated start: 2019-07-04
|
|
repeat_end: 2019-07-10 still recurrency's repeat_until
|
|
generated slots:
|
|
NOT 2019-07-11 because it still hits the repeat end
|
|
"""
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
|
|
|
# repeat until is big enough for the first pass to generate all 2 slots
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_interval': 1,
|
|
'repeat_until': datetime(2019, 7, 10, 8, 0, 0),
|
|
})
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 2, 'initial run should have generated 2 slots')
|
|
|
|
# run the cron, since last generated slot is less than one week (one week being the repeat_interval) before repeat_until, the next
|
|
# slots would be after repeat_until, so none will be generated.
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 2, 'running the cron right after should not generate new slots')
|
|
|
|
def test_repeat_until_cron_generation(self):
|
|
"""Generate a recurrence with repeat_until that allows first run, then first cron.
|
|
Check that if a cron is triggered, it doesn't generate more slots (since the date limit
|
|
in generated slots has been reached).
|
|
first run:
|
|
now : 2019-08-31
|
|
initial_start : 2019-09-01
|
|
repeat_end : forever
|
|
generated slots:
|
|
2019-09-01
|
|
2019-09-08
|
|
2019-09-15
|
|
2019-09-22
|
|
2019-09-29
|
|
first cron:
|
|
now: 2019-09-14 two weeks later
|
|
repeat_end: forever
|
|
generated slots:
|
|
2019-10-06
|
|
2019-10-13
|
|
second cron:
|
|
now: 2019-09-16 two days later
|
|
last generated start: 2019-10-13
|
|
repeat_end: forever
|
|
generated slots:
|
|
NOT 2019-10-20 because all recurring slots are already generated in the company interval
|
|
"""
|
|
with freeze_time('2019-08-31 08:00:00'):
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
|
|
|
# first run, 5 slots generated (all the slots for one month, one month being the planning_generation_interval)
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 9, 1, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 9, 1, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
'repeat_until': False,
|
|
})
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'first run should have generated 5 slots')
|
|
# run the cron, since last generated slot do not hit the soft limit, there will be 2 more
|
|
with freeze_time('2019-09-14 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 7, 'first cron should have generated 2 more slots')
|
|
# run the cron again, since last generated slot do hit the soft limit, there won't be more
|
|
with freeze_time('2019-09-16 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 7, 'second cron should not generate any slots')
|
|
|
|
def test_repeat_until_long_limit(self):
|
|
"""Since the recurrency cron is meant to run every week, make sure generation works accordingly when
|
|
the company's repeat span is much larger
|
|
first run:
|
|
now : 2019-06-01
|
|
initial_start : 2019-06-01
|
|
repeat_end : 2019-12-01 initial_start + 6 months
|
|
generated slots:
|
|
2019-06-01
|
|
...
|
|
2019-11-30 (27 items)
|
|
first cron:
|
|
now: 2019-06-08
|
|
last generated start 2019-11-30
|
|
repeat_end 2019-12-08
|
|
generated slots:
|
|
2019-12-07
|
|
only one slot generated: since we are one week later, repeat_end is only one week later and slots are generated every week.
|
|
So there is just enough room for one.
|
|
This ensure slots are always generated up to x time in advance with x being the company's repeat span
|
|
"""
|
|
with freeze_time('2019-06-01 08:00:00'):
|
|
self.configure_recurrency_span(6)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
|
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 1, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 1, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_interval': 1,
|
|
'repeat_until': datetime(2020, 7, 25, 8, 0, 0),
|
|
})
|
|
# over 6 month, we should have generated 27 slots
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 27, 'first run has generated 27 slots')
|
|
# one week later, always having the slots generated 6 months in advance means we
|
|
# have generated one more, which makes 28
|
|
with freeze_time('2019-06-08 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 28, 'second cron should only generate 1 more slot')
|
|
|
|
@freeze_time('2019-06-27 08:00:00')
|
|
def test_repat_until_cancel_repeat(self):
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
|
|
|
# since repeat span is 1 month, we should have 5 slots
|
|
planning_slot = self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 27, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 27, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_interval': 1,
|
|
'repeat_until': datetime(2019, 6, 29, 8, 0, 0),
|
|
})
|
|
planning_slot_form = Form(planning_slot)
|
|
planning_slot_form.repeat = False
|
|
planning_slot_form.save()
|
|
self.assertFalse(planning_slot.repeat)
|
|
|
|
def test_repeat_forever(self):
|
|
""" Since the recurrency cron is meant to run every week, make sure generation works accordingly when
|
|
both the company's repeat span and the repeat interval are much larger
|
|
Company's span is 6 months and repeat_interval is 4 weeks
|
|
first run:
|
|
now : 2019-05-16
|
|
initial_start : 2019-06-01
|
|
repeat_end : 2019-11-16 now + 6 months
|
|
generated slots:
|
|
2019-06-01
|
|
2019-06-29
|
|
2019-07-27
|
|
2019-08-24
|
|
2019-09-21
|
|
2019-10-19
|
|
first cron:
|
|
now: 2019-05-24
|
|
last generated start 2019-10-19
|
|
repeat_end 2019-11-24
|
|
generated slots:
|
|
2019-11-16
|
|
second cron:
|
|
now: 2019-05-31
|
|
last generated start 2019-11-16
|
|
repeat_end 2019-12-01
|
|
generated slots:
|
|
N/A (we are still 6 months in advance)
|
|
"""
|
|
with freeze_time('2019-05-16 08:00:00'):
|
|
self.configure_recurrency_span(6)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_joseph))
|
|
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 1, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 1, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 4,
|
|
})
|
|
|
|
# over 6 months (which spans 26 weeks), we should have generated 6 slots
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 6, 'first run should generate 7 slots')
|
|
|
|
# one week later, always having the slots generated 6 months in advance means we
|
|
# have generated one more, which makes 8
|
|
with freeze_time('2019-05-24 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 7, 'first cron should generate one more slot')
|
|
|
|
# again one week later, we are now up-to-date so there should still be 8 slots
|
|
with freeze_time('2019-05-31 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 7, 'second run should not generate any slots')
|
|
|
|
def test_number_of_repetitions(self):
|
|
""" Since the recurrency cron is meant to run every week, make sure generation works accordingly when
|
|
both the company's repeat span and the repeat interval are much larger
|
|
Company's span is 1 month and repeat_interval is 1 week
|
|
|
|
Test Case:
|
|
---------
|
|
First Automated Run:
|
|
now : 2020-04-20
|
|
initial_start : 2020-04-20
|
|
repeat_end : 2020-05-25 now + 1 month
|
|
generated slots:
|
|
2020-04-20
|
|
2020-04-27
|
|
2020-05-04
|
|
2020-05-11
|
|
2020-05-18
|
|
First Cron:
|
|
now: 2020-04-27
|
|
last generated start 2020-05-18
|
|
repeat_end 2020-05-25
|
|
generated slots:
|
|
2020-05-25
|
|
Second Cron:
|
|
now: 2020-05-04
|
|
last generated start 2020-05-25
|
|
repeat_end 2020-05-25
|
|
generated slots:
|
|
"""
|
|
with freeze_time('2020-04-20 08:00:00'):
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2020, 4, 20, 8, 0, 0),
|
|
'end_datetime': datetime(2020, 4, 20, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_interval': 1,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 6,
|
|
})
|
|
|
|
# we should have generated 5 slots
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'first run should generated 5 slots')
|
|
|
|
# one week later, always having the slots generated 1 month in advance means we have generated one more, which makes 6
|
|
with freeze_time('2020-04-27 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 6, 'first cron should generate one more slot')
|
|
|
|
# again one week later, we are now up-to-date so there should still be 6 slots
|
|
with freeze_time('2020-05-04 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 6, 'second run should not generate any slots')
|
|
|
|
@freeze_time('2020-04-20 08:00:00')
|
|
def test_recurrency_interval_type(self):
|
|
""" Since the recurrency cron is meant to run every week, make sure generation works accordingly when
|
|
both the company's repeat span and the repeat interval are much larger
|
|
Company's span is 12 month and repeat_interval is 1 and repeat_unit are day/week/month/year
|
|
Test Case:
|
|
---------
|
|
- create slot with repeat_unit set as day
|
|
- delete extra slots except original one
|
|
- update repeat_unit to week and repeat_until according slots we need to generate
|
|
- delete extra slots except original one
|
|
- date repeat_unit to month and repeat_until according slots we need to generate
|
|
- delete extra slots except original one
|
|
- update repeat_unit to year and repeat_until according slots we need to generates
|
|
"""
|
|
self.configure_recurrency_span(12)
|
|
|
|
slot = self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2020, 4, 20, 8, 0, 0),
|
|
'end_datetime': datetime(2020, 4, 20, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2020, 4, 24, 8, 0, 0),
|
|
})
|
|
|
|
# generated 5 slots
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'first run should generated 5 slots')
|
|
|
|
def _modify_slot(repeat_unit, repeat_until):
|
|
slot.write({
|
|
'repeat_unit': repeat_unit,
|
|
'repeat_until': repeat_until,
|
|
})
|
|
|
|
_modify_slot('week', datetime(2020, 5, 11, 8, 0, 0)) # generated 4 slots
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 4, 'After change unit to week generated slots should be 4')
|
|
|
|
_modify_slot('month', datetime(2020, 8, 31, 8, 0, 0)) # generated 4 slots, one of the slots would have landed on a weekend
|
|
self.assertEqual(
|
|
len(self.get_by_employee(self.employee_joseph)),
|
|
4,
|
|
'After change unit to month, generated slots should be 4 as one of the slots lands on company closing day and thus should not be generated'
|
|
)
|
|
|
|
_modify_slot('year', datetime(2020, 4, 26, 8, 0, 0)) # generated 1 slot
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 1, 'After change unit to year generated slots should be 1')
|
|
|
|
# ---------------------------------------------------------
|
|
# Recurring Slot Misc
|
|
# ---------------------------------------------------------
|
|
|
|
@freeze_time('2019-06-01 08:00:00')
|
|
def test_recurring_slot_company(self):
|
|
initial_company = self.env['res.company'].create({'name': 'original'})
|
|
initial_company.write({
|
|
'planning_generation_interval': 2,
|
|
})
|
|
# Should be able to create a slot with a company_id != employee.company_id
|
|
slot1 = self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 3, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 3, 17, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
'company_id': initial_company.id,
|
|
})
|
|
|
|
# put the employee in the second company
|
|
self.employee_bert.write({'company_id': initial_company.id})
|
|
|
|
slot1 = self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 3, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 3, 17, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
'company_id': initial_company.id,
|
|
})
|
|
|
|
other_company = self.env['res.company'].create({'name': 'other'})
|
|
other_company.write({
|
|
'planning_generation_interval': 1,
|
|
})
|
|
self.employee_joseph.write({'company_id': other_company.id})
|
|
slot2 = self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2019, 6, 3, 8, 0, 0),
|
|
'end_datetime': datetime(2019, 6, 3, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
'company_id': other_company.id,
|
|
})
|
|
|
|
# initial company's recurrency should have created 9 slots since it's span is two month
|
|
# other company's recurrency should have create 4 slots since it's span is one month
|
|
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 10, 'There will be a 10 slots because becuase other companys slot will become open slots')
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 4, 'other company\'s span is one month, so only 4 slots')
|
|
|
|
self.assertEqual(slot1.company_id, slot1.recurrency_id.company_id, "Recurrence and slots (1) must have the same company")
|
|
self.assertEqual(slot1.recurrency_id.company_id, slot1.recurrency_id.slot_ids.mapped('company_id'), "All slots in the same recurrence (1) must have the same company")
|
|
self.assertEqual(slot2.company_id, slot2.recurrency_id.company_id, "Recurrence and slots (2) must have the same company")
|
|
self.assertEqual(slot2.recurrency_id.company_id, slot2.recurrency_id.slot_ids.mapped('company_id'), "All slots in the same recurrence (1) must have the same company")
|
|
|
|
@freeze_time('2020-06-27 08:00:00')
|
|
def test_empty_recurrency(self):
|
|
""" Check empty recurrency is removed by cron """
|
|
# insert empty recurrency
|
|
empty_recurrency_id = self.env['planning.recurrency'].create({
|
|
'repeat_interval': 1,
|
|
'repeat_type': 'forever',
|
|
'repeat_until': False,
|
|
'last_generated_end_datetime': datetime(2019, 6, 27, 8, 0, 0)
|
|
}).id
|
|
|
|
self.assertEqual(len(list(filter(lambda recu: recu.id == empty_recurrency_id, self.env['planning.recurrency'].search([])))), 1, "the empty recurrency we created should be in the db")
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
# cron with no slot gets deleted (there is no original slot to copy from)
|
|
self.assertEqual(len(list(filter(lambda recu: recu.id == empty_recurrency_id, self.env['planning.recurrency'].search([])))), 0, 'the empty recurrency we created should not be in the db anymore')
|
|
recurrencies = self.env['planning.recurrency'].search([])
|
|
self.assertFalse(len(list(filter(lambda recu: len(recu.slot_ids) == 0, recurrencies))), 'cron with no slot gets deleted (there is no original slot to copy from)')
|
|
|
|
@freeze_time('2025-01-01 8:00:00')
|
|
def test_recurrency_slot_deletion(self):
|
|
""" Check cron on existing slots after auto deletion of other recurrence """
|
|
|
|
slot_a, slot_b = self.env['planning.slot'].create([
|
|
{
|
|
'start_datetime': datetime(2025, 1, 1, 8, 0, 0),
|
|
'end_datetime': datetime(2025, 1, 1, 17, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
'repeat_until': datetime(2025, 1, 10, 8, 0, 0),
|
|
},
|
|
{
|
|
'start_datetime': datetime(2025, 1, 1, 8, 0, 0),
|
|
'end_datetime': datetime(2025, 1, 1, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
'repeat_until': datetime(2025, 1, 10, 8, 0, 0),
|
|
},
|
|
])
|
|
|
|
# delete all slots on first recurrency
|
|
slot_a.recurrency_id.slot_ids.unlink()
|
|
|
|
# delete last slot on second recurrency
|
|
slot_b.recurrency_id.slot_ids.filtered(lambda s: s.start_datetime == datetime(2025, 1, 10, 8, 0, 0)).unlink()
|
|
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
|
|
self.assertTrue(slot_b.recurrency_id.slot_ids.filtered(lambda s: s.start_datetime == datetime(2025, 1, 10, 8, 0, 0)), "Recurrency cron should have re-created the deleted slot")
|
|
|
|
@freeze_time('2020-01-01 08:00:00')
|
|
def test_recurrency_change_date(self):
|
|
slot = self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2020, 1, 1, 8, 0, 0),
|
|
'end_datetime': datetime(2020, 1, 1, 17, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2020, 2, 29, 17, 0, 0),
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'week',
|
|
})
|
|
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 9, 'There are 9 weeks between start_datetime and repeat_until')
|
|
|
|
slot.update({'repeat_type': 'forever'})
|
|
self.assertEqual(slot.recurrency_id.repeat_until, False, 'Repeat forever should not have a date')
|
|
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 26, 'There are 26 weeks in 6 months (max duration of shift generation)')
|
|
|
|
@freeze_time('2020-01-01 08:00:00')
|
|
def test_recurrency_past(self):
|
|
with self.assertRaises(UserError):
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2020, 1, 1, 8, 0, 0),
|
|
'end_datetime': datetime(2020, 1, 1, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2019, 12, 25, 17, 0, 0),
|
|
'repeat_interval': 1,
|
|
})
|
|
|
|
@freeze_time('2020-10-19 08:00:00')
|
|
def test_recurrency_timezone(self):
|
|
"""
|
|
We need to calculate the recurrency in the user's timezone
|
|
(this is important if repeating a slot beyond a dst boundary - we need to keep the same (local) time)
|
|
|
|
company_span: 1 week
|
|
now : 10/20/2020
|
|
initial_start : 10/20/2020 08:00 CEST (06:00 GMT)
|
|
repeat_end : far away
|
|
generated slots:
|
|
10/20/2020 08:00 CEST
|
|
10/27/2020 08:00 CET (07:00 GMT)
|
|
"""
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_bert))
|
|
|
|
self.env.user.tz = 'Europe/Brussels'
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2020, 10, 20, 6, 0, 0),
|
|
'end_datetime': datetime(2020, 10, 20, 15, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2022, 6, 27, 15, 0, 0),
|
|
'repeat_interval': 1,
|
|
})
|
|
slots = self.get_by_employee(self.employee_bert).sorted('start_datetime')
|
|
self.assertEqual('2020-10-20 06:00:00', str(slots[0].start_datetime))
|
|
self.assertEqual('2020-10-27 07:00:00', str(slots[1].start_datetime))
|
|
|
|
@freeze_time('2020-10-17 08:00:00')
|
|
def test_recurrency_timezone_at_dst(self):
|
|
"""
|
|
Check we don't crash if we try to recur ON the DST boundary
|
|
(because 02:30 happens twice on 10/25/20:
|
|
- 10/25/2020 00:30 GMT is 02:30 CEST,
|
|
- 10/25/2020 01:30 GMT is 02:30 CET)
|
|
|
|
company_span: 1 week
|
|
now : 10/17/2020
|
|
initial_start : 10/25/2020 02:30 CEST (00:30 GMT)
|
|
repeat_end : far away
|
|
generated slots:
|
|
10/25/2020 02:30 CEST (00:30 GMT)
|
|
11/01/2020 02:30 CET (01:30 GMT)
|
|
"""
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.assertFalse(self.get_by_employee(self.employee_bert))
|
|
|
|
self.env.user.tz = 'Europe/Brussels'
|
|
self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2020, 10, 25, 0, 30, 0),
|
|
'end_datetime': datetime(2020, 10, 25, 9, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2022, 6, 27, 15, 0, 0),
|
|
'repeat_interval': 1,
|
|
})
|
|
slots = self.get_by_employee(self.employee_bert).sorted('start_datetime')
|
|
self.assertEqual('2020-10-25 00:30:00', str(slots[0].start_datetime))
|
|
self.assertEqual('2020-11-01 01:30:00', str(slots[1].start_datetime))
|
|
|
|
@freeze_time('2020-10-17 08:00:00')
|
|
def test_recurrence_update(self):
|
|
self.configure_recurrency_span(1)
|
|
slot = self.env['planning.slot'].create({
|
|
'name': 'coucou',
|
|
'start_datetime': datetime(2020, 10, 25, 0, 30, 0),
|
|
'end_datetime': datetime(2020, 10, 25, 9, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2020, 12, 25, 0, 0, 0),
|
|
'repeat_interval': 1,
|
|
})
|
|
|
|
all_slots = slot.recurrency_id.slot_ids
|
|
other_slots = all_slots - slot
|
|
|
|
slot.write({
|
|
'name': 'cuicui',
|
|
})
|
|
self.assertEqual(slot.name, 'cuicui', 'This slot should be modified')
|
|
self.assertTrue(all(slot.name == 'coucou' for slot in other_slots), 'Other slots should not be modified')
|
|
|
|
slot.write({
|
|
'name': 'cuicui',
|
|
'recurrence_update': 'all',
|
|
})
|
|
self.assertTrue(all(slot.name == 'cuicui' for slot in all_slots), 'All slots should be modified')
|
|
|
|
other_slots[0].write({
|
|
'name': 'coucou',
|
|
'recurrence_update': 'subsequent',
|
|
})
|
|
self.assertEqual(slot.name, 'cuicui', 'This slot should not be modified')
|
|
self.assertTrue(all(slot.name == 'coucou' for slot in other_slots), 'Other slots should be modified')
|
|
|
|
@freeze_time('2020-01-01 08:00:00')
|
|
def test_recurrence_with_already_planned_shift(self):
|
|
PlanningSlot = self.env['planning.slot']
|
|
dummy, slot = PlanningSlot.create([{
|
|
'start_datetime': datetime(2020, 1, 8, 8, 0),
|
|
'end_datetime': datetime(2020, 1, 8, 17, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
}, {
|
|
'start_datetime': datetime(2020, 1, 6, 8, 0),
|
|
'end_datetime': datetime(2020, 1, 6, 17, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_unit': 'day',
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2020, 1, 10, 15, 0),
|
|
'repeat_interval': 1,
|
|
}])
|
|
|
|
open_shift_count = PlanningSlot.search_count([('resource_id', '=', False), ('recurrency_id', '=', slot.recurrency_id.id)])
|
|
self.assertEqual(open_shift_count, 1, 'Open shift should be created instead of recurring shift if resource already has a shift planned')
|
|
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 5, 'There should be 5 shifts: 1 already planned shift and 4 recurring shifts')
|
|
|
|
def test_recurrency_last_day_of_month(self):
|
|
self.configure_recurrency_span(1)
|
|
self.env.user.tz = 'UTC'
|
|
slot = self.env['planning.slot'].create({
|
|
'name': 'coucou',
|
|
'start_datetime': datetime(2020, 1, 31, 8, 0),
|
|
'end_datetime': datetime(2020, 1, 31, 9, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2020, 6, 1, 0, 0),
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'month',
|
|
})
|
|
self.assertEqual(
|
|
slot.recurrency_id.slot_ids.mapped('start_datetime'),
|
|
[
|
|
datetime(2020, 1, 31, 8, 0),
|
|
# datetime(2020, 2, 29, 8, 0), should not be generated lands on a Saturday
|
|
datetime(2020, 3, 31, 8, 0),
|
|
datetime(2020, 4, 30, 8, 0),
|
|
# datetime(2020, 5, 31, 8, 0), should not be generated lands on a Sunday
|
|
],
|
|
'The slots should occur at the last day of each month, with the exception of February and May when the last day lands on a company closing day'
|
|
)
|
|
|
|
def test_recurrency_occurring_slots(self):
|
|
self.configure_recurrency_span(1)
|
|
self.env.user.tz = 'UTC'
|
|
slot = self.env['planning.slot'].create([{
|
|
'name': 'coucou',
|
|
'start_datetime': datetime(2020, 1, 28, 8, 0),
|
|
'end_datetime': datetime(2020, 1, 28, 9, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 2,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'month',
|
|
}, {
|
|
'name': 'concurrent slot',
|
|
'start_datetime': datetime(2020, 2, 28, 8, 0),
|
|
'end_datetime': datetime(2020, 2, 28, 11, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
}])
|
|
|
|
self.assertFalse(
|
|
slot.recurrency_id.slot_ids[1].resource_id,
|
|
'The second slot should be an open shift as the resource has a concurrent shift'
|
|
)
|
|
|
|
def test_recurrency_change_date_subsequent(self):
|
|
first_slot_start, first_slot_end = (
|
|
datetime(2020, 11, 22, 8 + i) for i in range(2)
|
|
)
|
|
first_slot = self.env['planning.slot'].create({
|
|
'start_datetime': first_slot_start,
|
|
'end_datetime': first_slot_end,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 3,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
})
|
|
recurrence = first_slot.recurrency_id
|
|
slots = recurrence.slot_ids
|
|
slots._compute_repeat_type() # force it, otherwise it is keeps default value 'forever'
|
|
second_slot = slots[1]
|
|
|
|
# Changing a non-date field
|
|
second_slot.write({
|
|
'allocated_hours': 0.5,
|
|
'recurrence_update': 'subsequent',
|
|
})
|
|
self.assertEqual(slots, recurrence.slot_ids, "The recurrence should have kept the same slots")
|
|
self.assertTrue(
|
|
all((slots - first_slot).mapped(lambda s: s.allocated_hours == 0.5)),
|
|
"The change should have been applied to all subsequent slots",
|
|
)
|
|
|
|
# Changing date fields
|
|
second_slot.write({
|
|
'start_datetime': first_slot_start,
|
|
'end_datetime': first_slot_end,
|
|
'recurrence_update': 'subsequent',
|
|
})
|
|
slots = recurrence.slot_ids.sorted('start_datetime')
|
|
self.assertNotIn(first_slot, slots, "The first slot shouldn't be part of the recurrence anymore")
|
|
self.assertTrue(all(
|
|
actual_start == first_slot_start + timedelta(days=i)
|
|
for i, actual_start in enumerate(slots.mapped('start_datetime'))
|
|
), "The changes should have been applied to all subsequent slots")
|
|
|
|
def test_recurrency_change_date_all(self):
|
|
first_slot_start, first_slot_end = (
|
|
datetime(2020, 11, 22, 8 + i) for i in range(2)
|
|
)
|
|
first_slot = self.env['planning.slot'].create({
|
|
'start_datetime': first_slot_start,
|
|
'end_datetime': first_slot_end,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 3,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
})
|
|
recurrence = first_slot.recurrency_id
|
|
slots = recurrence.slot_ids
|
|
second_slot = slots[1]
|
|
|
|
# Changing a non-date field
|
|
second_slot.write({
|
|
'allocated_hours': 0.5,
|
|
'recurrence_update': 'all',
|
|
})
|
|
self.assertEqual(slots, recurrence.slot_ids, "The recurrence should have kept the same slots")
|
|
self.assertTrue(
|
|
all(slots.mapped(lambda s: s.allocated_hours == 0.5)),
|
|
"The change should have been applied to all slots",
|
|
)
|
|
|
|
# Changing date fields
|
|
second_slot.write({
|
|
'start_datetime': first_slot_start,
|
|
'end_datetime': first_slot_end,
|
|
'recurrence_update': 'all',
|
|
})
|
|
self.assertNotEqual(slots, recurrence.slot_ids, "All slots but the first one should have been deleted and recreated")
|
|
slots = recurrence.slot_ids.sorted('start_datetime')
|
|
self.assertTrue(all(
|
|
actual_start == first_slot_start + timedelta(days=i - 1)
|
|
for i, actual_start in enumerate(slots.mapped('start_datetime'))
|
|
), "The changes should have been applied to all slots")
|
|
|
|
def test_recurrency_with_slot_on_closing_day(self):
|
|
"""
|
|
This test covers the case in which the initially planned shift in the recurrence is planned on a closing (non-working) day.
|
|
This is an exception from the regular flow.
|
|
In this case, we should generate all the recurring shifts normally even if they land on non-working days.
|
|
"""
|
|
first_slot = self.env['planning.slot'].create({ # this should be land on a Sunday (a closing day)
|
|
'start_datetime': self.random_sunday_date + timedelta(hours=8),
|
|
'end_datetime': self.random_sunday_date + timedelta(hours=10),
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 7,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
})
|
|
|
|
self.assertEqual(
|
|
len(first_slot.recurrency_id.slot_ids),
|
|
7,
|
|
'Since the initial slot was planned on a closing day, all recurring slots should be generated normally regardless of whether they land on a closing day.'
|
|
)
|
|
|
|
def test_recurrency_resource_partially_busy_below_100(self):
|
|
"""
|
|
This test covers the case where the resource has occuring slots at the same time as the recurrent slot but their hour sum doesn't go
|
|
over the available hours in the period we are planning over.
|
|
Specifically, the resource should have these 3 shifts on their calendar:
|
|
- 12/3/24 8:00 - 14:00 (1h allocated)
|
|
- 12/3/24 12:00 - 16:00 (1h allocated)
|
|
- 12/3/24 7:00 - 10:00 (1h allocated)
|
|
They all overlap with the recurring shift which should be generated like so:
|
|
- 12/3/24 8:00 - 14:00 (2h allocated)
|
|
The sum of the allocated hours is 5h while we are planning over a 9h period.
|
|
Thus we generate the recurring shift normally without making it open.
|
|
"""
|
|
first_slot = self.env['planning.slot'].create([{ # this should land on a Monday
|
|
'start_datetime': self.random_monday_date + timedelta(hours=8),
|
|
'end_datetime': self.random_monday_date + timedelta(hours=14),
|
|
'resource_id': self.resource_bert.id,
|
|
'allocated_hours': 2,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 2,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
}, {
|
|
'start_datetime': self.random_monday_date + timedelta(days=1, hours=8),
|
|
'end_datetime': self.random_monday_date + timedelta(days=1, hours=14),
|
|
'resource_id': self.resource_bert.id,
|
|
'allocated_hours': 1,
|
|
}, {
|
|
'start_datetime': self.random_monday_date + timedelta(days=1, hours=12),
|
|
'end_datetime': self.random_monday_date + timedelta(days=1, hours=16),
|
|
'resource_id': self.resource_bert.id,
|
|
'allocated_hours': 1,
|
|
}, {
|
|
'start_datetime': self.random_monday_date + timedelta(days=1, hours=7),
|
|
'end_datetime': self.random_monday_date + timedelta(days=1, hours=10),
|
|
'resource_id': self.resource_bert.id,
|
|
'allocated_hours': 1,
|
|
}])[0]
|
|
|
|
self.assertTrue(
|
|
first_slot.recurrency_id.slot_ids[1].resource_id,
|
|
'The 2nd recurring slot can be generated normally as the recurrent shift and the other occuring shifts would not pass 100 percent allocated percentage.'
|
|
)
|
|
|
|
def test_recurrency_resource_partially_busy_above_100(self):
|
|
"""
|
|
This test is similar to the one above but this time, the recurrent shift would leave the resource over-planned and thus it is not generated.
|
|
Specifically, the resource should have these 1 shift on their calendar:
|
|
- 12/3/24 8:00 - 10:00 (2h allocated)
|
|
It will overlap with the recurring shift which should be generated like so:
|
|
- 12/3/24 8:00 - 11:00 (2h allocated)
|
|
The sum of the allocated hours is 4h while we are planning over a 3h period.
|
|
Thus we generate the recurring shift as an open shift.
|
|
"""
|
|
first_slot = self.env['planning.slot'].create([{ # this should land on a Monday
|
|
'start_datetime': self.random_monday_date + timedelta(hours=8),
|
|
'end_datetime': self.random_monday_date + timedelta(hours=11),
|
|
'resource_id': self.resource_bert.id,
|
|
'allocated_hours': 2,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 2,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
}, {
|
|
'start_datetime': self.random_monday_date + timedelta(days=1, hours=8),
|
|
'end_datetime': self.random_monday_date + timedelta(days=1, hours=10),
|
|
'resource_id': self.resource_bert.id,
|
|
'allocated_hours': 2,
|
|
}])[0]
|
|
|
|
self.assertEqual(
|
|
first_slot.recurrency_id.slot_ids[0].resource_id.id,
|
|
self.resource_bert.id,
|
|
'The 1st recurring slot will be generated normally since the resource is available on that day.'
|
|
)
|
|
self.assertFalse(
|
|
first_slot.recurrency_id.slot_ids[1].resource_id,
|
|
'The 2nd recurring slot will be generated as an open shift because the resource cannot accomodate the extra slot given its allocated hours.'
|
|
)
|
|
|
|
def test_recurrency_with_slot_outside_working_hours(self):
|
|
"""
|
|
This test covers the case in which the initially planned shift in the recurrence is planned outisde the resources working hours.
|
|
This is an exception from the regular flow.
|
|
In this case, we should generate all the recurring shifts normally even if they land on non-working hours.
|
|
The initial shift will be at 18:00 in the afternoon and should be repeated 5 times.
|
|
"""
|
|
first_slot = self.env['planning.slot'].create({ # this should land on a Monday
|
|
'start_datetime': self.random_monday_date + timedelta(hours=18),
|
|
'end_datetime': self.random_monday_date + timedelta(hours=20),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 5,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
})
|
|
|
|
resources_per_slot = [slot.resource_id.id for slot in first_slot.recurrency_id.slot_ids]
|
|
|
|
self.assertTrue(
|
|
all(resources_per_slot),
|
|
'Since the initial slot was planned outside working hours, all recurring slots should be generated normally regardless of the hour of the day.'
|
|
)
|
|
|
|
def test_recurrency_outside_working_hours(self):
|
|
"""
|
|
This test covers the case in which the initially planned shift in the recurrence is planned inside the resources working hours,
|
|
but some of the repeated shifts will land outisde the resource's working hours.
|
|
In this case, the recurring shifts that land inside of the resource's working hours will be planned normally. The ones that dont,
|
|
will still be planned but as open shifts (without a resource).
|
|
In this example:
|
|
- The resource will not work on Tuesday and Thursday afternoon.
|
|
- The initial shift will be at 15:00 in the afternoon and should be repeated 5 times.
|
|
-> 3 shifts should be planned normally (for Monday, Wednesday, and Friday) and 2 as open shifts (Tuesday and Thursday)
|
|
"""
|
|
part_time_calendar = self.env['resource.calendar'].create({
|
|
'name': 'Part time calendar',
|
|
'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 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': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
|
(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': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
|
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
|
|
]
|
|
})
|
|
self.employee_bert.resource_calendar_id = part_time_calendar
|
|
first_slot = self.env['planning.slot'].create({ # this should land on a Monday
|
|
'start_datetime': self.random_monday_date + timedelta(hours=15),
|
|
'end_datetime': self.random_monday_date + timedelta(hours=16),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 5,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
})
|
|
|
|
resources_per_slot = [slot.resource_id.id for slot in first_slot.recurrency_id.slot_ids]
|
|
|
|
self.assertFalse(
|
|
all(resources_per_slot),
|
|
'Since the initial slot was planned during work hours, recurring slots outside work hours are generated as open shifts.'
|
|
)
|
|
self.assertFalse(
|
|
first_slot.recurrency_id.slot_ids[1].resource_id,
|
|
'The recurring shift to be planned on Tuesday should be an open shift.'
|
|
)
|
|
self.assertFalse(
|
|
first_slot.recurrency_id.slot_ids[3].resource_id,
|
|
'The recurring shift to be planned on Thursday should be an open shift.'
|
|
)
|
|
|
|
def test_number_of_repetitions_with_multiple_slots(self):
|
|
"""
|
|
Test number of repetations for a planning slots with multi records through cron
|
|
"""
|
|
with freeze_time('2024-06-16 08:00:00'):
|
|
self.configure_recurrency_span(1)
|
|
|
|
self.env['planning.slot'].create([{
|
|
'name': 'coucou',
|
|
'start_datetime': datetime(2024, 6, 16, 8, 0, 0),
|
|
'end_datetime': datetime(2024, 6, 16, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 6,
|
|
'repeat_interval': 1,
|
|
}, {
|
|
'name': 'concurrent slot',
|
|
'start_datetime': datetime(2024, 6, 16, 8, 0, 0),
|
|
'end_datetime': datetime(2024, 6, 16, 17, 0, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 6,
|
|
'repeat_interval': 1,
|
|
}])
|
|
|
|
# we should have generated 5 slots for each employee
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 5, 'first run should generated 2 slots')
|
|
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 5, 'first run should generated 2 slots')
|
|
|
|
# one week later, always having the slots generated 1 month in advance means we have generated one more, which makes 6
|
|
with freeze_time('2024-06-23 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 6, 'first cron should generate 6 slots')
|
|
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 6, 'first cron should generate 6 slots')
|
|
|
|
# again one week later, we are now up-to-date so there should still be 6 slots
|
|
with freeze_time('2024-06-30 08:00:00'):
|
|
self.env['planning.recurrency']._cron_schedule_next()
|
|
self.assertEqual(len(self.get_by_employee(self.employee_joseph)), 6, 'second run should not generate any slots')
|
|
self.assertEqual(len(self.get_by_employee(self.employee_bert)), 6, 'second run should not generate any slots')
|
|
|
|
def test_full_month_recurrency(self):
|
|
"""
|
|
Check slot generation with an original slot of 31 days (full month) repeated monthly.
|
|
- February being shorter than 31 days, the generated slot should not exceed the length of the month to avoid
|
|
overlapping slots.
|
|
- February also starts on a weekend, but the slot should still be generated as the original slot contains
|
|
non-working days.
|
|
"""
|
|
self.configure_recurrency_span(1)
|
|
self.env.user.tz = 'UTC'
|
|
|
|
slot = self.env['planning.slot'].create({
|
|
'name': 'Full month recurrency',
|
|
'start_datetime': datetime(2020, 1, 1, 8, 0),
|
|
'end_datetime': datetime(2020, 1, 31, 15, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 4,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'month',
|
|
})
|
|
|
|
self.assertEqual(
|
|
slot.recurrency_id.slot_ids.mapped(lambda r: (r.start_datetime, r.end_datetime)),
|
|
[(datetime(2020, 1, 1, 8, 0), datetime(2020, 1, 31, 15, 0)),
|
|
(datetime(2020, 2, 1, 8, 0), datetime(2020, 2, 29, 15, 0)),
|
|
(datetime(2020, 3, 1, 8, 0), datetime(2020, 3, 31, 15, 0)),
|
|
(datetime(2020, 4, 1, 8, 0), datetime(2020, 4, 30, 15, 0))],
|
|
)
|
|
|
|
@freeze_time('2022-01-10 10:00:00')
|
|
def test_recurring_shift_on_non_working_days_for_flexible_hours(self):
|
|
"""
|
|
Recurring shift should be generated on non-working days for flexible resources
|
|
(E.g. a recurring shift for everyday for a flexible employee should be generated on Saturday, Sunday as well)
|
|
|
|
Test Case
|
|
==========
|
|
1) set an employee with a flexible calendar.
|
|
2) create a recurring shift for everyday for a week for the flexible employee
|
|
3) verify that the recurring shift is generated on Saturday, Sunday and
|
|
the allocated hours should be equal to the original shift.
|
|
"""
|
|
# set the calendar for the employee as flexible
|
|
self.employee_bert.resource_calendar_id = self.flex_50h_calendar
|
|
|
|
# create the recutting shift
|
|
self.env['planning.slot'].create({
|
|
'resource_id': self.employee_bert.resource_id.id,
|
|
'start_datetime': datetime(2022, 1, 10, 10, 0), # Monday
|
|
'end_datetime': datetime(2022, 1, 10, 17, 0), # shift of 7 hours
|
|
'repeat': True,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2022, 1, 16, 23, 59, 59), # Sunday
|
|
})
|
|
generated_slots = self.get_by_employee(self.employee_bert)
|
|
|
|
# check the number of generated recurring shifts
|
|
self.assertEqual(len(generated_slots), 7, 'The recurring shift should be generated for 7 days of the week')
|
|
# slots in the weekend should have the same allocated hours of the recurring shift
|
|
self.assertEqual(all(slot.allocated_hours == 7 for slot in generated_slots), True,
|
|
'All the generated slots should have the same allocated hours')
|
|
|
|
@freeze_time('2022-01-10 10:00:00')
|
|
def test_fully_flexible_employee_recurring_shifts_with_conflicts(self):
|
|
"""
|
|
Test that recurring shifts are assigned to fully flexible employees even if they generate conflicts with existing shifts.
|
|
"""
|
|
# Create an employee working fully flexible hours
|
|
self.employee_bert.resource_calendar_id = False
|
|
|
|
# Plan some shifts for the employee
|
|
shift = self.env['planning.slot'].create({
|
|
'resource_id': self.employee_bert.resource_id.id,
|
|
'start_datetime': datetime(2022, 1, 10, 10, 0),
|
|
'end_datetime': datetime(2022, 1, 10, 17, 0),
|
|
})
|
|
|
|
# Create a recurring shift for the employee
|
|
self.env['planning.slot'].create({
|
|
'resource_id': self.employee_bert.resource_id.id,
|
|
'start_datetime': datetime(2022, 1, 10, 9, 0), # Monday
|
|
'end_datetime': datetime(2022, 1, 10, 18, 0), # shift of 9 hours
|
|
'repeat': True,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'day',
|
|
'repeat_type': 'until',
|
|
'repeat_until': datetime(2022, 1, 16, 23, 59, 59), # Sunday
|
|
})
|
|
|
|
# Fetch all shifts for the employee within the week
|
|
shifts = self.env['planning.slot'].search([
|
|
('id', '!=', shift.id),
|
|
('resource_id', '=', self.employee_bert.resource_id.id),
|
|
('start_datetime', '>=', datetime(2022, 1, 10, 0, 0)),
|
|
('end_datetime', '<=', datetime(2022, 1, 16, 23, 59, 59)),
|
|
])
|
|
|
|
# Verify that there are 8 shifts in total (7 recurring shifts)
|
|
self.assertEqual(len(shifts), 7, "There should be 7 recurring shifts for the employee.")
|
|
shift_count_per_start_day = {}
|
|
for shift in shifts:
|
|
start_day = shift.start_datetime.day
|
|
self.assertNotIn(start_day, shift_count_per_start_day, "There should be one shift per day.")
|
|
shift_count_per_start_day[start_day] = True
|
|
self.assertEqual(shift.start_datetime.hour, 9, "Recurring shift should start at 9:00 AM.")
|
|
self.assertEqual(shift.end_datetime.hour, 18, "Recurring shift should end at 6:00 PM.")
|
|
|
|
def test_edit_all_shifts_end_datetime(self):
|
|
self.configure_recurrency_span(1)
|
|
self.env.user.tz = 'UTC'
|
|
|
|
slot = self.env['planning.slot'].create({
|
|
'name': 'Full month recurrency',
|
|
'start_datetime': datetime(2024, 9, 16, 8, 0),
|
|
'end_datetime': datetime(2024, 9, 20, 17, 0),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'x_times',
|
|
'repeat_number': 4,
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'week',
|
|
})
|
|
|
|
original_slots = slot.recurrency_id.slot_ids
|
|
original_end_dates = original_slots.mapped('end_datetime')
|
|
original_slots[1].write({
|
|
'recurrence_update': 'all',
|
|
'end_datetime': '2024-09-27 12:00:00',
|
|
})
|
|
|
|
self.assertTrue(all(
|
|
modified_end_date == original_end_date - timedelta(hours=5)
|
|
for original_end_date, modified_end_date
|
|
in zip(original_end_dates, slot.recurrency_id.slot_ids.mapped('end_datetime'))
|
|
))
|
|
|
|
def test_recurrent_shifts_without_archive_resource(self):
|
|
shift=self.env['planning.slot'].create({
|
|
'start_datetime': datetime(2024, 12, 25, 8, 0, 0),
|
|
'end_datetime': datetime(2024, 12, 25, 17, 0, 0),
|
|
'resource_id': self.resource_joseph.id,
|
|
})
|
|
self.resource_joseph.action_archive()
|
|
shift.write({'repeat': True, 'repeat_interval': 1, 'repeat_type': 'forever'})
|
|
self.assertFalse((shift.recurrency_id.slot_ids - shift).resource_id)
|
|
|
|
|
|
class TestPlanningContract(TestPlanningContractCommon):
|
|
|
|
@freeze_time('2024-03-11 08:00:00')
|
|
def test_recurrence_permanent_contract(self):
|
|
"""
|
|
This test covers the default use case in which a recurrent shift is planned inside of the employee contract date range.
|
|
Specifically, the contract is a permanent one (it does not have an ending date).
|
|
In this use case, the recurrent shifts will be planned normally until the end of the planning period. This is by default 6 months.
|
|
"""
|
|
# This contract does not have a date_end, thus it is permanent.
|
|
self.employee_bert.version_id.write({
|
|
'date_version': datetime.strptime('2024-01-01', '%Y-%m-%d'),
|
|
'contract_date_start': datetime.strptime('2024-01-01', '%Y-%m-%d'),
|
|
'contract_date_end': False,
|
|
'name': 'Long contract for Bert',
|
|
'resource_calendar_id': self.calendar_40h.id,
|
|
'wage': 5000.0,
|
|
})
|
|
recurrent_slot = self.env['planning.slot'].create({ # this should be a Monday
|
|
'start_datetime': self.random_monday_date + timedelta(hours=15),
|
|
'end_datetime': self.random_monday_date + timedelta(hours=16),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'week',
|
|
})
|
|
|
|
self.assertEqual(
|
|
len(recurrent_slot.recurrency_id.slot_ids),
|
|
27, # There are 27 Mondays in the next 6 month period :(
|
|
'Since the employee\'s contract is permanent, shifts will be planned for the next 6 months (the cron period).'
|
|
)
|
|
|
|
@freeze_time('2024-03-11 08:00:00')
|
|
def test_recurrence_fixed_contract(self):
|
|
"""
|
|
This test covers the use case in which a recurrent shift is planned inside of the employee contract date range,
|
|
but the employee has a fixed term contract (it has an ending date).
|
|
In this use case, the recurrent shifts will be planned normally until the end of the contract.
|
|
"""
|
|
# This contract ends at the 27th of the month - it is a fixed term contract.
|
|
self.employee_bert.version_id.write({
|
|
'contract_date_end': datetime.strptime('2024-03-27', '%Y-%m-%d'),
|
|
'contract_date_start': datetime.strptime('2024-01-01', '%Y-%m-%d'),
|
|
'date_version': datetime.strptime('2024-01-01', '%Y-%m-%d'),
|
|
'name': 'Short contract for Bert',
|
|
'resource_calendar_id': self.calendar_40h.id,
|
|
'wage': 5000.0,
|
|
})
|
|
recurrent_slot = self.env['planning.slot'].create({ # this should land on a Monday
|
|
'start_datetime': self.random_monday_date + timedelta(hours=15),
|
|
'end_datetime': self.random_monday_date + timedelta(hours=16),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'week',
|
|
})
|
|
|
|
self.assertEqual(
|
|
len(recurrent_slot.recurrency_id.slot_ids),
|
|
3, # There are 3 Mondays until the 27th of March :(
|
|
'Since the employee\'s contract is fixed, shifts will be planned until the end of the contract.'
|
|
)
|
|
|
|
@freeze_time('2024-03-11 08:00:00')
|
|
def test_recurrence_with_slot_outside_fixed_contract(self):
|
|
"""
|
|
This test covers the use case in which the initial shift (to be repeated) is planned outisde of the employee contract date range.
|
|
Specifically in the test case, the contract has ended prior to the slot's date.
|
|
In this use case, the recurrent shifts will be planned again normally until the end of the planning period (6 months) as if it was within contract.
|
|
"""
|
|
# This contract ends at the 8th of the month - it should already be over
|
|
self.employee_bert.version_id.write({ # Fixed term contract
|
|
'contract_date_end': datetime.strptime('2024-03-08', '%Y-%m-%d'),
|
|
'contract_date_start': datetime.strptime('2024-01-01', '%Y-%m-%d'),
|
|
'date_version': datetime.strptime('2024-01-01', '%Y-%m-%d'),
|
|
'name': 'Outdated contract for Bert',
|
|
'resource_calendar_id': self.calendar_40h.id,
|
|
'wage': 5000.0,
|
|
})
|
|
recurrent_slot = self.env['planning.slot'].create({ # this should be a Monday
|
|
'start_datetime': self.random_monday_date + timedelta(hours=15),
|
|
'end_datetime': self.random_monday_date + timedelta(hours=16),
|
|
'resource_id': self.resource_bert.id,
|
|
'repeat': True,
|
|
'repeat_type': 'forever',
|
|
'repeat_interval': 1,
|
|
'repeat_unit': 'week',
|
|
})
|
|
|
|
self.assertEqual(
|
|
len(recurrent_slot.recurrency_id.slot_ids),
|
|
27,
|
|
'Since the initial shift was planned outside of the employee\'s contract, shifts will be planned normally for the next 6 months.'
|
|
)
|