Budgets & variance reporting¶
Backend domain: app/graphql/budgets/
Migration: add_budgets
RBAC Path: BUDGETS
Overview¶
A Budget is a named plan against a FiscalYear (optionally scoped to a project or office), containing one BudgetLine per (account, period_number) cell — i.e. up to 12 cells per account per year. Statuses: DRAFT → APPROVED → ARCHIVED.
Variance reporting (budgetVariance) joins the budget lines to the period calendar and to posted ledger lines, returning budget, actual, variance, and variancePct per account for any date range.
Setup prerequisites¶
- A
FiscalYearmust exist (see period-close.md) — the budget references one. - The chart of accounts the budget references must exist.
GraphQL surface¶
Queries¶
budgets: [Budget!]!
budget(budgetId: UUID!): Budget!
budgetVariance(
budgetId: UUID!,
startDate: Date!,
endDate: Date!
): [BudgetVarianceRow!]!
Budget fields: id, name, fiscalYearId, status (DRAFT | APPROVED | ARCHIVED), approvedAt, approvedById, projectId, officeId, plus nested lines: [BudgetLine!]!.
BudgetLine: id, budgetId, accountId, periodNumber (1-12), amount.
BudgetVarianceRow: accountId, budget, actual, variance (= actual − budget), variancePct.
Mutations¶
createBudget(budget: BudgetInput!): Budget!
approveBudget(budgetId: UUID!): Budget!
archiveBudget(budgetId: UUID!): Budget!
BudgetInput: name, fiscalYearId, lines: [BudgetLineInput!]!, optional projectId, officeId.
BudgetLineInput: accountId, periodNumber, amount.
Behaviour to handle in the UI¶
- The natural editor is a grid: rows = revenue/expense accounts, columns = 12 periods, cells = amount. The bulk
createBudgetmutation accepts the full grid in one call. - Don't constrain to revenue/expense in the UI — the backend accepts any account, but planning revenue/expense is the typical use.
- After creation, an "Approve" CTA locks edits (the backend doesn't block edits today — see follow-up — but the UI should respect the status flag).
- For variance reports, render as a P&L-style table with
budget | actual | variance | variance%columns. Color-code variance (favorable/unfavorable depends on whether the account family is revenue or expense — frontend has to interpret).
Error states¶
None notable. Unique-constraint violation on (budget_id, account_id, period_number) is the only structural rejection; that's prevented by sending one line per cell.
Suggested UX flow¶
- Budgets list — group by fiscal year, show status badge, project/office tags.
- Budget grid editor — paste-from-Excel-friendly grid; columns = period number + label (Jan…Dec).
- Budget variance report — date-range picker → P&L-style variance table. Drill from a row into the underlying ledger entries for that account in that range.
Open follow-ups (not yet implemented)¶
- Backend doesn't actually lock edits on
APPROVEDbudgets; rely on UI for now or request a server-side check. createFromPriorYear(fiscalYearId, growthPct)helper was specced but not built — would let users seed from posted GL × growth. Frontend can simulate by querying the prior year's P&L and applying the multiplier client-side.- No CSV import endpoint; provide a paste-from-spreadsheet UX instead.
variancePctis computed asvariance / budget * 100and is 0 when budget is 0 — surface as "—" rather than "0%" when budget is zero.- The variance query uses absolute net activity (
debits - credits) per account; for revenue accounts the sign will be inverted relative to common P&L expectations. UI should apply the sign flip based on account type (use theaccountsquery to fetchaccountType).