From 61e1f1492c42752563e81ae3d84d7123e9968517 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Fri, 15 May 2026 21:45:12 +0200 Subject: [PATCH] docs: document Manager/Salaried pay; park feature pending Konrad local verify + 2 follow-ups --- CLAUDE.md | 43 ++++++++++++++++++++++-- docs/plans/parked-work.md | 70 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d228d25..4222f66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. - `` 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: diff --git a/docs/plans/parked-work.md b/docs/plans/parked-work.md index 761c0ad..097b3d0 100644 --- a/docs/plans/parked-work.md +++ b/docs/plans/parked-work.md @@ -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