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) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-05-15 02:11:19 +02:00
parent 8f81e5ab94
commit c02edce0b9

View File

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