How monthly forecast figures become quarterly and annual statements, how report rows are calculated, how business plan drafts are filled, and how plan and actual figures are compared. This page gives the formulas, what each symbol means, when they hold and a worked example.
How to read this page. Each formula is shown on its own line, followed by its symbols, the conditions under which it holds, what it says in plain words and a short example. Implementation details for developers close the page.
Cadence aggregation
Monthly figures are grouped into monthly, quarterly (fiscal quarters, key FY-Qn) or annual (fiscal years)
periods using fiscalYearStartMonth. How a line is combined depends on what kind of figure it is (its catalogue
kind in calc/lines.py):
| Symbol | Meaning |
|---|---|
| Q | A period: a fiscal quarter or year |
| flow | Something that happens during a month, such as revenue or costs |
| opening | A balance at the start of a month, such as opening cash |
| stock | A balance at the end of a month, such as ending cash or customers |
| ratio | A ratio of two other lines, such as gross margin |
In plain words. Revenue of a quarter is the revenue of its three months added up; cash at the end of a quarter is the cash at the end of its last month. A ratio is never averaged: it is recalculated from the quarter's totals.
Valid when. Always. Months without a value are skipped in sums. info lines take the last month. Ratios are
rounded to 6 decimals and are null when the denominator is 0. Aggregation works on the rounded monthly series stored
in the run.
Example. Revenue of 10,000, 12,000 and 14,000 with gross profit of 7,000, 9,000 and 11,000 gives quarterly revenue of 36,000 and a quarterly gross margin of 27{,}000 / 36{,}000 = 75\,\%, not the average of the three monthly margins (74.5 %).
Statement and KPI templates
Starter templates live in packages/contracts/templates/starter/*.json and are installed as system templates (tenant_id null) by ReportService.installStarterTemplates. A tenant's own template with the same key takes precedence (ReportService.resolve).
| templateKey | kind | lang | default cadence | validations |
|---|---|---|---|---|
management-pnl |
statement | en | monthly | has_revenue, negative_cash, required_inputs_present |
cash-flow-runway |
statement | en | monthly | has_revenue, negative_cash |
balance-sheet-simplified |
statement | en | quarterly | balance_check |
de-operational-planning |
statement | de | annual | has_revenue, negative_cash |
startup-investor-financial-plan |
statement | en | annual (quarterly/annual only) | has_revenue, negative_cash |
kpi-controlling-dashboard |
kpi | en | monthly | has_revenue, negative_cash |
saas-operating-model |
kpi | en | monthly | has_revenue, negative_cash |
investor-business-plan-en / -es |
business_plan | en / es | annual only | none |
startup-business-plan-universal |
business_plan | en | annual only | profile_fields |
Rendering (render.render_report):
- Cadence must be in the template's
cadences, else error. - Each row is a header/text row (no values), a
refto a canonical line key, or aformulaover earlier row ids or line keys: \operatorname{sum}(a, b, \dots) = a + b + \dots, \operatorname{sub}(a, b, \dots) = a - b - \dots (the first minus the rest), \operatorname{neg}(a) = -a and \operatorname{ratio}(a, b) = a / b (6 decimals, null when b is 0 or null). Missing operands count as 0. Referencing a row defined later raises an error; an unknownrefyields nulls and a warning. sign: -1negates displayed values;hideIfZerodrops rows that are all 0/null.- Validations:
balance_checkwarns if anyabs(bs.check) > 0.05;negative_cashif anycash.ending < 0;has_revenueif no month has revenue > 0;required_inputs_presentif listed line keys are absent.profile_fieldsis accepted by the schema but not evaluated yet; missing profile facts surface as placeholders instead (below). Forecast warnings are appended. - Output includes lineage: run id, scenario, model version, engine version, input hash, calculated-at.
- The template's
currencyonly changes the displayed currency code; no FX conversion is applied.
The control plane records each JSON render or export in report_runs (template version, forecast run, cadence, format).
Business-plan drafts
business_plan.render_business_plan fills section body Markdown with a small token language (no code execution):
{{ path }}or{{ path | filter }}; filters:currency,currency_k,percent,months,number,number1,month,date,text. Currency is formatted with 0 decimals;de/esswap separators.{{ table:name }}forannual_pnl,pricing,hiring,funding,costs(first fiscal year, ranked),unit_economics,scenario_assumptions,key_assumptions.- Paths resolve against:
company(primary legal entity),profile(business_profiles.data),scenario,run,summaryandunit(snake_case field names of the run summary),products,plans,hires,funding(upcoming non-cancelled included events from the as-of month),year1..year5,lastYear. - Missing value or empty table: a visible placeholder
**[PLACEHOLDER: <name> — not provided yet]**(localised) is written and listed inplaceholders. Exceptions: a nullsummary.cash_out_month*renders "beyond the N-month horizon" and a nullsummary.break_even_monthrenders "not within the forecast horizon". - Every figure from
summary,unit,funding,hires,year*,lastYearand every table is listed ingroundingwith sourceforecast_run:<runId or input hash prefix>.
The control plane stores the draft in business_plan_drafts with generated_markdown (engine output) and markdown (user-editable; edited = they differ). Only drafts can be edited; approval requires the approve permission and writes an approvals row.
Plan-vs-actual variance
ControllingService.variance chooses the basis and calls engine POST /v1/variance (compare.variance).
Basis (ControllingService.basis):
| Input | Default |
|---|---|
| Plan run | explicit id, else the plan-baseline run, else the latest run of the primary scenario |
| Forecast run | explicit id, else the latest run of the primary scenario |
| Through month | explicit, else the latest month with an actual record, else the previous calendar month |
Actuals (RecordService.actuals):
finance_recordswithclassification = 'actual', not archived, status notvoid/draft, in base currency (amount_base).- Line mapping (
actualLine): invoice torevenue; expense categorypayrolltoopex.headcount; COGS categories tocogs.<category>; everything else toopex.<category>. - Invoices with a multi-month service period are spread evenly over its months (2 dp, remainder in the last month); everything else lands in the month of
record_date. - Payments (not archived) on the same set of records give
cash.collections(incoming) andcash.outflows(outgoing), converted with the record'sfx_rate_to_base.
Calculation (compare.variance):
- Only plan periods up to the through month are used.
cogsandopexactuals are the sum of their detail lines;gross_profit = revenue - cogs,operating_result = gross_profit - opex. - A month without any actual record has no actuals (null, not zero), and a note lists those months. A month with records but none for a cost line counts that line as 0.
- Default rows:
revenue,cogs,gross_profit,opex,operating_result, any detail cost lines present in actuals, and the cash lines if present. - There are no materiality thresholds; every non-zero variance is flagged favorable or unfavorable.
Variance per cell
| Symbol | Meaning | Unit |
|---|---|---|
| A | Actual amount for the line and month | currency |
| B | The base it is compared with: the plan (or the forecast) | currency |
| \Delta | Variance: how far the actual is from the base (variance) |
currency |
| \Delta\% | Variance as a share of the base (variance_pct) |
share |
In plain words. The variance is actual minus plan. Above plan is good for revenue and profit and bad for costs,
so the favorable flag (favorableVsPlan) looks at the kind of line. Dividing by the absolute value of the plan
keeps the sign of the percentage the same as the sign of the variance, even when the plan is a loss.
Valid when. Both values exist. The percentage is null when the plan is 0 or missing (there is no meaningful
percentage of nothing); the flag is null when the variance is 0 or missing. Cost lines are cogs*, opex* and
cash.outflows. Amounts are rounded to 2 decimals, percentages to 6. The same is calculated against the forecast.
Example. Planned revenue €50,000, actual €45,000: \Delta = -5{,}000 and \Delta\% = -10\,\%, unfavorable. Planned operating result −€20,000, actual −€15,000: \Delta = +5{,}000 and \Delta\% = 5{,}000 / 20{,}000 = +25\,\%, favorable. Planned marketing €8,000, actual €9,000: \Delta = +1{,}000, unfavorable because it is a cost.
Drill-down (ControllingService.drilldown) returns the matching records, plan values from the plan run, related input entities and overrides from the run's input snapshot, and the run's lineage. Records cannot be created or changed in a locked fiscal period (RecordService.assertPeriodOpen).
For developers
Sources: apps/finance-engine/src/finance_engine/calc/aggregate.py, reports/render.py, reports/business_plan.py, models/templates.py, calc/compare.py (variance), and in the control plane reports/Reports.kt, controlling/Controlling.kt, records/Records.kt (actuals, actualLine). Keep this page in sync with any change there.