Finance: the invoice you preview is the one you just edited¶
Re-processing a payslip used to hand you back the PDF from before the edit. It doesn't any more, and when an approval reopens a finalised payslip that is now written into the record with a reason. The CT Quarterly Bonus stopped choosing its own cutoff and asks you instead, the export dialog says who is in the file, external contractors gained a Country, and HRIS is moving to a new address.
The invoice preview shows the document you're actually looking at¶
Edit a payslip that had already been processed, then open its invoice, and HRIS would serve the copy it had archived earlier — no bonus line, no correction, nothing you had just done. The figures on screen and the figures in the PDF disagreed, and the PDF was the one people forwarded.
The archive is now served for exactly one thing: a payslip whose status is Sent, where the archived copy is the document that went out. Anything still in Draft or Processed renders live, every time.
- Open the invoice on a processed record and it matches the record.
- A sent record still shows what was sent, unchanged. That's the point of it — and it now says so, in a band across the top of every page giving the date and time it went out, so a figure read off it is never mistaken for a current one.
- Sent invoices still serves the archive completely untouched. That view answers "what exactly did we send her?", so nothing is added to it.
Guide: Viewing & downloading the invoice PDF
Reopening a finalised payslip now says who and why¶
Approving a time adjustment, a Zoom or additional reimbursement, or a quiz win against a payslip that was already processed or sent had to send it back to draft. It did — but silently. No history entry, nobody named, no reason, and the employee could be emailed a PDF that predated the approval that caused it.
Every one of those approvals now reopens the payslip the same way Convert to draft does: the period's history gains an entry naming the approver and what reopened it, and the invoice is rebuilt from the current figures rather than served from before the change.
- The history on a reopened payslip tells you what happened to it.
- A payslip already in draft is left alone — no spurious entries.
Guide: Convert to draft (fixing after the fact) and When something already finalized would change
External contractors have a Country¶
People & Culture → External Contractors now records where each contractor is based, as a searchable dropdown on Add contractor and on the contractor's own page.
- The roster has a sortable Country column, and the CSV export carries it as a standard column — not one of the three optional toggles.
- It's optional, and blank on every contractor created before now, so expect to fill them in as you go.
- Converting a contractor to an employee carries the country across for you. The conversion form doesn't ask, and the invitation only asks the new hire for it if the contractor's was left blank.
Guide: External contractors and Convert a contractor to an employee
HRIS is moving to a new address¶
HRIS now lives at https://hris.aruna-atlas.com. If you open the old
hris-staging.aruna-atlas.com address you'll get a banner across the top of
every page telling you so, with a link straight across.
- Update your bookmark. The old address is being retired and will stop working.
- Nothing about your account changes — same username, same password, same data.
Guide: Log in
AI Marketing is no longer a department you can pick¶
The AI Marketing (AIM) department has been dissolved. It's gone from every department dropdown in the product — Add Employee, invitations, account mapping, convert-to-employee, the P&C departments page and the management area.
Nobody's history is touched. Anyone who was in AIM keeps the department on
their profile, their payroll records and their AIM- staff code, and they can
still be moved to another department normally. You just can't put anyone
into it any more.
You choose which cutoff a quarterly bonus pays on¶
The CT Quarterly Bonus used to pick its own cutoff, and it picked badly. It walked forward from the quarter's own cutoff and skipped any that already had finalised payslips on it — which, by the time anyone prepares a finished quarter, is nearly always the quarter's own. On Q3 that was 37 of 40 members, so the money landed a fortnight late every time. Worse, the skip applied to the whole run: one person's sent payslip moved everybody.
Now there is a Pays on dropdown on the run card. It opens on the quarter's own last cutoff and lists every open cutoff from there onward. Change it and the whole quarter moves. Your choice sticks across Recompute the prepared run.
- A payslip already sent on the cutoff you chose is reopened to take the bonus, rather than the quarter being moved away from it. You are told how many and who, before you approve — each one goes back to Draft and needs re-processing and re-sending, because that person is holding an invoice without the bonus on it.
- Their original invoice is untouched. It stays in the archive exactly as it was emailed, marked as the sent copy.
- One member who needs a different cutoff from the rest is set in their own Decide dialog, and is named above the per-member table so the exception is visible.
Guide: The CT Quarterly Bonus
The export dialog says who is in the file¶
Export CSV on Employees and External Contractors opens with a Who is in the file choice — Active only, Offboarded only, or Everyone. It starts on whatever the page is filtered to and then wins, so you can change the scope without closing the dialog and re-filtering the list behind it. When the two disagree the row count disappears, because it came from the page's query and has stopped being true.
It also settles a disagreement the two pages had: employees defaulted to active-only and contractors to everyone, so the same button meant different things depending which page you were on.
Pay frequency and Monthly payout cutoff are now a single Pay
schedule column reading Every cutoff or Monthly · 2nd cutoff — the way
the contractor table already reads on screen. The second column only ever
meant anything for monthly people, and on an 89-row export it asserted "2nd
cutoff (16–end of month)" about 88 people whose pay has no cutoff half at all.
The employee export's columns have changed
This replaces two columns with one. Anything built on the previous employee export — a saved spreadsheet formula, a downstream import — needs updating.
Guide: Exporting the roster
Also¶
- Removing a payroll row no longer fails with an unexplained error when that person has a pending ArunaPay payout line. The pending line is released and the removal goes through, and it's named in the period's history. A line that has already been sent, confirmed or failed still blocks removal — money has moved, and that's a reconciliation, not a deletion.
Technical changes¶
For developers and whoever deploys. None of this needs a guide.
Data model
finance.CtQuarterlyBonusDecision.cutoff_period— nullable FK toCutoffPeriod,on_delete=PROTECT. Null means follow the run, not unset: a member left on it moves whenrun.target_cutoffchanges, which is why the default is not stamped in at decision time. Existing rows need no backfill for the same reason.finance.CtQuarterlyBonusRun.target_cutoffkeeps its column and gains a second job — Finance's chosen cutoff while the run is pending, not only where an approved one paid.prepare_runno longer clears it.finance.ExternalContractor.country—CharField(max_length=100, blank=True, default=""). A display name, not an ISO code, deliberately matchingpnc.Employee.country: the two have to stay comparable because the value is copied across on conversion.pnc.Department.active—BooleanField(default=True). A flag rather than a delete, becauseEmployeeAssignment.departmentison_delete=PROTECTand a soft delete would take the department name off the profile, payroll row and history of everyone who was ever in it.core.accesssplitsALL_MODULESin two:ALL_MODULESstays the full set and remains the validator,ASSIGNABLE_MODULESis the subset offered in pickers. A retired department's module is still a valid grant, so existing holders keep working; it's simply never offered again.
Behaviour
- The invoice PDF cache is keyed on content instead of identity:
invoices/{layout}/{invoice_id}/{updated_at}.pdf. A line-item change movesupdated_at, so an edited payslip lands on a new key and misses rather than serving a stale render. EmployeePayrollInvoicePdfViewuses theSentInvoicesnapshot only whenstatus == SENT.finance.invoices.stamp.stamp_archived_copybands a served archive at the top of every page. It works on the response bytes only —SentInvoice.pdfis evidence of what the recipient received and is never rewritten — and it returns its input unchanged on any failure, because an unlabelled payslip is a smaller problem than a payslip that won't open. Applied by the employee and contractor record views; deliberately not byfinance.invoices.views.SentInvoicePdfView.- CT Quarterly Bonus cutoff selection is gone:
target_with_skipsandresolve_target_cutoffare removed, and with them theskipped_cutoffsandblockedfields on the run payload.candidate_cutoffsstays and becomes the picker's option list.unpayable_membersbecomespayslips_to_reopen— same query, opposite meaning: a warning rather than a blocker. default_cutoff_for(run)readsrun.target_cutofffirst and ignores it once that cutoff has closed, falling back to the quarter's own cutoff, then to the earliest open one after it. A cutoff chosen last week may not take a line today.approve_runpays each accepted member onto their own cutoff, callsreopen_payroll_if_finalisedon anything finalised there, and returnsreopened. Still@transaction.atomic, so a failure reopens nothing.- New
POST /api/finance/ct-quarterly-bonus/<id>/cutoff/sets the run-level cutoff;nullclears it. - The employee and contractor CSV exports replace
Pay frequency+Monthly payout cutoffwith onePay schedulecolumn, and take anis_activeoverride from the export dialog rather than the page's filter. The employee export header changes — a replacement, not an addition. - New
reopen_payroll_if_finalised(payroll, *, reason, actor)— a no-op on a draft row,convert_payroll_to_draftotherwise. The eight rawpayroll.status = DRAFTdemotions inemployee_requests/services.pyandmanagement/quiz_winners/services.pynow call it. A test scans for any.status = ...DRAFToutside the helper and thePROCESSINGcrash-recovery resets, and names offenders by file and line. remove_payroll_rowreleases a pendingarunapay.ArunaPayPaymentand blocks on sent/confirmed/failed. A test introspectsEmployeePayroll._meta.related_objectsand fails on anyPROTECTFK missing from the declared handled set — deliberately not a generic auto-detach, since each new FK representing money needs its own release-or-block decision.GET /api/pnc/departments/and the P&C dashboard filter onactive=True; three write serializers reject an inactive department.seed_superuser_moduleskeeps a retired grant the superuser already holds. It runs on every deploy, and rebuilding fromSUPERUSER_MODULESalone would revoke a grant as a silent side effect.
Deploy
- Four migrations:
finance.0136_external_contractor_country,finance.0137_ctquarterlybonusdecision_cutoff_period,pnc.0060_department_active,pnc.0061_retire_ai_marketing_department. The last is a data migration flippingactiveon theAIMrow, and it is reversible. So are the rest. - Orphaned cache objects. The invoice PDF key changed shape, so anything
written at the old
invoices/{layout}/{invoice_id}.pdfpath is now unreferenced. Nothing reads it and nothing breaks; it wants a sweep. - New Python dependency:
pypdf(6.21.2), for banding the archived invoice. The image has to be rebuilt —uv syncalone on a running container won't carry it. reportlab was already present but can only write a PDF, not merge onto an existing one. - The site-wide banner is config, not environment. It lives in
frontend/src/lib/site-announcements.tsand is scoped by hostname, so taking it down is an edit and a deploy, not a variable. Nothing to set.
Tests
- Two new guard suites,
backend/core/tests/test_retired_departments.pyandbackend/pnc/tests/test_retired_departments.py, pin the registry and the DB row together — a department marked retired on one side and live on the other would vanish from management surfaces while staying in every combobox. frontend/scripts/check-access-registries.mjsnow compares the retired flag across the two registries, so a one-sided retirement failspnpm lint.