RFC-006: Finance — Budget Management Implementation
- Status: Draft
- Author(s): Aditya Lohumi
- Created: 2026-02-26
- Updated: 2026-02-26
- TSC Decision: Pending
- Supersedes: —
- Superseded by: —
Summary
Section titled “Summary”This RFC covers the implementation of Phase 04 (Budget Management) of the finance library. It defines the controller logic for Budget and BudgetRevision, the budget utilisation tracking mechanism triggered by GL postings, and the configurable budget enforcement behaviour (warn vs. block) as decided in RFC-003 §10.
Motivation
Section titled “Motivation”Enterprises and public-sector organisations alike operate under defined budget allocations tied to account heads and cost centres. The system must track utilised spend against approved budget allocations, raise alerts when approaching or exceeding limits, and maintain a full revision audit trail. The enforcement mode must be configurable to allow initial deployment in advisory/warning mode before switching to hard-block mode.
Detailed Design
Section titled “Detailed Design”1. Budget — Submit & Lock
Section titled “1. Budget — Submit & Lock”On submit of a Budget:
- Lock the
allocated_amount(cannot be changed directly; only viaBudgetRevision) - Status transitions to
"Active"
class BudgetController(Controller[Budget]):
async def before_submit(self, doc: Budget) -> None: """Validate budget before activation.""" if doc.allocated_amount <= Decimal("0"): raise ValidationError("Allocated amount must be greater than zero.")
async def after_submit(self, doc: Budget) -> None: doc.status = "Active"2. BudgetRevision — Audit Trail & Budget Update
Section titled “2. BudgetRevision — Audit Trail & Budget Update”On submit of a BudgetRevision:
- Validate
previous_amount == Budget.allocated_amount(optimistic concurrency check) - Update
Budget.allocated_amount = revision.revised_amount - Transition
BudgetRevision.status = "Approved"
class BudgetRevisionController(Controller[BudgetRevision]):
async def before_submit(self, doc: BudgetRevision) -> None: budget = await self.db.get(Budget, doc.budget) if budget.status != "Active": raise ValidationError("Can only revise an Active budget.") if budget.allocated_amount != doc.previous_amount: raise ValidationError( "Budget has been concurrently modified. " "Refresh and re-enter the revision." )
async def after_submit(self, doc: BudgetRevision) -> None: await self.db.set_value( Budget, doc.budget, "allocated_amount", doc.revised_amount ) doc.status = "Approved"3. Budget Utilisation Tracking
Section titled “3. Budget Utilisation Tracking”Budget.utilized_amount is updated whenever a GL entry is posted against the linked account and cost_center within the fiscal_year. This is driven by a hook from the GL posting layer (called from PaymentVoucher, JournalEntry, etc. controllers after Book-Keeper confirms the GL entry):
async def update_budget_utilization( company: str, account: str, cost_center: str | None, fiscal_year: str, amount: Decimal, reverse: bool = False,) -> None: """ Increment (or decrement on cancel/reverse) budget utilization. Called by PaymentVoucher, JournalEntry controllers after GL posting. """ budget = await db.get_value( Budget, filters={ "company": company, "account": account, "cost_center": cost_center, "fiscal_year": fiscal_year, "status": "Active", }, ) if not budget: return # No budget configured for this account head — no-op
delta = -amount if reverse else amount new_utilized = budget.utilized_amount + delta
settings = await FinanceSettings.get_single() if not reverse and new_utilized > budget.allocated_amount: if settings.budget_enforcement_mode == "block": raise BudgetExceededError( f"Budget exceeded for account '{account}'. " f"Allocated: {budget.allocated_amount}, " f"Would be utilized: {new_utilized}." ) else: # "warn" mode — show warning but allow posting add_message( type="warning", message=f"Budget limit exceeded for account '{account}'. " f"Allocated: {budget.allocated_amount}, " f"Utilized: {new_utilized}.", )
await db.set_value(Budget, budget.name, "utilized_amount", new_utilized)4. Budget Enforcement Mode (from FinanceSettings)
Section titled “4. Budget Enforcement Mode (from FinanceSettings)”| Mode | Behaviour |
|---|---|
"warn" (default) |
Allow GL posting; show warning on the document form |
"block" |
Raise BudgetExceededError; GL posting is blocked until budget is revised |
This setting lives in FinanceSettings.budget_enforcement_mode.
5. Budget Closure
Section titled “5. Budget Closure”When a FiscalYear is closed, all Budget records for that fiscal year should transition to "Closed". This is triggered from the FiscalYearController.after_save (when is_closed transitions to True):
async def after_save(self, doc: FiscalYear) -> None: if doc.is_closed: await self.db.update( Budget, filters={"fiscal_year": doc.name, "status": "Active"}, values={"status": "Closed"}, )6. API Endpoints
Section titled “6. API Endpoints”| Endpoint | Description |
|---|---|
| Standard CRUD | Budget, BudgetRevision |
GET /api/resource/Budget/{id}/utilisation_report |
Returns allocation vs utilisation breakdown |
Example
Section titled “Example”# Activate a budgetbudget = await Budget.new({ "budget_number": "BDG-2026-GS-001", "company": "Acme Corp", "fiscal_year": "FY 2026-27", "account": "General Stores Expenses", "cost_center": "Ops-Delhi", "allocated_amount": Decimal("500000.00"),})await budget.submit() # status → "Active"
# Revise itrevision = await BudgetRevision.new({ "revision_number": "REV-2026-001", "budget": budget.name, "revised_date": date(2026, 6, 1), "previous_amount": Decimal("500000.00"), "revised_amount": Decimal("600000.00"), "reason": "Additional demand due to increased project scope",})await revision.submit()# Budget.allocated_amount is now 600,000Drawbacks
Section titled “Drawbacks”- Budget utilisation is updated synchronously inside the GL posting transaction. If budget tracking is slow (e.g., complex multi-account queries), it adds latency to every payment submission.
- Concurrent GL postings against the same budget may cause race conditions on
utilized_amount. A database-levelSELECT FOR UPDATEor optimistic locking strategy must be applied.
Alternatives
Section titled “Alternatives”| Alternative | Pros | Cons | Why Not |
|---|---|---|---|
| Compute utilisation on-the-fly from GL | Always accurate, no stored value | Expensive read for every budget check | Performance at scale |
| Async utilisation update (queue) | Non-blocking GL posting | Utilisation lags behind reality | Unsafe for block enforcement mode |
Migration
Section titled “Migration”No migration required — Budget and BudgetRevision are new doctypes.
Unresolved Questions
Section titled “Unresolved Questions”- Should budget checking apply to all GL postings (including system-generated JEs like depreciation) or only user-initiated payments?
- How should budget enforcement interact with cancelled documents — i.e., when a
PaymentVoucheris cancelled,utilized_amountmust be decremented. Is this handled by the reversalreverse=Trueflag inupdate_budget_utilization?
Implementation Plan
Section titled “Implementation Plan”- Implement
BudgetController.before_submit— amount validation - Implement
BudgetController.after_submit— status transition - Implement
BudgetRevisionController.before_submit— concurrent modification guard - Implement
BudgetRevisionController.after_submit— updateBudget.allocated_amount - Implement
update_budget_utilizationutility function - Integrate
update_budget_utilizationintoPaymentVoucherandJournalEntrycontrollers - Implement budget closure on
FiscalYearclose - Implement
utilisation_reportendpoint - Unit tests for all controller methods
- Concurrency tests for budget utilisation updates
- Integration tests covering warn mode and block mode
Testing Strategy
Section titled “Testing Strategy”Unit Tests
Section titled “Unit Tests”| Test | Scenario |
|---|---|
test_budget_submit_negative_amount |
allocated_amount ≤ 0 → ValidationError |
test_budget_revision_stale_amount |
previous_amount mismatch → ValidationError |
test_budget_revision_closed_budget |
Revising a non-Active budget → ValidationError |
test_utilisation_warn_mode |
Over-limit in warn mode → warning message, posting proceeds |
test_utilisation_block_mode |
Over-limit in block mode → BudgetExceededError |
test_utilisation_reverse |
Cancel decrements utilized_amount |
test_budget_closure_on_fiscal_year_close |
Closing FY transitions all Active budgets to Closed |
Concurrency Tests
Section titled “Concurrency Tests”- Simulate concurrent GL postings against the same budget; verify
utilized_amountis correctly accumulated.
References
Section titled “References”- RFC-003: Finance Library Architecture (Decision 2 — warn/block enforcement)
- RFC-004: Foundation & F&A Masters