From c02edce0b9098c3ffad570a2b53fdc73e75f4611 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Fri, 15 May 2026 02:11:19 +0200 Subject: [PATCH] docs(CLAUDE.md): daily_rate semantics + dual-code-path note Documents two related foot-guns that bit recent audits: - `Worker.daily_rate` is computed LIVE from `monthly_salary`; it's never snapshotted onto a WorkLog row. So historical cost totals inflate retroactively when a worker gets a raise. The new "at current pay rates" subline on the report hero cards (commit 4186603) is the visible half of this convention. Future audits should read this note before deciding "this can't be right". - Two code paths compute the same formula: Python property and a SQL `Sum(F('workers__monthly_salary') / Decimal('20'))`. They produce identical results in normal use but could drift by 1 cent on edge-case rounding. `CompanyCostVelocitySQLAggregateTests` is the regression test that would catch a real divergence. Findings 2 + 11. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 42af622..3da3c13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -906,3 +906,37 @@ Either works — pick one and stick to it per change to avoid divergence: - This system handles real payroll for field workers — accuracy is critical - `render_to_pdf()` uses lazy import of WeasyPrint to prevent app crash if library missing; on Windows it also auto-registers the GTK3 runtime's DLL directory so `ctypes.find_library()` can locate `gobject-2.0-0` (Python 3.8+ requires explicit `os.add_dll_directory()`) - Django admin is available at `/admin/` with full model registration and search/filter + +### `daily_rate` semantics — read before touching cost formulas + +**The value is live, not snapshotted.** `Worker.daily_rate` is a Python +property defined as `monthly_salary / Decimal('20.00')`. It is NEVER +stored on a WorkLog row. So every cost figure on the dashboard and +report — "Outstanding payments", "Labour Cost by Project", +"Company Avg / Working Day" — reflects whatever the worker's CURRENT +`monthly_salary` is. If you give a worker a raise today, every +historical cost total goes up retroactively. Same direction applies +to demotions. + +Where this matters in practice: +- The "Company Avg / Working Day" and "Company Avg / Month" hero + cards on `/report/` therefore explicitly subline "at current pay + rates" so the reader knows historical comparisons aren't apples + to apples after a raise. +- The same caveat applies (but is not labelled) on every other + cost-vs-time figure. If we ever need a true snapshot-of-the-day + cost, we'd add a `daily_rate_snapshot` DecimalField to WorkLog and + back-fill from history — a much bigger change deliberately deferred. + +**Two code paths compute it. They can drift.** The property does it +in Python; `_get_labour_costs` and `_company_cost_velocity` use a SQL +aggregate `Sum(F('workers__monthly_salary') / Decimal('20'))`. They +SHOULD produce identical results because the formula is the same — +but Python `Decimal` arithmetic and the underlying DB's NUMERIC +division can round differently in the last digit. If a test ever +catches a 1-cent discrepancy, that's why. The +`CompanyCostVelocitySQLAggregateTests` regression test compares the +two paths on a small fixture and would catch a real divergence. +Don't unify the two paths blindly — the property is used in many +template contexts where loading every Worker via a queryset would +be overkill.