# 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.' )