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.

JournalProposalRow captures one journal entry waiting on operator approval before it posts to the external GL. Stored in ledgers.journal_proposals. Migration: 005_journal_proposals_subledger.sql.
runbook builds JournalLine[]
    ↓ (commit step)
JournalProposalRow stored in ledgers.journal_proposals
    ↓ (post step, idempotent)
JournalProposalRow.propose_for_gl(...) → JournalProposal (GL handoff)

GLProvider.journal_entries.create(proposal, external_id=...)
from ntro.subledger.types.journal_proposals import JournalProposalRow
The class is auto-registered as the "journal_proposals" subledger type on import.

Type-specific fields

FieldTypePurpose
descriptionstr | NoneNarrative the GL stores as memo / description. Operator-facing.
posting_datedate | NoneDate the journal posts on. Distinct from period — a March journal might post on the 31st, a year-end adjustment on the prior day. Required for GL commit; if absent we derive last-day-of-period at handoff.
currencystr | NoneISO-4217. Optional — the GL provider uses the entity’s default when omitted.
lineslist[dict]List of {account_code, description, debit, credit, tax_code?} dicts. Stored as JSONB. Strict typing happens at GL handoff.

GL-handoff fields

Mirrors ExpenseRow. Populated by the post step, never written by runbook code directly.
FieldTypePurpose
approved_atdatetime | NoneSet by the HITL approval step.
posted_to_glboolTrue once the GL post succeeded.
posted_journal_refstr | NoneProvider-assigned journal id.

Why lines is a list of dicts (not a typed sub-model)

  1. Matches the existing ledgers.journal_proposals.lines JSONB column shape — keeps the migration cheap.
  2. Line shape evolves per-runbook. nav-monthly-journals carries account_code/description/debit/credit/tax_code today. Other runbooks may add cost_centre or tracking_categories. Loose dict here, strict typing happens at propose_for_gl time when we land in the unified JournalEntryLineItem.

Validation rules

Two model validators run at row construction:

_strict_fields_present_when_not_needs_attention

Outside NEEDS_ATTENTION, lines must be non-empty AND each line must:
  • Have a non-empty account_code.
  • Have non-negative debit and credit.
  • Have a non-zero amount on exactly one side (debit XOR credit > 0). Both zero → invalid; both positive → invalid.
NEEDS_ATTENTION rows are the only status allowed to be empty (placeholder for HITL fix-up).

_balanced_when_approved

A journal posted to the GL MUST balance (Σ debits == Σ credits). Enforced only on APPROVED / POSTED rows so the runbook can store an in-progress draft mid-HITL before the user adds the balancing line.

GL handoff — JournalProposalRow.propose_for_gl

from uuid import UUID
from ntro.subledger.types.journal_proposals import JournalProposalRow

proposal = JournalProposalRow.propose_for_gl(
    rows=[approved_row],
    task_id=UUID("…"),
)
# proposal: ntro.subledger.proposals.JournalProposal
# Hand off via your GLProvider:
result = await provider.journal_entries.create(
    proposal, external_id=proposal.idempotency_key
)
Converts one or more APPROVED subledger rows to a single JournalProposal for GL commit. nav-monthly-journals calls this with one row (the approved monthly allocation journal). Other runbooks may pass multiple rows when the GL provider’s batched-journal model suits — e.g. a period-close runbook posting all true-up journals as one batch. When multiple rows are passed:
  • lines are merged into a single journal entry.
  • description and posting_date come from the first row (caller’s responsibility to pre-validate they share these).
Field mapping:
RowGL (JournalEntry)
line account_codeJournalEntryLineItem.ledger_account.nominal_code
line debit / creditJournalEntryLineItem.type ("Debit" / "Credit") + total_amount
line descriptionJournalEntryLineItem.description
descriptionmemo
currencycurrency
posting_date (or last-day-of-period default)posted_at (datetime, midnight UTC)
Idempotency key follows the canonical convention: "journal_proposals:{task_id}:{row.id}" for single-row proposals. Multi-row uses the first row’s id (deterministic across re-invocations as long as the row order is stable). Rejects non-APPROVED rows — only signed-off journals go to the GL.

Subledgers overview

Row base + SubledgerStatus lifecycle.

expenses

The other shipped platform type.

Accounting

JournalLine / JournalProposal proposal-builder helpers.

General ledgers

Posting the JournalProposal via provider.journal_entries.create.