Finance Platform · Learn

Learn

Reference

Reports, business plans and variance

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 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):

\begin{gathered} \text{flow: } X_Q = \sum_{t \in Q} X_t \\[8pt] \text{opening: } X_Q = X_{\text{first}(Q)} \\[8pt] \text{stock: } X_Q = X_{\text{last}(Q)} \\[8pt] \text{ratio: } X_Q = \frac{\sum_{t \in Q} \text{num}_t}{\sum_{t \in Q} \text{den}_t} \end{gathered}
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):

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):

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):

Calculation (compare.variance):

Variance per cell

\begin{gathered} \Delta = A - B \\[8pt] \Delta\% = \frac{\Delta}{\lvert B \rvert} \\[8pt] \text{favorable} = \begin{cases} \Delta > 0 & \text{revenue and profit lines} \\ \Delta < 0 & \text{cost lines} \end{cases} \end{gathered}
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.

Markdown · version bbfeb83dd425