Schedule III Financial Statement Templates
How a statutory Balance Sheet / Profit and Loss layout is expressed as pure configuration on top of an existing statement engine
1. Requirements
1.1 Functional requirements
- Ship a ready-to-use Balance Sheet and Profit and Loss layout matching a jurisdiction’s statutory presentation format (Schedule III), so an adopter does not author either from scratch.
- Both layouts are pure data — an ordered list of typed rows — consumed by a pre-existing, statement-agnostic rendering engine (§ 3.2), not bespoke computation code.
- A row pulls its number from ledger accounts matching a filter, by combining other rows with a formula, or as pure layout with no number at all.
- Seed statutory categories a base chart of accounts is unlikely to already have (e.g. Capital Work in Progress) into the shared category master on first install, without duplicating one that already exists.
- Each statement should be able to cross-check itself against an independently computed total.
1.2 Non-functional requirements
- No code changes for a new statutory layout — a line-item change is a configuration edit, never a rendering-engine change.
- Idempotent seeding — re-installing must not create duplicate category records; seeding is skip-if-exists by name.
- Fail loud on structural mistakes, fail quiet on data mistakes — a malformed formula is rejected at save time; a filter matching no accounts is not.
1.3 Constraints
- The two statutory layouts are ordinary records of a generic template construct any module can define — the rendering engine itself is not jurisdiction-specific.
- The line-to-account mapping is a chart-of-accounts concern: an accountant tags each relevant ledger account with a category value; the template never lists specific accounts.
- Structural validation runs on every ordinary save, but is bypassed the one time a shipped template is first installed as a fixture.
2. High-Level Design
2.1 Component diagram
2.2 Data flow — from shipped fixture to usable template
- Trigger. A legal entity’s chart of accounts is provisioned (typically once, at company setup); every installed module is scanned for a conventionally-named template subfolder.
- Seed categories first, skip-if-exists. Each entry in a found seed file is inserted into the shared category master unless a category of that exact name already exists.
- Load each template, insert-only. A template loads only if none of that name already exists — inserted directly, with structural validation switched off for this one insert.
- The account-to-category link comes later, entirely by manual chart-of-accounts maintenance — nothing here touches actual accounts.
3. Deep Dive
3.1 Data model
Statement Template (reused from the period-closing design) — a named, typed, ordered list of Template Rows. The two statutory layouts here are two such records: one typed Profit and Loss, one Balance Sheet.
Template Row:
| Field | Role |
|---|---|
reference_code |
unique id other rows’ formulas point to (e.g. REV_OPERATIONS) |
display_name / indentation_level |
printed label; 0=heading/total, 1=sub-category — purely visual, decoupled from the formula graph |
data_source |
Account Data / Calculated Amount / Custom API / Blank Line / Column Break / Section Break |
balance_type |
Opening / Closing / Period Movement — meaningful only for Account Data |
calculation_formula |
an account filter (Account Data) or an arithmetic expression over other rows’ codes (Calculated Amount) |
reverse_sign, bold_text, hidden_calculation, hide_when_empty, include_in_charts |
presentation and internal-only-calculation toggles |
An account-data row and the calculated total it feeds, illustrating the two live sources:
{ reference_code: "REV_OPERATIONS", data_source: "Account Data",
balance_type: "Period Movement (Debits - Credits)",
calculation_formula: ["account_category", "=", "Revenue from Operations"] }
{ reference_code: "TOTAL_REVENUE", data_source: "Calculated Amount",
calculation_formula: "REV_OPERATIONS + REV_OTHER" }
A filter can also be a boolean tree (e.g. “Operating Expenses” and a name matching a pay-related word). Nesting on the page and in the computation are independent — a total’s formula is always written explicitly, never inferred from indentation.
Account Category — a small shared master (name + description) a ledger account can be tagged with; the only mechanism by which most account-data filters resolve to real accounts. Most of the roughly two dozen category values these templates use (Trade Receivables, Cash and Cash Equivalents, Share Capital) are assumed already present on a typical chart of accounts. The Category Seed File supplies only the handful a base chart is unlikely to have — Capital Work in Progress, Deferred Tax Assets/Liabilities/Expense, Intangible Assets Under Development, Long-term Loans and Advances, Money Received Against Share Warrants, Share Application Money Pending Allotment. Renaming a category cascades into every referencing row; a few rows skip categories entirely, filtering on account type directly (depreciation matches “Depreciation”).
3.2 Self-checks, validation, and the renderer boundary
Both templates carry a row, marked compute-but-not-print, that exists purely to catch a mis-tagged or untagged account: the P&L template recomputes profit independently from root type (Income minus Expense) and prints the difference against its category-driven build-up as a hidden-unless-nonzero variance row; the Balance Sheet does the equivalent with a “Balance Check (should be zero)” comparing total assets against total equity-and-liabilities. Neither check is system-enforced — visible only if someone looks at the rendered statement, and a property of these templates’ own authoring, not something every template gets automatically.
A dedicated Template Validator runs whenever a template is edited and saved through the ordinary path (skipped the one time a shipped template is first installed). It checks reference-code uniqueness, that every formula-referenced code exists with no circular dependency, that formulas evaluate to a number, and that an account filter’s field/operator are well-formed. It does not check that a referenced category name actually exists or is assigned to any live account — the gap the self-check rows above partially, and only optionally, mitigate.
The construct that actually executes a template — resolving filters into accounts, fetching balances, evaluating rows in dependency order — is a generic engine outside this pair of templates entirely, already documented as the Statement Engine in the period-closing design. This document stops at that boundary and does not re-derive its internals.
3.3 Error handling
- Structural mistake (cycle, undefined reference, bad filter shape) — rejected outright at save time; bypassed by design for the one-time shipped-template install, which is also skipped entirely if the name already exists.
- Unmapped or mis-tagged account — no error; the line renders empty or zero, detectable only via a self-check row, if reviewed.
- Category seeding conflict — an existing name is left untouched; the seed file’s description is discarded silently.
4. Scale and Reliability
- A one-time, setup-time cost, not a runtime one — seeding and template loading happen once per chart-of-accounts setup; rendering cost belongs to the Statement Engine and does not grow with templates installed.
- Growth axis is module count, not ledger size — the sync step scans every installed module’s template folder per provisioning run and never touches transactional data.
- Silent drift is the real risk: a chart-of-accounts restructuring degrades statement accuracy with no error raised, caught only if someone notices a nonzero self-check row.
5. Trade-off Analysis
| Decision | Trade-off |
|---|---|
| Row configuration on a generic engine, not bespoke code per layout | A line-item change is a data edit, not a deploy — but anything the schema can’t express has no home here. |
| Mapping via an account-side category tag, not an account list in the template | Templates stay portable across chart structures — but an untagged account fails silently, with no forced error. |
| Structural validation enforced, but never against live chart-of-accounts data | Catches internal authoring mistakes — but no protection against a category that exists on paper but was never assigned. |
| Template-authored self-check rows instead of an engine-enforced reconciliation | No engine change needed for one template’s safety net — but it’s opt-in; a template that omits it gets none. |
6. What to Revisit as the System Grows
- Validate filter values against the live category master, and ideally whether any account carries them — today’s validator confirms shape, never substance.
- Promote the self-check pattern to an engine-level feature rather than a per-template convention, so a new custom template doesn’t silently lose it.
- Surface a nonzero self-check row as an actionable signal — nothing today distinguishes a genuine exceptional item from a tagging omission.