Skip to content

FS-04 · Generate statements

SOP: SOP_Financial_Statements.md §6 / Step 4.0 (TB_MAPPED → STATEMENTS_GENERATED)Actors: FS Preparer (fs-preparer@spade.local) — holds GENERATE_STATEMENTS. Pre-state: Engagement at TB_MAPPED. Every TB row has a taxonomyKey. Post-state: FsGeneratedStatement row inserted with versionNo incremented. Engagement at STATEMENTS_GENERATED.

0. Prerequisites

  • Engagement at TB_MAPPED (see FS-03).
  • The generate-statements worker 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):

  1. Reads every FsTrialBalanceItem and the SFRS taxonomy.
  2. 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).
  3. P&L — Revenue (sign-flipped from credit) − COGS = Gross Profit; + Other Income − Selling/Admin/Finance/Other = PBT; − Tax = PAT.
  4. SOCE — opening equity + PAT + share movements = closing. Zero-movement columns auto-omitted.
  5. 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.
  6. Anomaly detection — populates anomalies[]:
    • unbalanced
    • negative_working_capital
    • zero_revenue
    • profit_no_tax
    • sign_flip (per affected row)
  7. Inserts a new FsGeneratedStatement row with versionNo bumped from the previous max.
  8. 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.0

Audit log

Event typeNotes
fs.statements.generatedPayload 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 statementsis_balanced = false; a red banner appears on the SOFP tab; the engagement still transitions to STATEMENTS_GENERATED so the preparer can inspect the figures. Subsequent export will refuse without an is_balanced = true regeneration.
  • 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 bumps versionNo. 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.

Internal use only — BreezyCorp