Skip to main content

Excel VBA Guide

The Konsolidat VBA module turns Excel into a live reporting and budgeting client. Six worksheet functions cover reading (EPM, EPM_BUDGET, EPM_VARIANCE, EPM_DEBIT, EPM_CREDIT) and writing (EPMSAVE). A batch refresh mechanism fetches all read values in a single HTTP round-trip. Budget writes happen immediately on recalc.

Setup

  1. Import excel/OpenEPM.bas into your workbook (Alt+F11 → File → Import)
  2. Add VBA references: Microsoft Scripting Runtime, Microsoft XML, v6.0
  3. Save as .xlsm
  4. Run EPM_SetServer to configure the Frappe URL
  5. Run EPM_Login to authenticate

See Setup Guide for full installation steps.

Formula Functions

Read vs Write

FunctionDirectionWhen It Fires
EPM(), EPM_BUDGET(), EPM_VARIANCE(), EPM_DEBIT(), EPM_CREDIT()ReadReturns cached value; refresh with Ctrl+Shift+R
EPMSAVE()WriteSaves to server immediately on recalc (skips if unchanged)

Read Functions

All five read functions share the same parameter pattern. They differ only in the default measure and scenario.

EPM() — General Purpose

=EPM(entity, fiscal_year, fiscal_period, account, [measure], [scenario], [cost_center], [department], [scenario_id])
ParameterTypeRequiredDefaultDescription
entityStringYesLegal entity code (e.g., "USMF")
fiscal_yearNumberYesFiscal year (e.g., 2024)
fiscal_periodString/NumberYesPeriod: 112, "Q1""Q4", "H1", "H2", "FY"
accountStringYesMain account code (e.g., "401100")
measureStringNo"period_net_amount"Which value to return (see Measures)
scenarioStringNo"actuals"Data scenario (see Scenarios)
cost_centerStringNo""Filter by cost center
departmentStringNo""Filter by department
scenario_idStringNo""Filter to a specific scenario ID (e.g., "BUDGET_2025"). See Scenario ID Filtering.

Examples:

=EPM("USMF", 2024, 5, "401100")
=EPM("USMF", 2024, "Q1", "401100", "ytd_net_amount")
=EPM("USMF", 2024, "FY", "401100", "period_net_amount", "actuals", "SALES")
=EPM("USMF", 2025, 5, "6100", "period_amount", "budget", "", "", "BUDGET_2025")

EPM_BUDGET() — Budget Values

=EPM_BUDGET(entity, fiscal_year, fiscal_period, account, [cost_center], [department], [scenario_id])

Shorthand for =EPM(..., "period_amount", "budget", ...).

EPM_VARIANCE() — Actual vs Budget Variance

=EPM_VARIANCE(entity, fiscal_year, fiscal_period, account, [cost_center], [department], [scenario_id])

Shorthand for =EPM(..., "variance_abs", "variance", ...).

EPM_DEBIT() — Period Debits

=EPM_DEBIT(entity, fiscal_year, fiscal_period, account, [cost_center], [department])

Shorthand for =EPM(..., "period_debit", "actuals", ...).

EPM_CREDIT() — Period Credits

=EPM_CREDIT(entity, fiscal_year, fiscal_period, account, [cost_center], [department])

Shorthand for =EPM(..., "period_credit", "actuals", ...).

Write Function

EPMSAVE() — Budget Write-Back

=EPMSAVE(amount, entity, fiscal_year, fiscal_period, account, scenario_id, layer, [cost_center], [department])
ParameterTypeRequiredDefaultDescription
amountNumberYesThe value to save (number or cell reference)
entityStringYesLegal entity code (e.g., "USMF")
fiscal_yearNumberYesFiscal year (e.g., 2025)
fiscal_periodNumberYesSingle period 112 (no ranges — one cell per period)
accountStringYesMain account code (e.g., "6100")
scenario_idStringYesScenario instance (e.g., "BUDGET_2025")
layerStringYesBudget layer: "base", "challenge", "management", "board"
cost_centerStringNo""Cost center dimension
departmentStringNo""Department dimension

The cell displays the amount. The write to the server happens silently in the background.

Examples:

=EPMSAVE(100000, "USMF", 2025, 1, "6100", "BUDGET_2025", "base")
=EPMSAVE(-5000, "USMF", 2025, 1, "6100", "BUDGET_2025", "challenge")
=EPMSAVE(B5, $A$1, $A$2, C$3, $A5, $A$4, "base") ' cell references

How EPMSAVE Works

  1. Formula evaluates → cell displays the amount (pass-through)
  2. VBA checks if the value changed since last save (skip-unchanged cache)
  3. If changed → POST /api/method/konsol.api.budget_cell_save with the parameters
  4. Server upserts a single period+layer row in the Budget Input doc (creates the doc if new)
  5. Budget Input doc stays in Draft until approved via Frappe workflow
sequenceDiagram
participant User
participant Excel as EPMSAVE()
participant Cache as Save Cache
participant Frappe as Frappe API

User->>Excel: Types 95000 in cell
Excel->>Cache: Value changed?
alt Changed
Cache-->>Excel: Yes
Excel->>Frappe: POST budget_cell_save<br/>{entity, year, period, account,<br/>scenario_id, layer, amount}
Frappe-->>Excel: {status: ok}
Excel->>Cache: Update cache
else Unchanged
Cache-->>Excel: No (skip)
end
Excel-->>User: Cell shows 95000

Budget Layers

Layers are additive — the effective budget is always the sum across all layers for a given period. Each layer represents a different stakeholder's contribution:

LayerTypical Use
baseDepartment's original submission
challengeFinance team adjustments (e.g., 5% cut)
managementExecutive overrides (e.g., Q3 launch funding)
boardBoard-level final adjustments

Any authorized user can write to any layer — the layer parameter is data, not security. Access to the budget module is controlled by Frappe's standard role permissions.

See the Budget Layers Guide for a full worked example showing how layers build up across 12 periods.

Building a Budget Template

ABCDE
1Scenario:BUDGET_2025Year:2025
2Layer:base
3AccountP1P2P3...
46100=EPMSAVE(100000,$B$1,$D$1,B$3,$A4,$B$2,"base")=EPMSAVE(100000,$B$1,$D$1,C$3,$A4,$B$2,"base")...
56200=EPMSAVE(50000,$B$1,$D$1,B$3,$A5,$B$2,"base")=EPMSAVE(50000,$B$1,$D$1,C$3,$A5,$B$2,"base")...

Tips:

  • Use absolute references for scenario ($B$1), year ($D$1), and layer ($B$2)
  • Use mixed references for period (B$3, C$3) and account ($A4, $A5) so formulas copy correctly when dragged
  • To change the amount, just edit the first argument — EPMSAVE fires on recalc and saves the new value
  • To enter challenge adjustments, change $B$2 to "challenge" and type your deltas

Read + Write Side by Side

A common pattern: read the current approved budget on one row, write your adjustments on the next:

ABCD
1Scenario:BUDGET_2025Year:2025
2AccountP1P2P3
36100 Approved=EPM_BUDGET("USMF",2025,1,"6100")=EPM_BUDGET(...)=EPM_BUDGET(...)
46100 Challenge=EPMSAVE(-5000,"USMF",2025,1,"6100","BUDGET_2025","challenge")=EPMSAVE(...)=EPMSAVE(...)
56100 Effective=B3+B4=C3+C4=D3+D4
  • Row 3: reads current approved budget (Ctrl+Shift+R to refresh)
  • Row 4: writes your challenge layer adjustments (saves immediately)
  • Row 5: formula shows the effective budget (base + challenge)

What Happens After Save

EPMSAVE creates Budget Input docs in Draft state. To make them live:

  1. Open Frappe Desk → Budget Input list
  2. Review the entries
  3. Click Submit for ReviewApprove
  4. On approval: ClickHouse sync fires → dbt rebuild → EPM() formulas return updated values on next Ctrl+Shift+R

Measures

Each scenario exposes a specific set of measures. Using a measure not allowed for the scenario returns an error.

Actuals Measures

MeasureDescription
period_debitSum of debit amounts for the period
period_creditSum of credit amounts for the period
period_net_amountSum of accounting currency amount (debit − credit)
transaction_countNumber of GL entries
ytd_debitYear-to-date cumulative debit
ytd_creditYear-to-date cumulative credit
ytd_net_amountYear-to-date cumulative net amount

Budget Measures

MeasureDescription
period_amountBudget amount for the specific period (spread from annual)
annual_amountTotal annual budget amount

Variance Measures

MeasureDescription
actual_amountActual amount (from trial balance)
budget_amountBudget amount (from spread budget)
variance_absactual_amount − budget_amount
variance_pctVariance as a percentage of budget
variance_favorable1 if favorable, 0 if unfavorable (revenue: actual > budget; expense: actual < budget)

Scenarios

ScenarioSource TableDescription
actualsgold_trial_balancePosted GL data
budgetgold_spread_budgetBudget amounts spread across periods
variancegold_variance_analysisComputed actual-vs-budget comparison

Period Ranges

Instead of a single month number, you can pass period range codes. The API sums across the constituent months.

CodeMonths Included
112Single month
"Q1"Months 1, 2, 3
"Q2"Months 4, 5, 6
"Q3"Months 7, 8, 9
"Q4"Months 10, 11, 12
"H1"Months 1–6
"H2"Months 7–12
"FY"Months 1–12 (full year)

Example: =EPM("USMF", 2024, "Q1", "401100") returns the sum of periods 1+2+3.

Scenario ID Filtering

The scenario_id parameter lets you target a specific scenario instance (e.g., BUDGET_2025, FORECAST_Q3_2025) within tables that support it. This is useful for:

  • Multiple budget versions: Compare BUDGET_2025 vs BUDGET_2025_V2
  • What-if analysis: Query a what-if scenario alongside the approved budget
  • Forecast vs budget: Compare FORECAST_Q3_2025 with BUDGET_2025

When scenario_id is omitted or empty, the query returns the sum across all scenario IDs — which is the default behavior and matches how EPM() has always worked.

Currently supported tables: gold_spread_budget (scenario = budget)

FormulaWhat It Returns
=EPM_BUDGET("USMF", 2025, 5, "6100")Sum of ALL budget scenarios for P5
=EPM_BUDGET("USMF", 2025, 5, "6100", "", "", "BUDGET_2025")Only BUDGET_2025 for P5
=EPM_BUDGET("USMF", 2025, "Q1", "6100", "", "", "BUDGET_2025")BUDGET_2025 Q1 total

Macros

EPM_Refresh (Ctrl+Shift+R)

Refreshes the active sheet:

  1. Scans all cells for EPM-family formulas
  2. Extracts parameters (resolves cell references like $B$5)
  3. Sends a single batch POST to /api/method/konsol.api.epm_batch
  4. Populates the in-memory cache
  5. Triggers a single Calculate on the EPM range

EPM_RefreshAll

Refreshes all sheets in the workbook. Shows progress on the status bar: "Konsolidat: Sheet 3/12 — Income Statement".

EPM_Login

Prompts for Frappe username and password. Authenticates via POST /api/method/login and stores the session cookie for subsequent API calls. Auto-triggered by refresh if not logged in.

EPM_ClearCache

Clears the in-memory value cache. Use when you want to force a full re-fetch on next refresh.

EPM_SetServer

Prompts for the Frappe API URL and saves it as a Custom Document Property (EPM_API_URL) in the workbook. Persists across sessions.

EPM_ToggleLog

Enables/disables debug logging to a hidden _EPM_Log sheet. Columns: Timestamp, Level, Message.

EPM_Debug

Runs a diagnostic sequence:

  1. Tests cache initialization
  2. Tests HTTP connectivity
  3. Calls health endpoint (konsol.api.health)
  4. Scans active sheet for EPM formulas
  5. Sends a test batch query (USMF / 2024 / period 5 / account 401100)

Building a Report

Basic P&L Report

ABCD
1Entity:USMFYear:2024
2AccountJanFebMar
3Revenue (401100)=EPM($B$1,$D$1,1,$A3)=EPM($B$1,$D$1,2,$A3)=EPM($B$1,$D$1,3,$A3)
4COGS (501100)=EPM($B$1,$D$1,1,$A4)=EPM($B$1,$D$1,2,$A4)=EPM($B$1,$D$1,3,$A4)
5Gross Profit=B3+B4=C3+C4=D3+D4

Tips:

  • Use absolute references ($B$1) for entity/year cells so formulas copy correctly
  • Use relative row references for account codes so you can drag formulas down
  • Period numbers in the column headers can be cell references too

Budget vs Actual Report

ABCD
1Entity:USMFYear:2025
2AccountActualBudgetVariance
3Revenue (401100)=EPM($B$1,$D$1,"FY",$A3)=EPM_BUDGET($B$1,$D$1,"FY",$A3)=EPM_VARIANCE($B$1,$D$1,"FY",$A3)

Multi-Entity Comparison

Use different entity codes in each column:

=EPM("USMF", 2024, "FY", "401100") ' Column B: US entity
=EPM("DEMF", 2024, "FY", "401100") ' Column C: Germany entity
=EPM("GBMF", 2024, "FY", "401100") ' Column D: UK entity

How Refresh Works (Technical)

sequenceDiagram
participant User
participant VBA as VBA Module
participant Cache as Scripting.Dictionary
participant Frappe as Frappe API
participant CH as ClickHouse

User->>VBA: Ctrl+Shift+R
VBA->>VBA: Scan UsedRange for EPM formulas
VBA->>VBA: ResolveEpmArgs() — parse & evaluate cell refs
VBA->>Frappe: POST /api/method/konsol.api.epm_batch<br/>[{entity, year, period, account, ...}, ...]
Frappe->>Frappe: Validate scenarios & measures
Frappe->>Frappe: Group by (scenario, measure, periods, dims)
Frappe->>CH: Parameterized SELECT with SUM + GROUP BY
CH-->>Frappe: TSV results
Frappe-->>VBA: {"values": [1234.56, ...]}
VBA->>Cache: Store key → value
VBA->>VBA: epmRange.Calculate (single recalc)
VBA-->>User: Values appear in cells

The batch mechanism means 500 EPM cells = 1 HTTP request, not 500.

Troubleshooting

SymptomCauseFix
All cells show 0Not refreshedPress Ctrl+Shift+R
#VALUE! errorWrong parameter typesCheck entity is string, year is number
401 / 403 on refreshSession expiredRun EPM_Login again
Slow refreshToo many unique queriesGroup similar periods; use period ranges (Q1, FY)
Values don't updateStale cacheRun EPM_ClearCache, then Ctrl+Shift+R
ClickHouse connection failedClickHouse is downCheck docker ps for healthy container
EPMSAVE not savingNot logged inRun EPM_Login first — EPMSAVE skips if no session
EPMSAVE saving duplicatesValue unchanged but re-savingCheck save cache — run EPM_ToggleLog to verify skip behavior
Budget not visible in EPM_BUDGETNot approved yetBudget Input docs start as Draft — approve in Frappe Desk first

Next Steps