Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ntropii.com/llms.txt

Use this file to discover all available pages before exploring further.

ExpenseRow captures one expense receipt with provenance back to the originating ingest.submitted_records row. Stored in ledgers.expenses. Migration: 004_expenses.sql.
from ntro.subledger.types.expenses import ExpenseRow
The class is auto-registered as the "expenses" subledger type on import.

Type-specific fields

FieldTypePurpose
event_idUUIDProvenance — points to the originating ingest.submitted_records row. Joined during HITL review to surface the original receipt’s raw payload.
vendorstr | NoneSupplier name. Required outside NEEDS_ATTENTION.
amount_grossDecimal | None (gt=0)Receipt total. Receipts are positive expenses — a negative is either an extraction bug or a credit note (separate type). Required outside NEEDS_ATTENTION.
currencyCurrencyCode | NoneISO-4217. Required outside NEEDS_ATTENTION.
expense_datedate | NoneDate on the receipt.
payment_methodstr | None"card", "cash", "bank_transfer", etc. Free-form.
line_itemslist[dict] | NoneOptional sub-lines for multi-line receipts.
vat_amountDecimal | None (ge=0)VAT portion. Zero allowed (zero-rated items); negative isn’t a normal category.
notesstr | NoneFree-form.
categorystr | NoneGL-code-shaped classification. Auto-proposed, HITL-correctable.
category_sourcestr | None"vendor_lookup" | "llm" | "manual".
confidenceDecimal | None (0 ≤ x ≤ 1)Classifier confidence; drives HITL routing.

GL-handoff fields

Populated by the post step, never written by runbook code directly.
FieldTypePurpose
approved_atdatetime | NoneSet by the runbook’s HITL approval step.
posted_to_glboolTrue once the GL post succeeded.
posted_journal_refstr | NoneProvider-assigned journal id from the successful create call.

Loose-on-NEEDS_ATTENTION contract

vendor, amount_gross, currency are typed Optional so the insert_needs_attention path can build a “ghost row” with NULLs in the typed columns. Once the row leaves NEEDS_ATTENTION (i.e. HITL fixed the issue), these MUST be populated. Enforced two ways:
  1. _strict_fields_present_when_not_needs_attention — Pydantic model_validator raises at row construction.
  2. DB CHECK constraint mirrors the same rule (defense in depth).
A second cross-field invariant — vat_amount <= amount_gross — catches the common extraction bug where the agent confused gross with net somewhere on the receipt. Skipped on NEEDS_ATTENTION rows.

GL handoff — ExpenseRow.propose_for_gl

from uuid import UUID
from ntro.subledger.types.expenses import ExpenseRow

bills = ExpenseRow.propose_for_gl(rows=approved_rows, task_id=UUID("…"))
Converts APPROVED expense rows to a list of BillProposals — one bill per row (no merging — each receipt is its own bill in the GL). Defaults to the unified Bill primitive (AP / supplier invoice); Xero’s deprecated Expense Claims module isn’t used. Field mapping:
Row fieldBill field
vendorsupplier (LinkedSupplierInput by display_name)
amount_grosstotal + a single BillLineItem.total_amount
currencycurrency
expense_datebill_date
vat_amounttotal_tax
categoryBillLineItem.ledger_account.nominal_code
notesnotes
Idempotency key follows the canonical convention: "expenses:{task_id}:{row.id}". Rejects non-APPROVED rows — only signed-off bills go to the GL.

Mutation helpers — ntro.subledger.types.expenses_mutations

Pure-domain helpers for HITL row actions. No DB access — transports reuse them consistently.
from ntro.subledger.types.expenses_mutations import (
    can_reject,
    coerce_edit_value,
    EditableExpenseField,
)
EditableExpenseField enumerates which columns the UI can edit per cell: vendor, currency, expense_date, payment_method, notes, category, category_source, amount_gross, vat_amount. Anything else returns INVALID_FIELD. can_reject only allows reject from NEEDS_ATTENTION (or no-op from REJECTED); other statuses return INVALID_TRANSITION.

Canonicalisation — canonicalize_tabular_expense_row

Maps loose extraction keys onto the strict ExpenseRow field names — handles the common alias variants (amount, total, gross_totalamount_gross; tax, vatvat_amount; etc.). Used by tabular ingest (expense-processor parsing a CSV) so runbook authors don’t re-implement column-name normalisation.
from ntro.subledger.types.expenses import canonicalize_tabular_expense_row

normalised = canonicalize_tabular_expense_row(raw_row)
expense = ExpenseRow(**normalised, entity_id=..., period=..., task_id=...)

Subledgers overview

Row base + SubledgerStatus lifecycle.

journal_proposals

The other shipped platform type.

General ledgers

Posting BillProposal to the external GL.

Typing

CurrencyCode, ForgivingDate, Period — used in ExpenseRow field types.