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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-01 00:28:12 -04:00
parent f7ec1e28f9
commit c527c7cade
2 changed files with 8 additions and 3 deletions

View File

@@ -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=`.

View File

@@ -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])