Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Was this page helpful?