StabisoDocs

Accounting and finance

Plans: budgets and rolling forecasts

A plan holds planned profit and loss figures of one entity, beside the ledger: nothing in a plan is ever posted, and no report other than the comparison reads it. A budget covers one fiscal year; a forecast looks a number of periods ahead (3 to 24, 12 by default). Both are made of versions, and a person approves the version that becomes the plan's current one. The design is ADR-0034 (part A); this guide is slices F1 (budgets) and F2 (rolling forecasts) as built. The controls are in the control matrix (section "Plans", PL1 to PL11).

Everything is a REST operation first (/v1, tag "plans" in the generated reference); the MCP tools list_plans, get_plan_version and plan_comparison read the same operations.

Plans and versions

POST /v1/plans                               {entity, name, kind: budget|forecast, fiscal_year | horizon}
POST /v1/plans/{plan_id}/versions            {copy_from?, note?}       a new draft
PUT  /v1/plan-versions/{version_id}/lines    {lines: [...]}            replaces every line of a draft
POST /v1/plan-versions/{version_id}/submit                             fixes the lines (lines_digest)
POST /v1/plan-versions/{version_id}/send-back {reason}                 back to draft
POST /v1/plan-versions/{version_id}/approve  ?lines_digest=            the plan's current version
POST /v1/plans/{plan_id}/roll                {method?, periods?, growth_pct?, note?, refresh?}  a forecast's next draft
GET  /v1/plan-versions/{version_id}/comparison ?by=account|report_line&against_version=

A version moves draft to submitted to approved to superseded; a submitted one can be sent back to draft. Creating a plan creates version 1, empty. A new version is a copy of any version of the plan (copy_from), which is how an approved budget is revised: the copy is a draft, the original stays as it was. Approving a version supersedes the one that was current; at most one version of a plan is approved, which a unique index in the database guarantees, and approvals are serialised on the plan's row.

Who What
Anyone with draft (an agent too) create a plan, start a version, set its lines, submit it, send it back, rename the plan
A person with approve approve a submitted version (policy action approve_plan; agents are always denied, rule agents_no_plan_or_close_controls)

By default the person who approves is not one who prepared the version: not its creator, not anyone who set its lines (the audit trail keeps each plan_version.lines) and not its submitter (rule plan_submitter_does_not_approve, compared by person: another key or session is still the same person, and an agent run counts as the person it ran for). A one-person organization allows approve_own_plan in its approval policy. A key the system issued (the organization's first key) belongs to no person and approves nothing. A version older than the plan's current one is never approved over it (409): send it back, or copy it into a new version.

Lines

One amount per account and period of a version:

  • the account is a revenue or expense account of the plan's entity (a balance sheet account is refused, 422, and the database refuses it too);
  • the period is a regular period named YYYY-NN as the close checklist names it (2027-03), of the budget's fiscal year for a budget; adjustment periods are one day long and hold closing adjustments, so nothing is planned in them;
  • the amount is in the entity's functional currency, in the account's natural direction: revenue as a positive credit, expenses as a positive debit, exactly as the profit and loss shows them. A negative amount is a reduction (planned discounts on 3800). More decimals than the currency has are refused.

PUT .../lines replaces every line of a draft at once (at most 20,000), so repeating the request sets the same lines. Only a draft's lines change: once a version is submitted, the database refuses any insert, update or delete of its lines (trigger erp_check_plan_line), whatever the application does.

method is manual for a line entered: a line with another amount than the draft had, or a new one. A line sent with the amount it already had keeps its method, so a draft a person corrected in one place still says which figures the system worked out. The roll writes actual, run_rate, prior_year and schedule (below).

Submitting and approving

Submitting digests the version's note and lines (lines_digest, SHA-256). Approving recomputes the digest and refuses (409 version_changed) when the lines are not the submitted ones, and, when the approver passes the lines_digest they looked at, when that differs: an approval is always of exactly the figures that were reviewed.

Rolling a forecast

POST /v1/plans/{plan_id}/roll makes a forecast's next version, a draft, from its current version (else its latest one):

  1. Actuals. Every closed period of the fiscal year the forecast is in (the year of the first open period), and any closed period of the horizon, becomes lines with method actual; periods need not close in order, so a closed period after one that was reopened is an actual too: the posted profit and loss of the period, read as the comparison reads it (by period, closing entries left out, adjustment periods in the year's last regular period). They are stored, so the version stays a snapshot of what was known when it was made.

  2. Carried over. The open periods the version rolled from planned keep their lines as they were (amount, method, note). With refresh, only the lines people entered (manual) are carried over; the rest of those periods is worked out again as a new period is.

  3. New periods. The horizon is the plan's horizon regular periods from the first open one. A period of it the version rolled from did not plan is filled, per account, by the method of the account's latest line where that is run_rate or prior_year, else by the request's method:

    • run_rate: the average of the actuals of the last periods closed periods (default 3);
    • prior_year: the same period a year earlier, its actuals once it is closed, else the figure this version forecasts for it, times 1 + growth_pct / 100.

    The accounts filled are those the version rolled from planned and those with actuals in the periods read. An amount of zero is no line.

    Never below what is scheduled (schedule): what the system already knows of the period, where it is more than the projection, replaces it: the net revenue of the invoices recurring invoices and subscriptions will make (priced as their run would make them), the revenue and expense lines of the recurring journals that will post (accruals reversed in the next period left out), and the depreciation of the assets in service by their schedules. What is due already and not made yet counts in the first open period. A run rate of 500 with a new rent of 900 scheduled forecasts 900.

    A closed period inside the horizon is an actual, not a forecast, and still counts toward the horizon's length, so the forecast covers one period fewer for each such period.

Periods of the horizon that have no fiscal period yet are not invented: the answer says how many are missing (horizon_short_by); open the next fiscal year and roll again. Whether a period is closed is its own status, never the clock. Every period of the entity closed is 422; a budget does not roll (409). The roll never posts and is drafting (anyone with draft, an agent too): the draft is reviewed, submitted and approved by a person like any version, and the version note says what was rolled (plan_version.roll in the audit trail has the details).

Every company's rolling forecast. Each company has a forecast named "Rolling forecast" (12 periods) that the system makes and rolls with refresh after each close, whatever the close calendar says: see Rolling forecast.

After each close, by the close job. An entity whose close calendar names the step forecast_roll gets this roll, with the defaults, from the close job the morning after a period is closed, for each forecast with no version made since the close.

The comparison

GET /v1/plan-versions/{version_id}/comparison compares any version (a draft too) with what was posted:

  • periods: every regular period of a budget's year; for a forecast, the regular periods from its first line to its last;
  • actuals: the posted entries of each period, read by period (so a 4-4-5 period is what it is), for revenue and expense accounts, with the year-end closing entries (source close) left out as every profit and loss report leaves them out, and the entries of a year's adjustment periods counted in its last regular period;
  • per account (by=account) or per report line (by=report_line), and each period: plan, actual, variance (actual less plan), variance_pct (of the plan, one decimal, empty when the plan is zero) and favourable (more revenue or fewer expenses);
  • revenue first, then expenses; the rows revenue, expenses and net add them up;
  • every row ends with the outlook of each fiscal year of its periods: actual (its closed periods), plan_rest (the plan of the others), outlook (their sum: where the year lands) and plan (the plan of all its periods, for reference).

Against another version (against_version: a budget's current version, current_version_id of GET /v1/plans/{plan_id}, or an earlier forecast of the same entity), each cell also carries that version's amount (against), and the variance is this version's amount less that one's, its percent of that one's: a rolled forecast against the budget. The actuals stay in each cell. A version is not compared with itself, nor with another entity's (422).

An open period's actuals are not final (periods[].status). Nothing is stored: the comparison is computed from the ledger and the version each time it is asked.

What this does not do yet

Intercompany loan interest in the schedule method and an agent's draft with a commentary are the rest of slice F3. CSV and Excel import of lines, the plan series in analytics and the screens are slice F4; dimensions on plan lines wait for ADR-0024.

Stabiso, the Enterprise System of Record · generated from the code and its guides · release 20b7dd5b0c4d