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>
- 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>
- 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>
_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>
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>
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>
_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>
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>
_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>
(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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>