Skip to main content
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.
The class is auto-registered as the "journal_proposals" subledger type on import.

Type-specific fields

GL-handoff fields

Mirrors ExpenseRow. Populated by the post step, never written by runbook code directly.

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

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: 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.