Skip to main content
ntro.accounting is the domain layer — typed models for the things fund accountants care about (chart of accounts, journal lines, proposals) plus validation helpers and one reference proposal algorithm. It’s deliberately thin: most fund-ops calculation logic is tenant-specific and lives in the runbook itself.
Heads up: the trial-balance type moved to ntro.capabilities.gl.reports.TrialBalanceReport in N-70. It’s a typed read-only view of external GL state — fits the GL capability, not the legacy accounting kitchen-sink. The old import (from ntro.accounting.models import TrialBalance) still works for one release as a deprecated alias.
Three submodules ship today:
SubmodulePurpose
ntro.accounting.modelsPydantic types — ChartOfAccounts, JournalLine, JournalProposal
ntro.accounting.proposalReference algorithms — propose_allocation_journal()
ntro.accounting.validationPure validation — validate_journal_balance(), validate_required_accounts()

Install

pip install 'ntro[workflow]'
The accounting module is bundled with the workflow extra.

Models — ntro.accounting.models

Strict Pydantic models for the fund-ops domain. They give your runbook type safety, JSON-serialisable shapes, and a model_validate round-trip you can rely on at activity boundaries.
from ntro.accounting.models import (
    ChartOfAccounts,
    JournalLine,
    JournalProposal,
)
from ntro.capabilities.gl.reports import TrialBalanceReport  # was ntro.accounting.models.TrialBalance pre-N-70

TrialBalanceReport (moved to ntro.capabilities.gl.reports)

A balanced TB at a point in time. Lifted from nav-monthly-journals:
from ntro.capabilities.gl.reports import TrialBalanceReport

# Pattern B fast path — hydrate from the cached extraction
# document-ingest already wrote at upload time:
tb = TrialBalanceReport.from_extracted_payload(cached_payload, period=ctx.period)

# Or live path (only when re-extracting from raw bytes — ~30s):
# tb = TrialBalanceReport.from_extraction(result, period=ctx.period)

# Built-in invariants — raise if the TB doesn't balance or is the wrong period
tb.assert_balanced()
tb.assert_period_match(ctx.period)
See General ledgers → Reports for the full Pattern B explanation.

ChartOfAccounts

The list of accounts the entity uses, with their hierarchy and types:
from ntro.accounting.models import ChartOfAccounts

coa = ChartOfAccounts.load(entity_slug=ctx.entity_slug)  # populated upstream
income_accounts = coa.filter(account_type="income")

JournalLine and JournalProposal

A JournalProposal is a list of JournalLine objects assembled into a single journal entry — the artifact of nav-monthly-journals that an accountant approves before it posts back to Xero / SAP / whatever the authoritative GL is.
from ntro.accounting.models import JournalLine, JournalProposal

# Constructing a line manually (rare — usually proposal.py builds these)
line = JournalLine(
    account_code="200",
    debit=Decimal("1500.00"),
    credit=Decimal("0.00"),
    narrative="March rent — 12 High Street",
    period="2026-03",
)

Proposal — ntro.accounting.proposal

One reference algorithm ships today: propose_allocation_journal. It takes a parsed TB plus the ingested period documents and produces a balanced journal that allocates the activity to the right GL codes. Lifted from nav-monthly-journals:
from ntro.accounting.proposal import propose_allocation_journal


@activity.defn(name="nav_monthly_journals.propose_journal")
async def propose_journal(input: ProposalInput) -> JournalProposal:
    """Apply the pure proposal algorithm and validate balance."""
    proposal = propose_allocation_journal(
        tb=input.tb,
        ingested=input.ingested,
        gl_map=input.gl_map,
        period=input.period,
        entity_slug=input.entity_slug,
        mortgage_agreement=input.mortgage_agreement,
    )

    validate_journal_balance(proposal.lines).raise_if_failed()
    return proposal
The algorithm is pure (no I/O) so it can be unit-tested without the harness. Wrapping it in a Temporal activity is what gives the step durability, retries, and a LOADING UI surface — but the maths itself is just a function call.
propose_allocation_journal is the reference implementation for real-estate SPV monthly NAV. The proposal logic for other domains (private credit, infrastructure, capital calls) is tenant-specific and lives in the runbook itself, not the SDK.

Validation — ntro.accounting.validation

Pure validation helpers that return a ValidationResult with passed: bool and findings: list[str]. You either short-circuit on failure (raise_if_failed()) or surface the findings to a human.
from ntro.accounting.validation import (
    validate_journal_balance,
    validate_required_accounts,
)

# Balance check — debits == credits per period
result = validate_journal_balance(proposal.lines)
result.raise_if_failed()

# COA coverage — every required account exists
result = validate_required_accounts(coa, required=["200", "1100"])
if not result.passed:
    for finding in result.findings:
        log.warn(finding)
Lifted from nav-monthly:
from ntro.accounting.validation import validate_required_accounts

@activity.defn(name="nav_monthly.open_period")
async def open_period(ctx: NavMonthlyContext) -> PeriodOpened:
    coa = await load_coa(ctx.entity_slug)
    validate_required_accounts(
        coa, required=ctx.coa_required_accounts
    ).raise_if_failed()
    ...

What’s not in the SDK

The Notion structure plan calls out a roadmap of accounting calculations — NAV calculation, fee / waterfall / equalisation, FX translation, valuation adjustments. None of those are in ntro.accounting today. If your runbook needs them, you write them in the runbook itself (or in your own private accounting library that the runbook imports). The reason is twofold: (1) most of these calculations are tenant-specific in their detail, and (2) they’re commercially valuable enough that we’d rather not ship reference implementations that might lock customers into ours. The SDK gives you the primitives (typed TB, balanced journal validation, COA shape) and stays out of the calculations.

General ledgers

Post proposals to the customer GL via ntro.capabilities.gl.

Private AI

ai.extract() is what produces the ExtractionResult TrialBalanceReport.from_extraction() consumes.

Quality checks

Pair validate_journal_balance with a quality check for stronger HITL routing.