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:
parent
8f81e5ab94
commit
c02edce0b9
34
CLAUDE.md
34
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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user