Table export with user-chosen columns — design
Date: 2026-10-09. Status: awaiting owner review.
Two triggers. (1) D365 Finance journal uploads (salary JV, reclass, project fee template) carry
ten-odd dimension columns (LOB, SUBLOB, DEPT, project, region, …) that the journal_entries
import ignores. (2) Exports are inconsistent: the claim export has an org-wide admin column
layout (CSV only), reports have fixed columns, petty cash and parties each have their own
download code. Users cannot choose columns when they export.
Decisions (owner, 2026-10-09)
Section titled “Decisions (owner, 2026-10-09)”- The user picks columns per export, at export time.
- Presets are private by default, optionally shared with the organization; only the owner edits.
- First batch: expense claims, journal entries, petty cash, customers and suppliers, plus
axis.<key>dimension columns on the journal import. “Everything” is reached module by module, made visible by a drift test. - Synchronous, row cap 20,000 (the claim export’s precedent); no background jobs.
Design
Section titled “Design”Registry — platform/tableexport
Section titled “Registry — platform/tableexport”Each export kind registers: key; required read permission; column catalogue (key, default
heading, type text|date|money, default-on); a row source taking the list’s filters and streaming
rows. Dynamic columns (analytic axes, custom fields) come from the organization at request time.
claims moves its catalogue here; the org claim layout becomes its default selection.
journal_entries is one row per line with an axis.<key> column per analytic axis.
Writers are the existing reporting/render csv.go and xlsx.go.
API (declared in api/openapi.yaml first)
Section titled “API (declared in api/openapi.yaml first)”GET /v1/exports/kinds/{kind}/columns— catalogue incl. dynamic columns.- The file itself comes from each owning module’s existing export route (modules may not import
each other, and row visibility rules live in the module), which takes
columns,headingsandformat=csv|xlsxquery parameters; absentcolumns= the kind’s default selection. Over 20,000 rows: refused (422export_too_many_rows) with a message to narrow the filters. (Amended 2026-10-09 while planning; replaces a singlePOST /v1/exports/{kind}.) GET/POST/PUT/DELETE /v1/exports/presets—{kind, name, columns, headings, shared}. New table withorg_id, ENABLE+FORCE RLS, policyorg_isolation. Shared presets are readable org-wide; edit/delete by owner only./expenses/claims/exportstays as a thin wrapper.- Columns the caller may not read are neither listed nor served; the kind’s existing read permission gates everything.
One ExportDialog (kind, current filters): column checklist, reorder, editable headings,
CSV/XLSX, save as preset with share toggle. Replaces the claim Export button, petty cash and
party download code. en/ms/zh strings (ms/zh need native review).
Journal import dimensions
Section titled “Journal import dimensions”journal_entries import accepts axis.<key> columns (same prefix mechanism as cf.). Each cell
is an analytic account code on that axis, posted as a 100% share; unknown code → per-line error
naming the column. Export and import share column names, so an exported file imports back
unchanged. D365 headings (LOB, SUBLOB, DEPT, …) are heading aliases on a built-in preset.
Invariants and tests
Section titled “Invariants and tests”- Money is a decimal string, never a float, in CSV and XLSX (test both).
- Preset table covered by the every-table tenant-isolation test.
- Unauthorised column/kind: assert present for an authorised user first, then absent (presence before absence); watch it fail once.
- Drift: every registered kind has a catalogue and permission; a second test lists list pages without an export.
- Round trip: export journals → import → identical ledger lines and distributions.
Out of scope (separate roadmap items)
Section titled “Out of scope (separate roadmap items)”Background/scheduled/emailed exports; PDF (stays with reports); reversing-entry column on journal import; project billing-lines import (Capcom fee template); multi-currency/tax columns of D365.