docs: document Manager/Salaried pay; park feature pending Konrad local verify + 2 follow-ups

This commit is contained in:
Konrad du Plessis 2026-05-15 21:45:12 +02:00
parent 268a050397
commit 61e1f1492c
2 changed files with 110 additions and 3 deletions

View File

@ -77,7 +77,7 @@ staticfiles/ — Collected static assets (Bootstrap, admin) — NOT in git (
## Key Models
- **UserProfile** — extends Django User (OneToOne); minimal, no extra fields in v5
- **Project** — work sites with supervisor assignments (M2M User), start/end dates, active flag
- **Worker** — profiles with salary, `daily_rate` property (monthly_salary / 20), photo, ID doc, PPE sizing (shoe, overall top, pants, tshirt), drivers license (boolean + file upload)
- **Worker** — profiles with salary, `daily_rate` property (monthly_salary / 20), photo, ID doc, PPE sizing (shoe, overall top, pants, tshirt), drivers license (boolean + file upload). `pay_type` CharField (`'daily'` default | `'fixed'` = manager/salaried) + `is_salaried` property (True when `pay_type='fixed'`). Migration `0016_worker_pay_type` (defaults all existing workers to `'daily'`).
- **Team** — groups of workers under a supervisor, with optional pay schedule (`pay_frequency`: weekly/fortnightly/monthly, `pay_start_date`: anchor date)
- **WorkLog** — daily attendance: date, project, team, workers (M2M), supervisor, overtime, `priced_workers` (M2M)
- **PayrollRecord** — completed payments linked to WorkLogs (M2M) and Worker (FK)
@ -119,7 +119,7 @@ When writing ANY formula, filter, comparison, ORM query, test
fixture, CSS class name, or `data-type=` attribute: use the
DATABASE value (left column of the model).
- `ADDITIVE_TYPES = ['Bonus', 'Overtime', 'New Loan', 'Advance Payment']`
- `ADDITIVE_TYPES = ['Bonus', 'Overtime', 'New Loan', 'Advance Payment', 'Salary']`
in `views.py` uses DB values.
- `if adj.type == 'New Loan':` checks the DB value.
- `<span class="badge-type-{{ adj.type|type_slug }}">` produces
@ -143,6 +143,16 @@ string comparisons across ~30 source locations.
**Symptom of getting this wrong:** code that filters for
`type='Loan'` returns zero rows. Fix: use `type='New Loan'`.
**"Manager / Salaried" is a Path-A display label** (same pattern as
"New Loan"→"Loan"): the model stays `Worker`, the discriminator is
`Worker.pay_type` (`'daily'` | `'fixed'`). No `Manager` model/table.
**Adding a value to `PayrollAdjustment.TYPE_CHOICES` DOES generate an
`AlterField` migration in this codebase** — Django tracks `choices` in
migration state, so `makemigrations --check` flags it; always commit
the generated migration. Precedent `0012`; this feature added `0017`
(no-op AlterField for the new `'Salary'` choice).
## 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')`
@ -226,9 +236,35 @@ is the single authority for "which absences can this user see/touch". Admin
sees all; supervisor sees absences for workers in any team they supervise
(`worker__teams__supervisor=user`).
## Manager / Salaried pay (May 2026)
Managers are just `Worker(pay_type='fixed')` (Path-A — no new model;
"Manager / Salaried" is a display-only label, discriminator is
`pay_type`). They are excluded from **attendance + absence pickers
ONLY** (`AttendanceLogForm`, `_build_team_workers_map`, attendance
cost-estimate loop; `AbsenceLogForm`, `AbsenceEditForm`).
**CRITICAL invariant:** managers MUST stay selectable in payroll
modals (`PayrollAdjustmentForm`, the Add-Adjustment modal) and
`TeamForm`. Safety rationale: a manager can never reach a `WorkLog`,
so all WorkLog-derived money math (daily-wage / outstanding /
per-project labour cost) is provably unchanged — managers are paid
purely through the `Salary` adjustment.
Paid via the `Salary` adjustment type through `add_adjustment`:
project-required; "Pay Immediately" → isolated `PayrollRecord` (exact
New Loan pattern); unpaid → generic pending row, netted later by
`_process_single_payment` (NOT modified). Report `_build_report_context`
exposes `salaried_cost_by_project` shown as a separate per-project
"Management / Salaried Cost" card — NEVER merged into WorkLog-derived
daily labour cost. Clean payslip layout via an `is_salary` flag that
mirrors `is_advance`/`is_loan` across `pdf/payslip_pdf.html`,
`email/payslip_email.html`, `core/payslip.html`. `--badge-salary-*`
CSS (dark+light) + `.badge-type-salary`.
## Payroll Constants
Defined at top of views.py — used in dashboard calculations and payment processing:
- **ADDITIVE_TYPES** = `['Bonus', 'Overtime', 'New Loan', 'Advance Payment']` — increase worker's net pay
- **ADDITIVE_TYPES** = `['Bonus', 'Overtime', 'New Loan', 'Advance Payment', 'Salary']` — increase worker's net pay
- **DEDUCTIVE_TYPES** = `['Deduction', 'Loan Repayment', 'Advance Repayment']` — decrease net pay
## Django ORM gotcha — M2M filter + aggregate inflation
@ -268,6 +304,7 @@ OR-union — "adjustments where either FK points to project P".
- **Overtime** — links to `WorkLog` via `adj.work_log` FK; managed by `price_overtime()` view
- **Loan Repayment** — links to `Loan` (loan_type='loan') via `adj.loan` FK; loan balance changes during payment processing
- **Advance Repayment** — auto-created when an advance is paid; deducts from advance balance during `process_payment()`. If partial repayment, remaining balance converts advance to regular loan (`loan_type` changes from 'advance' to 'loan'). Editable by admin (amount can be reduced before payday).
- **Salary** — manager / fixed monthly pay (additive, project-required). Has a "Pay Immediately" path mirroring New Loan (isolated `PayrollRecord`); unpaid nets via `_process_single_payment`. Only for `Worker(pay_type='fixed')` — see "Manager / Salaried pay" section.
## Outstanding Payments Logic (Dashboard)
The dashboard's outstanding amount uses **per-worker** checking, not per-log:

View File

@ -35,6 +35,76 @@ collectstatic — pure template + view change.
---
## ⏸ Paused — implemented locally, awaiting Konrad's verification (not pushed)
### Manager / Salaried Pay
**Status:** Brainstormed + designed + planned + **fully
implemented** (Tasks 1-7), each task two-stage code-reviewed,
execution complete but **HARD STOPPED before push** pending
Konrad's manual local verification (Konrad's call, 15 May 2026).
Design doc `docs/plans/2026-05-15-manager-salaried-pay-design.md`
(local commit `325c59d`), plan
`docs/plans/2026-05-15-manager-salaried-pay-plan.md` (local commit
`4dadb7c`). **The design + plan + all 7 implementation/polish
commits are local-only on `ai-dev`, NOT pushed to origin** — Konrad
wants nothing reaching the working app until he's verified it
locally. **201/201 tests green locally.**
**What it does:** Lets a manager / salaried worker be paid a fixed
monthly amount without ever logging attendance. A new
`Worker.pay_type` (`'daily'` default | `'fixed'`) marks managers;
they're excluded from attendance + absence pickers ONLY (NOT payroll
modals / TeamForm — they must stay payable), and paid via a new
`Salary` `PayrollAdjustment` type (project-required; "Pay
Immediately" → isolated PayrollRecord like New Loan; unpaid nets via
`_process_single_payment`). The report shows a separate per-project
"Management / Salaried Cost" line — never merged into WorkLog-derived
daily labour cost, so all existing money math is provably unchanged.
"Manager / Salaried" is a Path-A display-only label (model stays
`Worker`). Migrations `0016_worker_pay_type`, `0017_alter_payrolladjustment_type`.
**To resume:** Konrad runs the manual verification checklist
("Verification (manual, local — Konrad) — HARD STOP before any
push", section in
`docs/plans/2026-05-15-manager-salaried-pay-design.md`) on a local
instance, then **explicitly approves a push**. Only after that
explicit approval does anything get pushed to `origin/ai-dev`. No
code changes pending — implementation is complete; this is purely a
human verify-then-approve gate.
#### Follow-ups from code review (parked, out of scope for this feature)
1. **Atomicity hardening (cross-cutting, pre-existing).**
`add_adjustment`'s three immediate-payment branches — `New Loan`,
`Advance Payment`, and now `Salary` — create a `PayrollAdjustment`
+ `PayrollRecord` and call `_send_payslip_email` WITHOUT a
wrapping `transaction.atomic()`, and `_send_payslip_email`
re-raises on email failure. A mid-sequence failure can therefore
orphan a payment row / 500 after commit. This is **PRE-EXISTING**
(Salary just consistently matches the New Loan / Advance pattern,
it did not introduce the gap) — flagged by Task 5 code review as a
separate hardening ticket, **NOT a Manager/Salaried defect**.
Recommended future cross-cutting fix: wrap each immediate-branch
body in `transaction.atomic()` and swallow/log email failures the
way `process_payment` already does. Out of scope for this feature.
2. **Pre-existing flaky test.** `AbsenceListViewTests` (in
`core/tests.py`) has an `assertContains`/`assertNotContains`
against the `/absences/` list HTML that intermittently failed
~1-in-3 in one run during this feature's execution, then passed on
rerun; 3+ subsequent full-suite runs were clean. A reviewer's
isolation analysis concluded it's pre-existing environmental
nondeterminism (proper Django `TestCase` transactional isolation;
the Manager/Salaried tests cannot perturb it). Low priority:
investigate a possible date/locale or response-rendering
nondeterminism in that test. **NOT introduced by this feature.**
> Note: the **Post-Attendance Flow v2** paused entry above is STILL
> paused / unchanged — this Manager/Salaried entry does not affect
> it.
---
## Production status — ✅ fully caught up (15 May 2026)
Production (`https://foxlog.flatlogic.app/`) is deployed at