FS-04 · Generate statements
SOP:
SOP_Financial_Statements.md§6 / Step 4.0 (TB_MAPPED → STATEMENTS_GENERATED)Actors: FS Preparer (fs-preparer@spade.local) — holdsGENERATE_STATEMENTS. Pre-state: Engagement atTB_MAPPED. Every TB row has ataxonomyKey. Post-state:FsGeneratedStatementrow inserted withversionNoincremented. Engagement atSTATEMENTS_GENERATED.
0. Prerequisites
- Engagement at
TB_MAPPED(see FS-03). - The
generate-statementsworker is registered.
1. Steps
1.1 Trigger generation
http
POST /ops/financial-statements/engagements/<engagementId>/statements/generate
Authorization: Bearer <fs-preparer-jwt>(Or click Generate on the Statements tab.)
1.2 Worker — generate-statements
The handler is a pure deterministic computation (no LLM):
- Reads every
FsTrialBalanceItemand the SFRS taxonomy. - Balance Sheet — sums by section:
- Non-current assets → Current assets → Total assets
- Equity → Non-current liabilities → Current liabilities → Total equity & liabilities
- Balance check
total_assets ≈ total_equity_and_liabilities(±1.0 SGD).
- P&L — Revenue (sign-flipped from credit) − COGS = Gross Profit; + Other Income − Selling/Admin/Finance/Other = PBT; − Tax = PAT.
- SOCE — opening equity + PAT + share movements = closing. Zero-movement columns auto-omitted.
- Cash Flow (indirect) — Operating = PBT + D&A + working capital changes − tax paid; Investing = PPE/ROU/intangibles/investment movements; Financing = share / loan / lease movements. Reconciles to opening + closing cash.
- Anomaly detection — populates
anomalies[]:unbalancednegative_working_capitalzero_revenueprofit_no_taxsign_flip(per affected row)
- Inserts a new
FsGeneratedStatementrow withversionNobumped from the previous max. - Transitions engagement
TB_MAPPED → STATEMENTS_GENERATED.
2. Verification
Database
sql
SELECT version_no, is_balanced, jsonb_array_length(anomalies) AS anomaly_count, generated_at
FROM fs_generated_statements
WHERE engagement_id = '<engagementId>'
ORDER BY version_no DESC LIMIT 1;
-- expect is_balanced = true (or false with anomalies)sql
SELECT statements_json->'balance_sheet'->'total_assets' AS total_assets,
statements_json->'balance_sheet'->'total_equity_and_liabilities' AS tel
FROM fs_generated_statements
WHERE engagement_id = '<engagementId>'
ORDER BY version_no DESC LIMIT 1;
-- inspect: total_assets and tel should match within ±1.0Audit log
| Event type | Notes |
|---|---|
fs.statements.generated | Payload includes versionNo, isBalanced, anomalies[] |
UI
The Statements tab shows four panes (BS / P&L / SOCE / Cash Flow). Anomalies surface as inline banners. The Export Word button is now available — but to FS_REVIEWER / SENIOR_ACCOUNTANT / PLATFORM_ADMIN only.
3. Negative & edge cases
- Unbalanced statements —
is_balanced = false; a red banner appears on the SOFP tab; the engagement still transitions toSTATEMENTS_GENERATEDso the preparer can inspect the figures. Subsequent export will refuse without anis_balanced = trueregeneration. - Sign flip — anomaly row carries
{kind: 'sign_flip', message, fields: [...]}. Open the Mapping tab; the offending rows are highlighted; correct and regenerate. - Regenerate — re-running with no upstream change is allowed (it's the self-loop
STATEMENTS_GENERATED → STATEMENTS_GENERATED) and bumpsversionNo. The latest version is what downstream notes / DOCX read. - Generate without
TB_MAPPED— handler refuses (400); the engagement state guards against generation on an unmapped TB.
Next
Proceed to FS-05 · Disclosure interrogation.