Skip to content

RFC-004: Finance — Foundation & F&A Masters Implementation

  • Status: Draft
  • Author(s): Aditya Lohumi
  • Created: 2026-02-26
  • Updated: 2026-02-26
  • TSC Decision: Pending
  • Supersedes: —
  • Superseded by: —

This RFC covers the implementation of the Phase 00 (Foundation) and Phase 01 (F&A Masters) of the finance library. Together these establish the foundational data structures — company context, financial calendar, chart of accounts, cost centres, and financial party extensions — that every other finance module depends on. Because all other RFCs build on these, this phase must be implemented and stabilised first.

Without a working Company, FiscalYear, AccountingPeriod, and Account hierarchy, no GL entry can be posted, no budget can be tracked, and no payment can be recorded. These masters are the roots of the entire F&A data model.


Timezone-Aware Dates (Decision 5 from RFC-003)

Section titled “Timezone-Aware Dates (Decision 5 from RFC-003)”

All date/datetime fields on FiscalYear and AccountingPeriod must be migrated from the current naive str representation to timezone-aware datetime objects using Python’s datetime with timezone.utc.

# BEFORE (incorrect — naive string)
start_date: str = Field(description="Start Date (YYYY-MM-DD)")
# AFTER (correct — timezone-aware datetime)
from datetime import datetime, timezone
start_date: datetime = Field(description="Fiscal Year Start (timezone-aware)")

All date comparisons, period overlap checks, and GL posting date validations must use timezone-aware datetimes throughout.

A new singleton settings doctype to drive configurable behaviours:

class FinanceSettings(DocType):
__doctype_name__ = "FinanceSettings"
# Budget enforcement: "warn" (default) or "block"
budget_enforcement_mode: str = Field(default="warn")
# Depreciation cron schedule (cron expression)
depreciation_cron_schedule: str = Field(default="0 1 * * *")
# Book-Keeper API base URL
bookkeeper_api_url: str = Field(default="")
bookkeeper_api_key: str | None = Field(default=None)

The most significant logic in this phase is the Account → m-ledger sync:

class AccountController(Controller[Account]):
doctype = Account
async def after_save(self, doc: Account) -> None:
"""Sync account to m-ledger Book-Keeper after every save."""
try:
ledger_id = await bookkeeper_client.sync_account(
name=doc.name,
account_type=doc.account_type,
company=doc.company,
is_group=doc.is_group,
parent_account=doc.parent_account,
)
# Update ledger_account_id without triggering another save
await self.db.set_value(Account, doc.name, "ledger_account_id", ledger_id)
except BookKeeperSyncError as e:
# Surface a non-blocking warning; the account is saved but not yet synced
self.add_message(
type="warning",
message=f"Account saved but could not sync to Book-Keeper: {e}. "
"Use 'Sync to Ledger' action to retry.",
)

A custom sync_to_ledger action endpoint must be registered on the Account doctype to allow manual retry.

class FiscalYearController(Controller[FiscalYear]):
async def before_save(self, doc: FiscalYear) -> None:
"""Validate that start_date < end_date and no overlap with existing years."""
if doc.start_date >= doc.end_date:
raise ValidationError("Fiscal year start date must be before end date.")
# Check for overlap with other fiscal years for the same company context
await self._validate_no_overlap(doc)
class AccountingPeriodController(Controller[AccountingPeriod]):
async def before_save(self, doc: AccountingPeriod) -> None:
"""Validate period is within fiscal year bounds and non-overlapping."""
fiscal_year = await self.db.get(FiscalYear, doc.fiscal_year)
if doc.start_date < fiscal_year.start_date or doc.end_date > fiscal_year.end_date:
raise ValidationError("Accounting period must fall within the fiscal year.")
await self._validate_no_overlap(doc)

On before_save of Account, validate:

  • A group account (is_group=True) cannot be directly posted to (no GL entries)
  • A leaf account must belong to a parent_account that is a group
  • account_type of a child must be consistent with its parent’s account_type
  • Circular references in parent_account must be rejected

4. FinanceSupplier / FinanceCustomer Uniqueness

Section titled “4. FinanceSupplier / FinanceCustomer Uniqueness”

Each supplier/customer record in m-core should have at most one FinanceSupplier / FinanceCustomer record per company:

class FinanceSupplierController(Controller[FinanceSupplier]):
async def before_save(self, doc: FinanceSupplier) -> None:
existing = await self.db.get_value(
FinanceSupplier,
filters={"supplier": doc.supplier, "company": doc.company},
)
if existing and existing.name != doc.name:
raise ValidationError(
f"A FinanceSupplier record already exists for supplier "
f"'{doc.supplier}' in company '{doc.company}'."
)

# Creating the foundational master data programmatically
company = await Company.new({
"name": "Acme Corp",
"abbr": "ACME",
"default_currency": "INR",
})
fy = await FiscalYear.new({
"name": "FY 2026-27",
"start_date": datetime(2026, 4, 1, tzinfo=timezone.utc),
"end_date": datetime(2027, 3, 31, 23, 59, 59, tzinfo=timezone.utc),
})
account = await Account.new({
"name": "Bank Accounts",
"account_type": "Asset",
"is_group": True,
"company": "Acme Corp",
})
# AccountController.after_save fires → syncs to m-ledger automatically

  • The automatic m-ledger sync in after_save introduces a network call on every save, which adds latency. Mitigation: the sync is non-blocking (warning on failure, not an error); the manual fallback ensures data consistency.
  • Date migration from str to datetime requires a data migration script for any existing records.
Alternative Pros Cons Why Not
Lazy sync (sync on first GL posting) No extra latency on account save ledger_account_id is absent until first GL use; breaks GL validation Deferred complexity
Event-driven sync (queue) Decoupled, retryable Requires queue infrastructure Overkill for master data volume
  • Existing FiscalYear / AccountingPeriod records: Run a data migration script to parse the str date fields and re-save as timezone-aware datetime objects.
  • No breaking API changes for consumers that already use ISO 8601 strings — the serialised form remains identical.
  • FinanceSettings is a new singleton; no migration required for existing records.
  • Should Account sync to Book-Keeper support partial failures (some legs sync, some fail) in a batch scenario, or must all accounts in a batch succeed atomically?
  • Should CostCenter also sync to Book-Keeper (i.e., does Book-Keeper have a “cost centre” concept)?
  • Fix FiscalYear / AccountingPeriod date fields to timezone-aware datetime
  • Scaffold FinanceSettings singleton doctype
  • Implement AccountController.after_save m-ledger sync
  • Implement Account — custom sync_to_ledger action endpoint
  • Implement FiscalYearController.before_save — overlap validation
  • Implement AccountingPeriodController.before_save — bounds + overlap validation
  • Implement Account tree validation (group/leaf, circular ref, type consistency)
  • Implement FinanceSupplierController / FinanceCustomerController uniqueness guards
  • Write unit tests for all controllers
  • Write integration tests for m-ledger sync (with mock Book-Keeper)
  • Write API endpoint tests for CRUD + sync_to_ledger action
Test Scenario
test_fiscal_year_date_validation start ≥ end raises ValidationError
test_fiscal_year_overlap Overlapping fiscal year raises ValidationError
test_accounting_period_out_of_bounds Period outside fiscal year raises error
test_account_circular_ref Circular parent_account raises error
test_account_group_consistency Leaf account type mismatch raises error
test_finance_supplier_duplicate Duplicate FinanceSupplier per company raises error
test_account_sync_success Successful sync sets ledger_account_id
test_account_sync_failure_warning Failed sync surfaces warning, does not block save
  • Mock Book-Keeper API; verify Account.after_save calls correct endpoint with correct payload
  • Verify sync_to_ledger custom action retries and sets ledger_account_id on success
  • RFC-003: Finance Library Architecture & Doctypes
  • Framework M Controller lifecycle documentation
  • m-ledger Book-Keeper API specification