276 Commits

Author SHA1 Message Date
Konrad du Plessis
e3e5bdeb78 docs: audit bundle pushed to origin — breadcrumb now tracks the pending prod deploy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:23:23 +02:00
Konrad du Plessis
2d3cc43984 fix: run_dev.bat works from any launch directory (cd to its own folder first)
The bat assumed it was started from the repo root — python couldn't
find manage.py when the Claude preview runner (or any other launcher)
started it from elsewhere. cd /d "%~dp0" pins it to the script's own
directory. .claude/launch.json (gitignored) now also passes the bat's
absolute path so cmd finds it regardless of working directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:03:09 +02:00
Konrad du Plessis
94b6ec08e1 docs: audit-fix breadcrumb (HARD STOP), resolve stale SiteReport push status, fix views.py size
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:07:20 +02:00
Konrad du Plessis
921bdb6b73 chore: money-handling + template hygiene (audit items 11-16)
- money filter: format via Decimal(str(...)) instead of float — money
  never passes through binary floating point on its way to the screen
- preview_payslip / worker_lookup_ajax: accumulate totals in Decimal,
  convert to float only at the JsonResponse boundary, so previews are
  bit-identical to what _process_single_payment records
- price_overtime: explicit .quantize(0.01) instead of letting the DB
  engine silently round the 3rd decimal place
- stacked-chart wage rollup: Decimal(str(salary)) — on SQLite dev the
  value arrives as float and Decimal(float) keeps the binary noise
- payroll_dashboard fmt(): en-ZA space-separated thousands like every
  other money helper on the page (was the one comma-format outlier)
- base.html admin gate: '(auth and staff) or superuser' precedence trap
  replaced with 'staff or superuser' — exact same truth table (both
  flags False on AnonymousUser), matches server-side is_admin()
- attendance_log.html: worker day-rates now ship via the house
  json_script pattern instead of |safe-rendering a Python dict repr
  into the script block

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:04:01 +02:00
Konrad du Plessis
541b8973c7 perf: kill per-row queries on the History tab and Batch Pay preview
- paid_records (History tab) now prefetches work_logs + adjustments:
  the template shows a day-count and loops adjustments per row, which
  fired 2 queries per visible record (~100 on a long history).
- batch_pay_preview replaces the per-worker get_worker_active_team()
  call (worker.teams.filter(...).first() — bypasses prefetch, 1 query
  per worker) with the same batched membership-dict pattern
  payroll_dashboard already uses, and reads the unpaid-adjustments
  check from the existing filtered prefetch instead of .exists().

Also includes (committed earlier in 25910b2 but noting for the record):
the /report/ worker-breakdown loop's per-worker-per-type aggregates
were replaced by one GROUP BY dict (audit fix #7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:58:56 +02:00
Konrad du Plessis
25910b2861 fix: worker batch report 'Total Paid' was inflated by the work-log join (+ kill its N+1s)
_build_worker_report_context computed Sum('payroll_records__amount_paid')
in the SAME .annotate() as counts over work_logs and warnings. Django
joins ALL those relations into one query, so each payroll row was
duplicated once per work-log row — the lifetime Total Paid column on
/workers/report/ (HTML, CSV and PDF) was multiplied by the worker's
log count (3 logs + one R100 payslip displayed R300). The distinct=True
counts were immune, which is why the suite never caught it. Verified by
a controlled experiment before fixing; regression test added.

Restructure: payroll aggregates stay in one annotate (same join — safe);
days-worked, warnings and project names move to three batched GROUP BY
dicts; teams + certificates are prefetched. Also removes the ~4 queries
per worker the loop used to fire (audit findings #8).

Display-only bug — payment processing was never affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:56:39 +02:00
Konrad du Plessis
f0f3938621 fix: worker report views survive stale/typo'd ?project= and ?team= params
All three worker batch-report views (HTML / CSV / PDF) passed raw
query-string values into queryset filters — ?team=abc raised ValueError
deep in the ORM, and the PDF view's display-name lookups raised
Project/Team.DoesNotExist on a deleted id (stale bookmark) → 500s.

New _int_param_or_none() sanitizer coerces filter params at the view
boundary (junk degrades to 'no filter'); the PDF's display-name lookups
fall back to 'All Projects'/'All Teams' on DoesNotExist. Template
dropdowns already compare via |stringformat so int params are safe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:50:48 +02:00
Konrad du Plessis
4d029dd6e5 fix: cap attendance date range at 31 days (year-typo flood guard)
AttendanceLogForm.clean only checked end >= start — a typo'd year in
the end-date field created a WorkLog per worker per day for the whole
span (365+ days) in one submit, with no way back but manual deletion.
Ranges longer than 31 days (a full calendar month) are now rejected
with a message pointing at the year fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:47:27 +02:00
Konrad du Plessis
81753695a1 fix: refuse payments where deductions exceed earnings (no negative PayrollRecords)
_process_single_payment wrote logs_amount + adj_amount straight into
PayrollRecord.amount_paid with no floor — a selection where deductions
beat earnings (easy via the split-payslip checkboxes: untick the work
logs, leave a big loan repayment ticked) recorded a NEGATIVE payment,
emailed a negative payslip to Spark Receipt, and deducted the loan.

Per Konrad's decision (12 Jun 2026): REFUSE such payments. New
DeductionsExceedEarningsError raised inside the atomic block (full
rollback — loan balance and adjustments untouched); process_payment
shows a clear error toast, batch_pay names the refused worker in its
summary. Exactly-zero net stays allowed (a repayment consuming the
whole wage legitimately settles a loan with a R 0.00 payslip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:45:39 +02:00
Konrad du Plessis
7ce3bfb232 fix: Batch Pay modal — filters no longer silently re-tick unticked workers; surface server errors
Two payment-safety fixes in the Batch Pay modal JS:

1. Changing the team/loan filter force-checked every visible row,
   silently re-selecting workers the admin had deliberately unticked
   (untick a disputed worker -> change filter -> Confirm & Pay All pays
   them anyway). Filters now only EXCLUDE (hidden rows untick); visible
   rows keep the admin's manual choice, and Select All reflects the
   real state instead of being forced on.

2. The batch-pay fetch() redirected to the dashboard on ANY HTTP
   response — fetch only rejects on network failure, so a 500 (batch
   died partway; each worker pays in its own transaction) looked like
   success. Now checks resp.ok and tells the admin to verify the
   History tab before retrying.

JS-only change; needs manual verification in the browser (no JS test
harness in this project).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:42:39 +02:00
Konrad du Plessis
cfc78b72ad fix: email failure after payment no longer shows a 500 to interactive callers
_send_payslip_email caught email errors, queued a warning toast, then
re-raised unconditionally — but only batch_pay catches that re-raise.
The Pay button (process_payment) and the three Pay-Immediately paths in
add_adjustment called it bare, so an SMTP failure AFTER the payment
committed replaced the warning with a 500 error page (the 28 May 2026
incident behaviour — admin can't tell the payment saved, may pay twice).

Re-raise is now conditional on suppress_messages=True (the batch path,
which counts failures in its summary); interactive callers get the
warning toast as intended. Regression tests cover both sides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:41:20 +02:00
Konrad du Plessis
14ab8d0f76 docs: capture 27-29 May incident lessons (two-tier env precedence, --insecure, SSH access) + gitignore .claude.local.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:36:45 +02:00
Konrad du Plessis
663b7d98ba docs: park 2 Manager/Salaried follow-ups (Spark Receipt payslip verify + monthly_salary display)
(3) PENDING: Konrad testing 17 May whether Spark Receipt handles the
is_salary payslip layout for variable-pay managers. (4) Deferred:
monthly_salary required field is misleading on manager report rows
(inert for pay; display-only tidy-up parked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 06:25:25 +02:00
Konrad du Plessis
e71109d27e docs: scrub SiteReport from CLAUDE.md + park rebuild (capture doc)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:11:37 +02:00
Konrad du Plessis
120f21d645 polish: fix stale attendance_log comment + idiomatic WorkLog.objects.all() (SiteReport removal cleanup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:06:32 +02:00
Konrad du Plessis
7f5e4c9c50 feat!: remove SiteReport / Log Today's Work feature (model, code, UI, tests)
Drops core_sitereport via 0018_delete_sitereport. Knowledge preserved
in docs/plans/2026-05-17-site-report-removed-capture.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:52:19 +02:00
Konrad du Plessis
15d9132fb2 docs: fix stale AbsenceAttendanceShortcutTests docstring (no Site Report flow)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:43:27 +02:00
Konrad du Plessis
f9b190a26d refactor: revert post-attendance flow to redirect-home (SiteReport removal step 1)
Also deletes AttendanceLogRedirectsToSiteReportTests (it asserted only
the removed redirect destination — a behavioural test, not a model
test). Suite 209 -> 208.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:35:14 +02:00
Konrad du Plessis
a502bac8ec docs: TDD plan for SiteReport removal (3 tasks, HARD STOP)
Task 1 revert attendance flow (TDD via 2 rewritten cross-ref tests),
Task 2 delete the SiteReport surface + 5 test classes + autogenerate
0018_delete_sitereport (suite 209->193), Task 3 docs. HARD STOP +
grep-clean/makemigrations-check gate before any push. Local-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:27:40 +02:00
Konrad du Plessis
777c7c6dcc docs: SiteReport removal design + future-rebuild capture doc
Konrad-approved design to fully remove the SiteReport / "Log Today's
Work" feature (drop core_sitereport, no backup, revert post-attendance
to redirect-home). Capture doc preserves the schema-as-Python pattern,
the flow, recovery pointers, and rebuild guidance. Local-only; the
removal itself is HARD-STOPPED before push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:23:09 +02:00
Konrad du Plessis
aaca0b36d3 docs: production caught up & verified (Manager/Salaried bundle live)
Konrad confirmed the 36-commit bundle "all working well" on prod
(17 May 2026). Flip CLAUDE.md + parked-work.md production status from
"deploy pending" to " fully caught up & verified at 80d96d7".
Also flags the in-progress (local-only) SiteReport removal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:21:22 +02:00
Konrad du Plessis
80d96d7c91 docs: breadcrumb accuracy — Manager/Salaried bundle pushed (4c25011), deploy pending
Flip parked-work.md + CLAUDE.md from "paused, not pushed" to "pushed to
origin/ai-dev d7015b9..4c25011, Flatlogic VM deploy pending (migrate +
collectstatic + restart-last)". Prevents a fresh session reading stale
"not pushed" status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:50:13 +02:00
Konrad du Plessis
4c250110e2 docs: note Pay Salary quick action (rides paused bundle)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:06:25 +02:00
Konrad du Plessis
9ab0c68243 feat: Pay Salary quick action on home dashboard (deep-link to modal)
Admin Quick Actions tile -> /payroll/?action=pay-salary; the payroll
page auto-clicks the existing paySalaryBtn then strips the param.
Reuses all existing Pay-Salary machinery; param inert server-side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:58:10 +02:00
Konrad du Plessis
56c10ab938 docs: TDD plan for Pay Salary quick action (2 tasks, HARD STOP)
Task 1: tile + deep-link hook + render test (TDD on the Django-render
part; auto-click is JS/manual-checklist). Task 2: docs. Suite 207->208.
Nothing pushed until Konrad's local verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:54:30 +02:00
Konrad du Plessis
fb19655a1d docs: design for Pay Salary dashboard quick action
Konrad-approved: a home-dashboard admin Quick Actions tile that
deep-links /payroll/?action=pay-salary and auto-clicks the existing
paySalaryBtn (then strips the param). Reuses all existing machinery;
no view/model/URL change. Rides the same paused-bundle HARD STOP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:52:58 +02:00
Konrad du Plessis
b397cdf46c docs: note Salary auto-scope picker (rides paused bundle)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:31:28 +02:00
Konrad du Plessis
bf3b63fc6b docs: clarify no synthetic change-event needed in Salary sync block
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:27:26 +02:00
Konrad du Plessis
31ee9e2e3c feat: type=Salary auto-scopes Add-Adjustment picker to managers
When type=Salary: set pay-type filter to Managers-only, hide daily
rows, and untick any selected daily worker so a Salary can never
silently target a daily worker. Re-applied on the Pay-Salary open
path (the show.bs.modal reset clears it first). Pure JS; verified by
manual checklist; suite stays 207/207.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:20:46 +02:00
Konrad du Plessis
0c705129f6 docs: TDD plan for Salary auto-scope picker (2 tasks, HARD STOP)
2 small tasks: (1) toggleProjectField() Salary sync + _paySalaryOpen
re-apply, (2) docs. JS-only — manual-checklist verified, suite stays
207/207. Nothing pushed until Konrad's local verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:14:06 +02:00
Konrad du Plessis
8f443faebc docs: design for Salary auto-scope picker (filter + auto-untick)
Konrad-approved: when Add-Adjustment type=Salary, auto-set the pay-type
filter to Managers-only, hide daily rows, and untick any selected daily
worker so a Salary can never silently target a daily worker. Pure JS,
hooks the toggleProjectField() chokepoint. Rides the same HARD STOP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:12:03 +02:00
Konrad du Plessis
0d77d7228d docs: note managers pay-type filter (rides with paused Manager/Salaried)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:05:34 +02:00
Konrad du Plessis
18ec393c0a fix: reset Add-Adjustment pay-type filter on every modal open
Prevents a pre-checked quick-adjust worker from opening hidden behind a
stale 'Managers only'/'Daily only' filter. Display-only; no data impact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:01:34 +02:00
Konrad du Plessis
3a18ea008a feat: 'Managers only' client-side filter on Add-Adjustment picker
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:55:41 +02:00
Konrad du Plessis
fb8952a323 feat: pay-type dropdown on /workers/ filter row
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:49:04 +02:00
Konrad du Plessis
d949a01550 refine: document ?pay_type= param + add unknown-value regression test
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:45:24 +02:00
Konrad du Plessis
a442658430 feat: ?pay_type= filter on /workers/ (managers/daily, display-only)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:40:40 +02:00
Konrad du Plessis
45871225e1 docs: TDD plan for Managers pay-type filter (4 tasks, HARD STOP)
4 bite-sized TDD tasks: (1) worker_list ?pay_type= view+tests,
(2) /workers/ dropdown, (3) Add-Adjustment modal data-pay-type +
client-side toggle, (4) docs. ~205/205 expected. Nothing pushed until
Konrad's local verification — rides with paused Manager/Salaried.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:17:39 +02:00
Konrad du Plessis
4aac2c1cf2 docs: design for Managers pay-type filter (Approach A, display-only)
Konrad-approved design for a display-only ?pay_type= filter on /workers/
and a "Managers only" toggle on the Add-Adjustment modal picker. No
model/migration/URL changes; rides with the paused Manager/Salaried
feature's HARD STOP (nothing pushed until local verification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:13:30 +02:00
Konrad du Plessis
4d06b83e30 docs: correct Salary verification step (manual amount entry, not auto-filled)
Final whole-feature review flagged the design doc's verification
checklist step 4 over-promised an auto-filled amount. Manual entry is
intentional; corrected so Konrad's local verification expectations match
actual behaviour. Docs-only, local-only — feature still NOT pushed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:00:26 +02:00
Konrad du Plessis
61e1f1492c docs: document Manager/Salaried pay; park feature pending Konrad local verify + 2 follow-ups 2026-05-15 21:45:12 +02:00
Konrad du Plessis
268a050397 polish: Salary multi-adjustment payslip-layout guard test; tighten list test; a11y badge contrast 2026-05-15 21:34:12 +02:00
Konrad du Plessis
862766f9b5 feat: pay_type UI (form/list/detail), Salary modal+entry, badge, clean Salary payslip
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:14:27 +02:00
Konrad du Plessis
d6f12e7dd1 polish: document team-filter Salary scoping; stricter report regression assertion 2026-05-15 20:52:30 +02:00
Konrad du Plessis
65b10e74ec feat: per-project Management/Salaried Cost report line + regression & netting guards 2026-05-15 20:35:11 +02:00
Konrad du Plessis
255ec82cef feat: add_adjustment Salary branch (project-required, pay-now/pending) 2026-05-15 20:15:30 +02:00
Konrad du Plessis
86b0cb9dd6 test: strengthen Task 4 absence-exclusion tests (parity + behavioral POST guard) 2026-05-15 20:05:15 +02:00
Konrad du Plessis
5fa3efcf64 feat: exclude fixed-salary managers from absence pickers 2026-05-15 19:55:42 +02:00
Konrad du Plessis
0f45d64eea fix: close inline team-map manager-exclusion gap + add cost-rate exclusion test 2026-05-15 19:46:34 +02:00
Konrad du Plessis
65df9f817e feat: exclude fixed-salary managers from attendance pickers 2026-05-15 19:36:33 +02:00