Originally created by: grynn-in
How a budget is entered currently determines where it is stored, and the downstream readers (K.EPM, the consolidated/scenario trial balance, reports) do not agree on a single source. There is no canonical budget fact. As a result, a budget entered one way is invisible to a reader wired to another store.
Concretely observed: a full FY2024 budget loaded via the monthly-direct path (budget_save_batch → Budget Sheet → cycle lock) lands in epm_gold.budget_monthly_input and is completely invisible to K.EPM, because K.EPM's budget_input fact reads epm_gold.gold_spread_budget (the annual+spread path's output). Same scenario name, same grain, different table → returns 0.
This violates the intended UX: users should be able to spread budgets themselves (manual monthly) or via a profile (annual × weights), and the result should land in the same place regardless of who/how it was spread.
| Entry path | Spread by | Lands in | Read by |
|---|---|---|---|
Monthly direct — budget_save_batch / budget_cell_save → Budget Sheet → on_submit sync |
user, by hand | epm_gold.budget_monthly_input (budget_sheet.py:27 CLICKHOUSE_TABLE, sync :87) |
nobody downstream |
Annual + profile (PRD-6) — budget_annual_input → dbt |
spread_profiles weights |
epm_gold.gold_spread_budget (models/gold/gold_spread_budget.sql) |
K.EPM (budget_input fact → clickhouse_table=epm_gold.gold_spread_budget) |
| Write-back / API staging | — | epm_staging.budget_input (DDL only, clickhouse/init-db.sql; ~1 placeholder row, no app writer) |
gold_scenario_trial_balance.sql:47-52 (source('epm_staging','budget_input')) |
| D365 | — | silver_budget_entries |
gold_scenario_trial_balance.sql:33-44 (BUDGET branch) |
So gold_scenario_trial_balance reads stores [#3] (empty) and [#4] (D365) but not [#1] or [#2], and K.EPM reads only [#2]. No path reads budget_monthly_input at all.
Key insight: a manual monthly entry is just a spread whose 12 weights are the numbers the user typed. Both entry modes are the same operation; only the source of the weights differs. Therefore they can share one model and one output table.
gold_spread_budget (or introduce gold_budget) to be the single source of truth, at grain (scenario_id, data_area_id, fiscal_year, fiscal_period, main_account, <budget dims>). It is a UNION of:budget_annual_input × normalized spread_profiles weights); andbudget_monthly_input), modeled as a per-line custom/identity profile (profile_id = 'manual', period_weight = period_amount / annual, so the same columns/semantics as the profile path).budget_sheet._sync_to_clickhouse) and the annual path should both feed the canonical model's inputs, so locking a cycle makes manual entries first-class budget data (not a dead-end table).budget_input fact clickhouse_table → the canonical model (already there if we extend gold_spread_budget).gold_scenario_trial_balance API/input branch → read the canonical model instead of the empty epm_staging.budget_input placeholder.epm_staging.budget_input (currently DDL-only and unwired) so it stops masquerading as a live source.This makes "who spread it / how it was spread" irrelevant to where it lands, and incidentally fixes the invisible-budget bug.
budget_save_batch + cycle lock) and a budget entered via annual + profile both appear in one canonical budget table at monthly grain, with identical column semantics.K.EPM(entity, year, period, account, "period_amount", "budget", …, scenarioId) returns the value for both entry modes (no zeros for manually-entered budgets).gold_scenario_trial_balance includes manually-entered + profile-spread budgets for the requested scenario_id (so consolidated/variance reporting sees them).profile_id='manual' or equivalent) with period_weight summing to 1.0 per line; FY total reconciles to the sum of 12 periods.epm_staging.budget_input is no longer a silent dead source (removed, or actually populated by the convergence path).BUDGET_2025 (profile-spread) values are unchanged after unification.budget_input fact → clickhouse_table: epm_gold.gold_spread_budget (konsol-cli fact show budget_input).konsol/epm/doctype/budget_sheet/budget_sheet.py:27 (CLICKHOUSE_TABLE = "epm_gold.budget_monthly_input"), sync at :87.dbt_project/models/gold/gold_spread_budget.sql (from budget_annual_input × spread_profiles).dbt_project/models/gold/gold_scenario_trial_balance.sql:33-52.epm_staging.budget_input created by clickhouse/init-db.sql (MergeTree, ~1 row, no app writer).konsol.api.budget_save_batch → lock cycle → K.EPM(...,2024,...,"budget",...,"BUDGET_2024") returns 0, while the rows exist in epm_gold.budget_monthly_input.
Tickets: #1
Tickets: #2
Tickets: #3
Tickets: #4
Tickets: #99
Originally posted by: grynn-in
Resolved by [#99].
gold_spread_budgetis now the single canonical monthly budget fact (see its header comment):epm_gold.budget_monthly_input) treated as an identity spread (profile_id = 'manual').budget_inputfact andgold_scenario_trial_balance— so a budget is visible regardless of how it was entered (the original symptom: monthly-direct invisible to K.EPM).layercolumn; K.EPM sums by default, filters when a layer is passed.epm_staging.budget_inputstore was removed (34b0cb1).The four-store fragmentation is unified. Closing.
Related
Tickets:
#99Ticket changed by: grynn-in