From c527c7cade818df566f9c319de9f341492d02173 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 1 Jun 2026 00:28:12 -0400 Subject: [PATCH] fix(fusion_clock): migration must recompute net_hours/overtime, not just break Recomputing only x_fclk_break_minutes left historical x_fclk_net_hours / x_fclk_overtime_hours stale (add_to_compute+flush of one field does not cascade to dependents). Recompute the full chain in dependency order. Caught verifying the entech deploy. Co-Authored-By: Claude Opus 4.8 (1M context) --- fusion_clock/CLAUDE.md | 1 + fusion_clock/migrations/19.0.4.1.0/post-migrate.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/fusion_clock/CLAUDE.md b/fusion_clock/CLAUDE.md index 1521d93e..46a12ae5 100644 --- a/fusion_clock/CLAUDE.md +++ b/fusion_clock/CLAUDE.md @@ -329,6 +329,7 @@ All new JSON endpoints must use `type="jsonrpc"`, not deprecated `type="json"`. - `hr.employee._get_fclk_scheduled_times(date)` returns naive UTC datetimes suitable for Odoo comparisons. - **`hr.attendance.x_fclk_break_minutes` is a stored COMPUTE, not a writable field** (`_compute_fclk_break_minutes`): statutory break (per the employee's province `fusion.clock.break.rule`, from actual `worked_hours`, 2-tier — first break after N1 h, second after N2 h, inclusive `>=`) **plus** Σ penalty minutes. It recomputes on every path incl. manual backend create/edit, which is what makes the break auto-apply on manually-entered hours. NEVER `write()` it — change the province rule or toggle `fusion_clock.auto_deduct_break` instead. Penalty minutes are now strictly additive (the old controller `max()` that could swallow a late clock-in penalty is gone). Rule resolved via `hr.employee._get_fclk_break_rule()` (company `state_id` → matching rule → global `is_default` rule). The retired `break_threshold_hours` setting is superseded by per-rule `break1_after_hours`. - `x_fclk_net_hours` is computed from Odoo `worked_hours` minus break minutes. **Gotcha: `worked_hours` itself subtracts the resource-calendar lunch interval for NON-flexible employees** (Odoo core `hr.attendance._get_worked_hours_in_range`), so the statutory tiers run on lunch-excluded hours; flexible / no-calendar employees get the raw check_in→check_out span. Tests that need a deterministic span give the employee a `flexible_hours` calendar. +- **Migration recompute gotcha**: recomputing ONE stored computed field via `env.add_to_compute(field, recs) + recs.flush_recordset([field])` does NOT cascade to fields that depend on it. The `19.0.4.1.0` post-migrate recomputes `x_fclk_break_minutes`, `x_fclk_net_hours` AND `x_fclk_overtime_hours` (in that dependency order, flushing each) — recomputing only the break left historical `net_hours` stale (caught on the entech deploy 2026-06-01). - Daily overtime compares net hours to the employee's scheduled hours or the daily threshold. (The old `weekly_overtime_threshold` and `grace_period_minutes` settings were removed 2026-05-31 — they were defined/shown but never consumed.) - `fusion_clock.enable_ip_fallback` is honoured: `_verify_location()` only attempts IP-whitelist matching when the toggle is on (default on). - **All fusion_clock Boolean settings are persisted explicitly** (`'True'`/`'False'`) via the `_FCLK_BOOL_PARAMS` loop in `res.config.settings.get_values/set_values`, NOT via `config_parameter=`. Reason: a `config_parameter` Boolean can't be turned OFF (Odoo deletes the param row on a falsy value, so `get_param` returns the default and the feature stays on). When adding a new Boolean setting, add it to `_FCLK_BOOL_PARAMS` with its default; don't use `config_parameter=`. diff --git a/fusion_clock/migrations/19.0.4.1.0/post-migrate.py b/fusion_clock/migrations/19.0.4.1.0/post-migrate.py index 0a853196..1c62c5ba 100644 --- a/fusion_clock/migrations/19.0.4.1.0/post-migrate.py +++ b/fusion_clock/migrations/19.0.4.1.0/post-migrate.py @@ -15,8 +15,12 @@ def migrate(cr, version): ) env = api.Environment(cr, SUPERUSER_ID, {}) Attendance = env['hr.attendance'] - field = Attendance._fields['x_fclk_break_minutes'] closed = Attendance.search([('check_out', '!=', False)]) if closed: - env.add_to_compute(field, closed) - closed.flush_recordset(['x_fclk_break_minutes']) + # Recompute the break AND everything that derives from it, in dependency + # order (break -> net hours -> overtime). Recomputing break alone leaves + # stored x_fclk_net_hours / x_fclk_overtime_hours stale, because + # add_to_compute + flush of one field does not cascade to its dependents. + for fname in ('x_fclk_break_minutes', 'x_fclk_net_hours', 'x_fclk_overtime_hours'): + env.add_to_compute(Attendance._fields[fname], closed) + closed.flush_recordset([fname])