Skip to content

Getting data out, and getting it in without typing

Two sides of the same complaint. The employee and contractor directories can now leave HRIS as a spreadsheet, and a SextPanther cutoff can arrive as a paste instead of 165 keystrokes.

Export the employee directory, with the columns you need (P&C)

People & Culture → Employees has an Export CSV action, and so does External contractors.

  • The file is what's on screen. Filter to a department, search a name, and the export is every row that matches — not the page you're looking at.
  • Extra columns are checkboxes, unticked by default: contact & personal, work schedule, referral, linked accounts, bank details and crypto wallet. Contractors get conversion, bank details and crypto.
  • The boxes don't stay ticked. Every export asks again, because "bank details" remembered from three weeks ago is how account numbers end up in a file somebody thought was a name-and-department list.
  • Two separate files, because a contractor has no department, role or employment type and a shared sheet would be half blank.

Guide: Exporting the roster

Paste a SextPanther cutoff instead of typing it

Paste from SextPanther on a model's SP tab takes the text straight off their dashboard — highlight the rows, copy, paste.

  • It reads whichever way the text comes out: a dragged highlight (one value per line) or a copy of the table (tab-separated).
  • It shows what it read before anything moves — how many days, which span, whether SextPanther's own row totals agree with their own columns, and which days fall outside the cutoff you're on.
  • Their row total goes into Reported, so the Diff column checks the paste against their arithmetic. A column landing one place to the left shows up as a red number instead of a plausible wrong figure.
  • Fill grid only drafts. Save cutoff still saves, Discard still undoes, and days the paste didn't mention are left alone.

Guide: Pasting a range instead of typing it

Also

  • A day the paste couldn't read is listed with its line number, and the rest of the paste still comes through.
  • A pasted $0.00 lands as an empty cell, so a pasted cutoff and a typed one look the same afterwards.
  • The paste box keeps its size with a real paste in it. A fortnight is 195 lines, and the box used to grow to fit all of them and push its own buttons off the screen.

Public API (for integrations)

The routing lookup now answers a question it used to leave blank. When nobody manages a creator today it reported manager: null and stopped — true, but a tool chasing an overdue invoice then had nobody to mention.

"manager": null,
"manager_missing": "no_open_assignment",
"former_manager": {
  "employee_id": "…", "name": "Dana Okafor",
  "slack_member_id": "U07MANAGER2", "ended_on": "2026-10-17"
}
  • former_manager is additive — manager keeps its exact meaning, so an integration already reading it needs no change.
  • It is filled only when manager_missing is no_open_assignment. Never beside a live manager, and deliberately not on no_slack_mapping: somebody does own her there, and the fix is mapping their Slack account, not escalating past them.
  • ended_on is the inclusive last day they had her — what lets you decide whether that person is still worth asking.

The reference is in the app at Profile menu → HRIS API, which needs an HRIS account — a partner who only has a key can't read it.

Guide: What an integration calls

Technical changes

For developers and whoever deploys. None of this needs a guide.

Behaviour

  • GET /api/pnc/employees/export/ (HasModule(PNC)) and GET /api/finance/external-contractors/export/ (ContractorAccess) — synchronous text/csv, unpaginated, running each list view's own filterset, search and ordering. ?columns= is repeatable or comma-joined and validated against the group registry; an unknown group is a 400, not a narrower file. Columns always emit in registry order, so a formula built on one export survives the next.
  • core/csv_export.py holds the column-group registry and renderer; each app declares its own groups in pnc/exports.py and finance/external_contractors/exports.py. Both write a UTF-8 BOM — without it Excel reads the file in the local codepage and mangles any non-ASCII name.
  • EmployeeListView.get_queryset is now employee_directory_queryset(), so the export reads the same annotations and LIVE_ASSIGNMENT prefetches as the list rather than a second hand-built copy.
  • POST /api/models/sextpanther/parse-paste/ (HasModule(MODELS)) — parses pasted dashboard text and writes nothing. Not model-scoped: parsing needs no model. Tokenised on newlines and tabs; $, thousands separators, bullets, a leading minus and parenthesised negatives all read. A row whose categories don't sum to its stated total is returned flagged rather than dropped, as are days outside the requested cutoff.
  • AMOUNTS_PER_DAY derives from CATEGORY_FIELDS, so a dashboard column added later changes one tuple.
  • former_manager on /api/public/v1/lookup/model/, read inside integrations/public/lookup/ rather than by widening finance.retainer.scoping.current_manager_for — that function answers "who owns this model", and None is the correct answer there once an assignment has ended, because the retainer engine prices what a manager is owed. Resolved as the most recently ended assignment by end_date, with start_date breaking a same-day tie.

Deploy

  • No migrations. No new settings, env vars or permission surfaces — every gate is one that already existed on the page beside it.
  • Run pnpm generate consumers: two endpoints added to the pnc, finance and models schemas.

Tests

  • The SextPanther parser's headline test is a real 15-day paste, verbatim, asserting every row reconciles to the cent — so a column mapping that drifts fails loudly rather than producing plausible figures.