Budget Spreading / Seasonality Profiles¶
Problem¶
Budget input is per-period line items. In practice, planners often enter annual amounts and expect the system to spread them across months using a profile: - Even — divide by 12 - Seasonal — use historical seasonal pattern - Custom — user-defined weights per period
Currently there is no spreading logic — users must enter all 12 periods manually.
Requirements¶
R1: Spread profile seed¶
New seed spread_profiles.csv:
profile_id,profile_name,fiscal_period,weight
EVEN,Even Spread,1,1.0
EVEN,Even Spread,2,1.0
...
EVEN,Even Spread,12,1.0
SEASONAL_RETAIL,Retail Seasonal,1,0.6
SEASONAL_RETAIL,Retail Seasonal,2,0.5
...
SEASONAL_RETAIL,Retail Seasonal,11,1.8
SEASONAL_RETAIL,Retail Seasonal,12,2.5
R2: API endpoint for annual budget input¶
POST /api/v1/budget-annual— accepts:{ "scenario_id": "BUDGET_2025", "legal_entity_id": "USMF", "fiscal_year": 2025, "main_account": "6100", "annual_amount": 1200000, "spread_profile_id": "EVEN", "submitted_by": "user@co.com" }- API spreads the annual amount into 12 period rows in
budget_inputstaging table
R3: dbt model gold_spread_budget¶
- Reads annual budget entries (where
fiscal_period = 0convention) from staging - Joins with
spread_profilesseed - Normalizes weights:
period_weight = weight / sum(weight) over profile - Calculates:
period_amount = annual_amount × period_weight - Outputs 12 rows per annual input
R4: Integration with scenario TB¶
gold_scenario_trial_balancemust include spread budget entries- Either via the staging table (API does the spreading) or via the dbt model (dbt does the spreading)
- Decision: API does the spreading into staging → simpler, dbt just reads staging as-is
Acceptance Tests¶
| Test | Assertion |
|---|---|
assert_even_spread_equal_periods |
EVEN profile: all 12 periods have same amount (annual / 12) within 0.01 |
assert_spread_sums_to_annual |
Per annual input: sum of 12 period amounts = annual_amount within 0.01 |
assert_seasonal_weights_applied |
Period amounts are proportional to profile weights |
assert_spread_profile_weights_positive |
All weights > 0 |
Out of Scope¶
- Driver-based planning (revenue × price × volume)
- Rolling forecast auto-spread
- Phasing templates at account-group level