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. - **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. - **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 ## Key Business Rules
- All business logic lives in the `core/` app — do not create additional Django apps - 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')` - 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 - **ADDITIVE_TYPES** = `['Bonus', 'Overtime', 'New Loan', 'Advance Payment']` — increase worker's net pay
- **DEDUCTIVE_TYPES** = `['Deduction', 'Loan Repayment', 'Advance Repayment']` — decrease 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 ## PayrollAdjustment Type Handling
- **Bonus / Deduction** — standalone, require a linked Project - **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 - **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 import_production_data # Import real production data (14 workers)
python manage.py collectstatic # Collect static files for production python manage.py collectstatic # Collect static files for production
python manage.py check # System check 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 ## 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`: `_supervisor_user_queryset()` in `core/forms.py`:
```python ```python
User.objects.filter(is_active=True).filter( User.objects.filter(is_active=True).order_by('username')
Q(is_staff=True) | Q(is_superuser=True) | Q(groups__name='Work Logger')
).distinct()
``` ```
So anyone who's either an admin OR a Work Logger shows up as an eligible Any active user can be picked. The picker is deliberately NOT pre-filtered
supervisor. Deactivated accounts (`is_active=False`) are hidden. 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 ### 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 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 password. **Uncheck "Staff status"** on the initial form (they don't need
Django admin access). 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 3. (Optional) Assign them as the supervisor of one or more teams via
`/teams/<id>/edit/` (Supervisor dropdown — they'll appear in the list `/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/` 4. (Optional) Add them to one or more projects via `/projects/<id>/edit/`
(Supervisors M2M checklist). (Supervisors M2M checklist).
5. They can now log in at `/accounts/login/` and will land on the Dashboard 5. They can now log in at `/accounts/login/` and will land on the Dashboard