Docs: update CLAUDE.md with session learnings

Five focused updates from the Apr 22-23 bug-fix + gitignore session:

1. Fix stale supervisor-picker queryset doc: it was showing the pre-fix
   Q(is_staff)|Q(is_superuser)|Q(groups__name='Work Logger') filter.
   Since commit 0ceceeb the queryset is just User.objects.filter(is_active=True).

2. Update "How to add a new supervisor" step 2: Work Logger group
   membership is no longer required for picker visibility — optional now.

3. Add "Schema name-drifts to remember" block near Key Models. Three
   recurring gotchas that burned four subagent tasks across two sessions:
   - PayrollAdjustment.description (not reason)
   - log.adjustments_by_work_log (not payrolladjustment_set)
   - log.overtime_amount (not log.overtime)

4. Add canonical test-command one-liner to the Commands section:
   USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2

5. Add "Django ORM gotcha" subsection documenting the M2M filter +
   values().annotate(Sum()) inflation bug and the id__in subquery fix
   pattern (refs commit f1e246c, ReportContextFilterInflationTests).

No code changes; no test impact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-04-22 21:36:35 +02:00
parent 88e68f5e36
commit 92036f7e4c

View File

@ -59,6 +59,14 @@ staticfiles/ — Collected static assets (Bootstrap, admin) — NOT in git (
- **WorkerCertificate** — per-worker certifications (Skills, PDP, First Aid, Medical, Work at Height) with `valid_until` expiry and optional document upload. Unique per (worker, cert_type). Has `is_expired` and `expires_soon` (≤30 days) properties.
- **WorkerWarning** — disciplinary records per worker with severity (verbal/written/final), reason, optional document. Ordered -date.
### Schema name-drifts to remember
Fields / accessors that differ from what you'd guess. Each has bitten multiple
sessions; grep `core/models.py` before using any field you haven't used before:
- `PayrollAdjustment.description` — NOT `reason`
- `log.adjustments_by_work_log` (reverse accessor for PayrollAdjustment.work_log FK) — NOT `payrolladjustment_set` (the FK has `related_name` set)
- `log.overtime_amount` (DecimalField, default 0.00) — NOT `log.overtime`
## Key Business Rules
- All business logic lives in the `core/` app — do not create additional Django apps
- Workers have a `daily_rate` property: `monthly_salary / Decimal('20.00')`
@ -75,6 +83,17 @@ Defined at top of views.py — used in dashboard calculations and payment proces
- **ADDITIVE_TYPES** = `['Bonus', 'Overtime', 'New Loan', 'Advance Payment']` — increase worker's net pay
- **DEDUCTIVE_TYPES** = `['Deduction', 'Loan Repayment', 'Advance Repayment']` — decrease net pay
## Django ORM gotcha — M2M filter + aggregate inflation
Chained `.filter(m2m__field=X).filter(m2m__other=Y)` creates **separate JOIN
aliases**, producing a cartesian product of rows. `.aggregate(Sum(...))` dedupes
via subquery when `distinct()` is present; `.values().annotate(Sum(...))` does
NOT — it `GROUP BY`s the inflated rows and multiplies sums by N×M (where N and
M are the counts of matching related rows). Fix pattern: use
`.filter(id__in=Model.objects.filter(m2m__field=X).values('id'))` to keep the
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).
## 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
@ -102,6 +121,9 @@ python manage.py setup_test_data # Populate sample workers, projects,
python manage.py import_production_data # Import real production data (14 workers)
python manage.py collectstatic # Collect static files for production
python manage.py check # System check
# Run the test suite (sets env vars inline — works in Git Bash; on cmd.exe use `set` first)
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
```
## Development Workflow
@ -420,13 +442,16 @@ When editing a Team or Project via the friendly UI (`/teams/<id>/edit/` or
`_supervisor_user_queryset()` in `core/forms.py`:
```python
User.objects.filter(is_active=True).filter(
Q(is_staff=True) | Q(is_superuser=True) | Q(groups__name='Work Logger')
).distinct()
User.objects.filter(is_active=True).order_by('username')
```
So anyone who's either an admin OR a Work Logger shows up as an eligible
supervisor. Deactivated accounts (`is_active=False`) are hidden.
Any active user can be picked. The picker is deliberately NOT pre-filtered
by group/staff flags because `is_supervisor(user)` (views.py) grants
supervisor powers to anyone assigned to a team/project FK/M2M — so the
picker shouldn't be stricter than the permission model. Pre-Apr 2026 the
picker required Work Logger group membership, which hid valid supervisors
(see commit 0ceceeb for the fix + regression tests). Deactivated accounts
are still hidden.
### Typical user setups
@ -449,10 +474,13 @@ we'd have to add a separate flag or group check — not currently supported.
1. Go to `/admin/auth/user/add/` and create the user with a username and
password. **Uncheck "Staff status"** on the initial form (they don't need
Django admin access).
2. On the user's change page, add them to the **Work Logger** group.
2. (Optional) Add them to the **Work Logger** group if you want
`is_supervisor(user)` to return True even without a team/project
assignment. Not required for the picker to show them — the picker
shows any active user (see commit 0ceceeb, Apr 2026).
3. (Optional) Assign them as the supervisor of one or more teams via
`/teams/<id>/edit/` (Supervisor dropdown — they'll appear in the list
because of their Work Logger group membership).
because they're active).
4. (Optional) Add them to one or more projects via `/projects/<id>/edit/`
(Supervisors M2M checklist).
5. They can now log in at `/accounts/login/` and will land on the Dashboard