Skip to content

Accounting checklist: specification

Status: phase 1 built and merged to main (PR #33, 229ec438). Rationale and options are in 2026-10-08-closing-checklist-design.md; this file is what gets built and how it is accepted. Phase 2 (automatic tracking of rows and columns from real ERP data) is planned separately in 2026-10-09-checklist-auto-verification-plan.md. Source sheet: “Mcom Groups, Summary of Check List.xlsx” (19 sections A to S, 85 rows).

Merged to main in PR #33: migrations 01143 (tables) and 01144 (Owner grants), internal/platform/checklist, internal/modules/checklist, the /checklist/... API, and web/src/features/checklist. Where it differs from the text below, this list wins:

  • A template is a run with is_template = true. One set of tables (checklist_run, _column, _section, _item, _event) serves both, so there are no separate checklist_template_* tables: starting a run and saving a layout as a template are both a copy.
  • The source sheet has 85 rows, numbered 1 to 84 with 58 used twice; the seeded default has 19 sections and 85 rows, numbered by position, so the duplicate cannot occur.
  • Permissions are checklist.read, checklist.work, checklist.manage (no manage_group); Owner gets all three by migration 01144.
  • Runs have a period end date only (no period start), so the “back-dated before the period” reason rule is not built. A reason is still required for N/A and un-N/A, unlock, changing or clearing a performed date set more than 24 hours ago, and removing a row that has a performed date.
  • Autosave is as in 3.3a: edits commit on Enter, Tab or blur, queue in the browser and go out as one POST /checklist/runs/{id}/commits after 500 ms of quiet; each commit has its own result. Later commits to a row inside one batch follow the first and are not stale against it. The F22a merge of repeated commits is not built (the setting’s default is off, so every commit is one event).
  • Not in phase 1: Excel import and export, pasting rows from Excel, undo, group default per organization group, per-user column widths, column type change. The default checklist is seeded from the source sheet the first time a run is started.
  • Page location: Workspace > Checklist in the sidebar (after Activity), at /checklist (runs and templates) and /checklist/{runId} (the table). A template opens in the same page. Permission checklist.read shows it; nav entry in web/src/app/shell/nav.ts.
  • Edit in place, edit layout for structure. Anyone with checklist.manage edits Category, Description, Standard Deadline, the column headings and the section code/title directly in the table, with no mode switch (the same Excel-style commit on Enter, Tab or blur). “Edit layout” is only for structure: add, move, hide and delete columns, sections and rows. Staff with checklist.work fill Performed Date, N/A and Remarks. A locked run is read-only for everyone.
  • Cells grow to their text. Category, Description, Deadline and headings are multi-line boxes that resize to fit (Enter saves, Shift+Enter adds a line) and the columns have minimum widths, so nothing is clipped behind a scrollbar.
  • What the counts mean. Done = rows with a Performed Date. Not applicable = rows marked N/A. Open = rows - done - N/A. The progress bar and each section’s “x / y” are (done + N/A) / rows. Nothing is verified by the system in phase 1; a date typed by hand counts the same as the real work. Phase 2 splits the count into verified and manual.
  • Column types are fixed once added (text, date, number, pick-list, person, tick box); a wire date is YYYY-MM-DD.

An editable, logged version of the accounting closing checklist, at Workspace > Checklist.

In scope: month-end and year-end closing checklists for accounting staff; per-company copies; editable columns, sections and rows; Performed Date recording; change history; Excel import and export. Out of scope (phase 3): assignees and reminders, computed due dates and overdue flags, auto-ticking from ERP modules, group roll-up, year-on-year comparison. The data shape below does not block them.

Permission Who Can
checklist.view any accounting member open a run, read history
checklist.work accounting staff set or clear Performed Date, mark N/A, edit Remarks and custom cells marked editable
checklist.manage accounting manager everything in “Edit layout”: columns, sections, rows, start and lock runs, edit the template
checklist.manage_group group controller edit the group default template; copy it to companies

Permissions are registered like the other module permissions and granted per role, so a tenant can map them to its own roles. A locked run is read-only to everyone except for comments.

  • F1. A template is a named list (for example “Year-end close”) made of sections, rows and columns. The group has one default template; each company may keep its own copy, made by copying the group’s.
  • F2. A run is one template for one company and one period (a date range with an end date). Starting a run copies the template’s sections, rows and columns into the run. Later template edits do not change a run.
  • F3. A run has a status: open or locked. Locking is allowed to checklist.manage only and can be undone only with a reason (logged).
  • F4. Starting a second open run for the same company, template and period is refused (duplicate_run).
  • F5. Default columns: No, Category, Description, Standard Deadline, Performed Date. Rows are grouped under section headers “A General Closing” etc., collapsible, each showing “done / total”.
  • F6. Performed Date holds a date or is empty. Setting it records who set it and when. A future date is refused; a date before the run’s period start asks for a reason.
  • F7. A row can be marked Not applicable with a required reason; it then counts as complete and shows “N/A”.
  • F8. Remarks is free text, available to checklist.work.
  • F9. Filters: text search, section, status (open / done / N/A), “mine” (rows I last changed). A progress line shows done, N/A and open counts.
  • F10. Export produces an .xlsx in the original layout (section header rows, column headings as currently named, Performed Date filled), plus the history as a second sheet.
  • F11. Columns. Rename, hide, show, reorder, add and delete. Types: text, date, number, pick-list (options defined on the column), person, tick box. The five built-in columns can be renamed, hidden and reordered but not deleted. Deleting a custom column hides it and keeps its values.
  • F12. Sections. Add, rename (code and title), reorder, delete if empty. A section with rows cannot be deleted (section_not_empty); the user must move or delete the rows first.
  • F13. Rows. Edit any cell in place; insert above or below; add at the end of a section; duplicate; drag to reorder, including between sections; delete (kept in history, hidden from the table).
  • F14. Numbering. Row numbers are assigned by position within the run and renumber automatically. They are never typed in, so duplicates cannot occur.
  • F15. Edits in a run apply to that run only. A manager can “save layout to template” to copy the run’s layout (not its Performed Dates) back to the company template, creating a new template version.
  • F16. Keyboard: Enter saves and moves down, Tab moves right, Esc cancels. Pasting tab-separated rows from Excel inserts them as rows at the cursor, mapped by column order; the user sees a preview before saving. Undo reverts the user’s last edit and is itself logged.

3.3a Autosave (Excel-style: commit on Enter, batched)

Section titled “3.3a Autosave (Excel-style: commit on Enter, batched)”
  • F16a. Edits behave like Excel. Typing in a cell is a draft that lives only in the browser (edit mode): nothing is sent per keystroke or while the user pauses, and Esc discards it. The cell is committed when the user presses Enter or Tab, clicks another cell, or picks a date, ticks a box or chooses a list option. Navigating away or closing the tab commits the open cell first.
  • F16a2. Commits are batched, not sent one by one. A commit is applied on screen immediately, then queued; the queue is sent after a 500 ms quiet period as one request holding all pending commits in order (so filling twenty Performed Dates down a column is one request, not twenty). Each commit in the batch keeps its own version check, its own result and its own activity event; one failure does not roll back the others.
  • F16b. Each cell shows its state: saving (small spinner), saved (tick, fades), or failed (red outline with Retry). A page-level “All changes saved” / “Saving…” / “N changes not saved” indicator sits in the toolbar. Failed saves stay in the cell and retry on request and automatically when the connection returns; the user’s text is never discarded.
  • F16c. Commits to the same cell are sent in order, and a later commit supersedes an earlier unsent one. Each commit carries the cell’s version; a 409 reloads that cell and shows “Changed by just now”, keeping the user’s text so they can reapply it.
  • F16d. Edits that need a reason (F20: N/A, back-dated date, clearing an old date, removing a dated row) do not autosave silently: a small reason prompt opens first, and the change is saved when the reason is confirmed. Cancel reverts the cell.
  • F16e. Layout edits (rename a column or section, reorder, add, delete) save immediately, not batched.
  • F17. Every change is recorded by the server in the same database transaction as the change: a failed change leaves no event, and a change without an event cannot happen.
  • F18. Each event holds: run, row (if any), actor, time, kind, old value, new value, reason, and source (web, import, api).
  • F19. Kinds: run_started, run_locked, run_unlocked, performed_set, performed_changed, performed_cleared, marked_na, unmarked_na, remarks_changed, cell_changed, row_added, row_removed, row_moved, section_added, section_renamed, section_moved, section_removed, column_added, column_renamed, column_hidden, column_removed, layout_saved_to_template, imported.
  • F20. A reason is required for: marked N/A, unmarking N/A, back-dated Performed Date, clearing a Performed Date that was set more than 24 hours ago, unlocking a run, removing a row that has a Performed Date.
  • F21. The row history icon shows that row’s events; the run Activity tab shows all, filterable by person, kind, section and date range, and exportable to CSV.
  • F22. Events also write one line to the existing activity feed. performed_*, marked_na, run_locked and run_unlocked also write the existing hash-chained audit log.
  • F22a. One commit is one event, as in Excel’s own edit history: pressing Enter on a cell is one logged change, so the log reflects what the user decided, not their typing. A cell the same person re-commits within a short window may merge into one event (keeps the first old_value, takes the latest new_value); the window is a setting, default 0 (off), so every commit is logged unless a tenant turns merging on. Dates, N/A, layout and any change with a reason are never merged.
  • F23. Events are append-only and never edited or deleted by the application.
  • F24. A one-time import reads the .xlsx structure: a row whose first cell is a letter and second cell a title is a section; a row whose first cell is a number is an item (category, description, deadline, performed date). Blank rows are ignored. Anything unrecognised is listed in a preview and skipped, not guessed. Duplicate numbers are reported and renumbered.

All tables carry org_id and use the existing org row-level-security policy. IDs are UUIDs.

  • checklist_template(id, org_id, name, version, kind [month_end|year_end|custom], owner_scope [group|company], created_by, created_at, active)
  • checklist_template_column(id, template_id, key, label, type, options jsonb, position, visible, built_in)
  • checklist_template_section(id, template_id, code, title, position)
  • checklist_template_item(id, template_id, section_id, position, category, description, deadline_text, cells jsonb)
  • checklist_run(id, org_id, template_id, template_version, period_start, period_end, status, locked_by, locked_at, created_by, created_at), unique open run per (org, template, period_end).
  • checklist_run_column, checklist_run_section: same shape as the template tables, keyed by run_id.
  • checklist_run_item(id, run_id, section_id, position, category, description, deadline_text, cells jsonb, performed_on date, performed_by, performed_at, not_applicable bool, na_reason, remarks, removed bool, origin [template|added])
  • checklist_event(id, org_id, run_id, run_item_id null, actor_member_id, occurred_at, kind, old_value jsonb, new_value jsonb, reason, source), insert-only; the app role has no delete, and update is allowed only through the save routine’s F22a merge of a person’s own recent text edit. Index on (run_id, occurred_at desc) and (run_item_id, occurred_at desc).

Constraints: not_applicable requires na_reason; performed_on and not_applicable are mutually exclusive; a locked run rejects writes to its tables (trigger, as the claim-line draft guard does); the built-in column keys are no, category, description, deadline, performed. Customization never needs a migration: new columns and sections are rows, custom values live in cells.

Under /checklists. Reads need checklist.view; writes as listed in section 2.

Method and path Purpose
GET /checklists/templates, POST, GET/PATCH /checklists/templates/{id} list, create (or copy), read, edit
GET/POST /checklists/runs, GET /checklists/runs/{id} list, start, read a run with columns, sections, rows
POST /checklists/runs/{id}/lock, /unlock lock; unlock with reason
POST /checklists/runs/{id}/commits batch of cell commits from the autosave queue; each has its own version and result
PATCH /checklists/runs/{id}/items/{itemId} performed date, N/A, remarks, cell values; body carries reason when required
POST /checklists/runs/{id}/items, DELETE .../items/{itemId}, POST .../items/{itemId}/move add, remove, reorder rows
POST/PATCH/DELETE /checklists/runs/{id}/sections[/{sectionId}], .../move sections
POST/PATCH/DELETE /checklists/runs/{id}/columns[/{columnId}], .../move columns
POST /checklists/runs/{id}/save-to-template F15
GET /checklists/runs/{id}/events, GET .../items/{itemId}/events history, keyset-paged
POST /checklists/runs/{id}/import/preview, /import F24
GET /checklists/runs/{id}/export F10

Refusals use the existing field and rule 422 shape, for example performed_on / future, reason / required, run / locked, section / not_empty, column / built_in. Concurrency: item and layout writes carry the row’s version; a stale write returns 409 so two people editing the same cell do not silently overwrite each other.

  • Route /checklist, listed under the Workspace section of the sidebar, with the run list and the run page. Strings in en, zh and ms.
  • Run page: header (company, period, status, progress), toolbar (search, filters, “Edit layout” for managers, Export, Activity), the table, and a section-level “+ Add row”.
  • The table is a keyboard-navigable grid; inline cell editors by column type; sticky header; column widths saved per user in browser storage.
  • Edit layout mode shows column menus (rename, type, hide, delete), section menus, row drag handles and “+” insert controls; staff without checklist.manage never receive these controls.
  • Reuses: the existing date input, the RadioCards-style simple pickers, MoneyText is not needed.
  • A run of up to 1,000 rows loads in one request and renders without pagination; history is paged.
  • All amounts in the system stay decimal strings; this module stores no money, so no float risk.
  • Every endpoint is covered by row-level security; cross-company access is refused by the same org policy.

Go (platform + HTTP) and web (Vitest + MSW), at least:

  1. Starting a run copies sections, rows and columns; a later template edit does not change it.
  2. Setting Performed Date writes an event with actor, time and old/new value in the same transaction; a forced failure writes neither.
  3. A future date, a missing reason (N/A, back-dated, unlock) and a locked run each refuse with the stated rule.
  4. Renaming a built-in column changes the label and not the key; deleting a built-in column is refused; deleting a custom column keeps its values.
  5. Deleting a non-empty section is refused; moving rows then deleting succeeds.
  6. Inserting and deleting rows renumbers without duplicates.
  7. Two edits of the same cell with the same version: the second gets 409.
  8. Import of the real sheet produces 19 sections and 85 rows, reports the duplicate number 58, and renumbers.
  9. Export then import round-trips sections, rows and Performed Dates.
  10. Event rows cannot be updated or deleted by the application role, except the F22a merge performed by the save routine itself.
  11. Autosave: typing in a cell sends nothing until Enter, Tab or blur; a failed request keeps the text and shows Retry; a 409 keeps the user’s text and shows who changed it; nothing is sent until Enter, Tab or blur; Esc discards the draft; twenty commits within 500 ms leave in one batch request with twenty events; a failed item in a batch does not undo the others; an edit with a required reason does not save until the reason is given.
  12. Web: staff see no layout controls; manager “Edit layout” shows them; Tab/Enter move as specified; pasting three rows shows a preview then inserts three rows.
  • Custom columns: one set per company or for the whole group?
  • Is Performed Date alone the sign-off, or is a second person’s review required (phase 3 adds a Reviewed column either way, and it can already be added now as a custom “person” column)?
  • Monthly close: the same list as year-end, or a separate template?
  • Should a future-dated Performed Date really be refused (F6), or allowed for planned dates?
  • Phase 2 (see the plan): which of the 85 default rows ship with a rule, and whether the header counts show verified versus manual separately (recommended: yes).

10. Phase 2: automatic tracking (summary; the plan holds the detail)

Section titled “10. Phase 2: automatic tracking (summary; the plan holds the detail)”

Status: first tranche built on feat/checklist-auto-tracking; section 15 of the plan lists what exists and what is deferred.

  • A row (columns are deferred) may carry an optional track rule chosen in the same dialog that sets it up: Manual (default) or Track automatically (source, conditions, done-when, link page). Manual rows behave exactly as in phase 1 and are badged manual; tracked rows are badged verified by system.
  • The description is a free label; the rule, not the wording, decides tracking.
  • Evaluation is pull-first (a read-only check per rule hash), with eventbus events as an accelerator, its own job queue, per-org fairness, shared results and one batched audit entry per run per pass. Targets, hot spots, tests and rollout are in sections 13 and 14 of the plan.
  • Acceptance for phase 2 is the eight tests in plan section 14.5 plus: a tracked row cannot be ticked early without a manager override and reason; a reversed source shows no longer satisfied; turning tracking off leaves every row manual with its history intact.