Files
Odoo-Modules/fusion_clock/tests/test_recurrence.py
gsinghpal 734b3b94fd feat(fusion_clock): native recurring shifts engine [A4-A5]
fusion.clock.schedule.recurrence (repeat every N day/week/month/year;
forever/until/N-times) re-fit from planning.recurrency onto per-day rows;
daily generation cron; _fclk_on_leave skip; planner Repeat…/Stop-repeat
UI + endpoints; recurrence + role indicators on cells.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:49:26 -04:00

96 lines
4.5 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from datetime import date
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('-at_install', 'post_install', 'fusion_clock')
class TestRecurrence(TransactionCase):
def setUp(self):
super().setUp()
self.emp = self.env['hr.employee'].create({'name': 'Rita'})
self.Schedule = self.env['fusion.clock.schedule']
def _seed(self, day):
return self.Schedule.create({
'employee_id': self.emp.id,
'schedule_date': day,
'start_time': 9.0, 'end_time': 17.0, 'break_minutes': 30.0,
'state': 'posted',
})
def test_weekly_until_generates_inclusive_series(self):
seed = self._seed(date(2026, 6, 1))
rule = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 1, 'repeat_unit': 'week',
'repeat_type': 'until', 'repeat_until': date(2026, 6, 29)})
rows = self.Schedule.search([('recurrence_id', '=', rule.id)], order='schedule_date')
self.assertEqual(
rows.mapped('schedule_date'),
[date(2026, 6, 1), date(2026, 6, 8), date(2026, 6, 15),
date(2026, 6, 22), date(2026, 6, 29)])
# Generated (non-seed) rows are draft until posted.
generated = rows.filtered(lambda r: r.schedule_date != date(2026, 6, 1))
self.assertTrue(all(r.state == 'draft' for r in generated))
def test_x_times_counts_seed(self):
seed = self._seed(date(2026, 6, 1))
rule = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 1, 'repeat_unit': 'week',
'repeat_type': 'x_times', 'repeat_number': 3})
rows = self.Schedule.search([('recurrence_id', '=', rule.id)])
self.assertEqual(len(rows), 3, "3 repetitions = seed + 2 generated")
def test_interval_two_weeks(self):
seed = self._seed(date(2026, 6, 1))
rule = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 2, 'repeat_unit': 'week',
'repeat_type': 'until', 'repeat_until': date(2026, 7, 1)})
rows = self.Schedule.search([('recurrence_id', '=', rule.id)], order='schedule_date')
self.assertEqual(rows.mapped('schedule_date'),
[date(2026, 6, 1), date(2026, 6, 15), date(2026, 6, 29)])
def test_stop_deletes_future_drafts_keeps_posted(self):
seed = self._seed(date(2026, 6, 1))
rule = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 1, 'repeat_unit': 'week',
'repeat_type': 'x_times', 'repeat_number': 4})
# Post one generated occurrence.
gen = self.Schedule.search([
('recurrence_id', '=', rule.id), ('schedule_date', '=', date(2026, 6, 8))])
gen.state = 'posted'
rule._stop(date(2026, 6, 2))
remaining = self.Schedule.search([('recurrence_id', '=', rule.id)]).mapped('schedule_date')
self.assertIn(date(2026, 6, 1), remaining) # seed, before cutoff
self.assertIn(date(2026, 6, 8), remaining) # posted, kept
self.assertNotIn(date(2026, 6, 15), remaining) # future draft, removed
self.assertNotIn(date(2026, 6, 22), remaining)
def test_leave_day_skipped(self):
self.env['fusion.clock.leave.request'].create({
'employee_id': self.emp.id,
'leave_date': date(2026, 6, 8), 'date_to': date(2026, 6, 8)})
seed = self._seed(date(2026, 6, 1))
rule = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 1, 'repeat_unit': 'week',
'repeat_type': 'until', 'repeat_until': date(2026, 6, 15)})
dates = self.Schedule.search([('recurrence_id', '=', rule.id)]).mapped('schedule_date')
self.assertNotIn(date(2026, 6, 8), dates, "Leave day should be skipped")
self.assertIn(date(2026, 6, 15), dates)
def test_clear_recurrence_unlinks_rule_when_empty(self):
seed = self._seed(date(2026, 6, 1))
rule = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 1, 'repeat_unit': 'week',
'repeat_type': 'x_times', 'repeat_number': 3})
rule_id = rule.id
self.Schedule.fclk_clear_recurrence(seed)
# Seed kept (it's posted), future drafts gone, seed detached.
self.assertFalse(seed.recurrence_id)
self.assertFalse(self.env['fusion.clock.schedule.recurrence'].browse(rule_id).exists())