Files
Odoo-Modules/fusion_clock/tests/test_recurrence.py
gsinghpal 19d484680d fix(fusion_clock): code-review hardening [19.0.5.0.1]
- _cron_generate: per-rule savepoint isolation (one bad rule can't abort the
  whole daily batch)
- fclk_attach_recurrence: clear an existing recurrence first (no orphaned rule
  generating forever)
- fclk_apply_planner_cell: collapse split rows (search was limit=1 after the
  UNIQUE drop, orphaning extras)
- fclk_release_shift: reject non-posted/open shifts (raw-POST guard)
- delete_open_shift: report success=false when nothing was deleted + JS surfaces it
- _generate: log before removing an empty recurrence
Tests added for collapse, re-attach, draft-release.

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

111 lines
5.2 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, 'reason': 'Vacation',
'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_reattach_recurrence_replaces_old_rule(self):
seed = self._seed(date(2026, 6, 1))
rule1 = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 1, 'repeat_unit': 'week',
'repeat_type': 'x_times', 'repeat_number': 3})
rule1_id = rule1.id
rule2 = self.Schedule.fclk_attach_recurrence(seed, {
'repeat_interval': 1, 'repeat_unit': 'week',
'repeat_type': 'x_times', 'repeat_number': 2})
# The old rule must be gone (not left generating forever) and the seed
# must point at the new rule.
self.assertFalse(
self.env['fusion.clock.schedule.recurrence'].browse(rule1_id).exists())
self.assertEqual(seed.recurrence_id, rule2)
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())