docs: document Manager/Salaried pay; park feature pending Konrad local verify + 2 follow-ups
This commit is contained in:
parent
268a050397
commit
61e1f1492c
43
CLAUDE.md
43
CLAUDE.md
@ -77,7 +77,7 @@ staticfiles/ — Collected static assets (Bootstrap, admin) — NOT in git (
|
|||||||
## Key Models
|
## Key Models
|
||||||
- **UserProfile** — extends Django User (OneToOne); minimal, no extra fields in v5
|
- **UserProfile** — extends Django User (OneToOne); minimal, no extra fields in v5
|
||||||
- **Project** — work sites with supervisor assignments (M2M User), start/end dates, active flag
|
- **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)
|
- **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)
|
- **WorkLog** — daily attendance: date, project, team, workers (M2M), supervisor, overtime, `priced_workers` (M2M)
|
||||||
- **PayrollRecord** — completed payments linked to WorkLogs (M2M) and Worker (FK)
|
- **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
|
fixture, CSS class name, or `data-type=` attribute: use the
|
||||||
DATABASE value (left column of the model).
|
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.
|
in `views.py` uses DB values.
|
||||||
- `if adj.type == 'New Loan':` checks the DB value.
|
- `if adj.type == 'New Loan':` checks the DB value.
|
||||||
- `<span class="badge-type-{{ adj.type|type_slug }}">` produces
|
- `<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
|
**Symptom of getting this wrong:** code that filters for
|
||||||
`type='Loan'` returns zero rows. Fix: use `type='New Loan'`.
|
`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
|
## 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')`
|
||||||
@ -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
|
sees all; supervisor sees absences for workers in any team they supervise
|
||||||
(`worker__teams__supervisor=user`).
|
(`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
|
## Payroll Constants
|
||||||
Defined at top of views.py — used in dashboard calculations and payment processing:
|
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
|
- **DEDUCTIVE_TYPES** = `['Deduction', 'Loan Repayment', 'Advance Repayment']` — decrease net pay
|
||||||
|
|
||||||
## Django ORM gotcha — M2M filter + aggregate inflation
|
## 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
|
- **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
|
- **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).
|
- **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)
|
## Outstanding Payments Logic (Dashboard)
|
||||||
The dashboard's outstanding amount uses **per-worker** checking, not per-log:
|
The dashboard's outstanding amount uses **per-worker** checking, not per-log:
|
||||||
|
|||||||
@ -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 status — ✅ fully caught up (15 May 2026)
|
||||||
|
|
||||||
Production (`https://foxlog.flatlogic.app/`) is deployed at
|
Production (`https://foxlog.flatlogic.app/`) is deployed at
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user