diff --git a/CLAUDE.md b/CLAUDE.md index 074207a..3b48ece 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,6 +99,25 @@ outer queryset JOIN-free. See `_build_report_context` in `core/views.py` and `ReportContextFilterInflationTests` in `core/tests.py` for the reference implementation (commit f1e246c, Apr 2026). +## Django ORM gotcha — PayrollAdjustment project double-attribution +`PayrollAdjustment` has TWO project FKs: a direct `adj.project` and an +indirect `adj.work_log.project`. For every **Overtime** adjustment these +always point at the same project (see `price_overtime()` — it sets +BOTH). When rolling up "costs per project" you typically want the +OR-union — "adjustments where either FK points to project P". + +- **Correct**: `Q(project_id__in=ids) | Q(work_log__project_id__in=ids)` filter + + `.annotate(effective_project_id=Coalesce('project_id', 'work_log__project_id'))` + + `.values('effective_project_id', ...).annotate(total=Sum('amount'))`. + Each row contributes to exactly ONE project. +- **WRONG**: two separate filtered querysets (one per FK) summed in + Python. Any row with BOTH FKs set (every Overtime) gets counted twice. + Bit us during the Apr 2026 perf pass — Coalesce fix is commit + `167c821`. Regression test: `PayrollDashboardAdjustmentAggregationTests` + in `core/tests.py`. See `payroll_dashboard()` in `core/views.py` for + the reference implementation on both the unpaid-outstanding card and + the paid-monthly stacked chart. + ## PayrollAdjustment Type Handling - **Bonus / Deduction** — standalone, require a linked Project - **New Loan** — creates a `Loan` record (`loan_type='loan'`); has a "Pay Immediately" checkbox (checked by default) that auto-processes the loan (creates PayrollRecord, sends payslip to Spark, marks as paid). When unchecked, the loan sits in Pending Payments for the next pay cycle. Editing syncs loan amount/balance/reason; deleting cascades to Loan + unpaid repayments @@ -114,6 +133,22 @@ The dashboard's outstanding amount uses **per-worker** checking, not per-log: - This correctly handles partially-paid logs (e.g., one worker paid, another not) - Unpaid adjustments: additive types increase outstanding, deductive types decrease it +## Payroll dashboard query-count baselines (post Apr 2026 perf pass) +Target ranges after `payroll_dashboard()` was optimized with batched +aggregates + `Prefetch(to_attr='active_workers_cached')` + Coalesce-based +project attribution (commits `61c485f` + `167c821`): +- `/` (admin dashboard) — ~15 queries +- `/payroll/?status=pending` — ~24 +- `/payroll/?status=history` — ~24 +- `/payroll/?status=loans` — ~25 +- `/payroll/?status=adjustments` — ~32 + +If any of these jumps meaningfully (>50%) after a future change, an N+1 +was reintroduced. Profile with Django Debug Toolbar (see Profiling +section below) to find it. The test suite does NOT have `assertNumQueries` +guards on these views — deliberate YAGNI for now, worth adding if +regressions become a pattern. + ## Commands ```bash # Local development (SQLite) @@ -131,6 +166,22 @@ python manage.py check # System check USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2 ``` +## Profiling locally — Django Debug Toolbar +Installed as a dev-only dependency in `requirements.txt` since Apr 2026. +Triple-gated in `config/settings.py`: only loads when **DEBUG=true AND +USE_SQLITE=true AND NOT running tests**. Never loads in production — +prod has neither flag, and the test-run gate exists because the toolbar +emits an E001 system-check error + breaks template rendering when +DEBUG=false (which Django forces during `manage.py test`). + +To profile a page: start the dev server normally (`run_dev.bat` or +inline `USE_SQLITE=true DJANGO_DEBUG=true python manage.py runserver`), +log in as admin, navigate to any URL, click the toolbar tab on the +right edge. The **SQL panel** shows query count + highlights any +duplicate-query groups — the go-to tool for N+1 hunting. See the +"Payroll dashboard query-count baselines" section for expected +numbers on hot pages. + ## Development Workflow - Active development branch: `ai-dev` (PR target: `master`) - Local dev uses SQLite: set `USE_SQLITE=true` environment variable