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.
"journal_proposals" subledger type on import.
Type-specific fields
| Field | Type | Purpose |
|---|---|---|
description | str | None | Narrative the GL stores as memo / description. Operator-facing. |
posting_date | date | None | Date 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. |
currency | str | None | ISO-4217. Optional — the GL provider uses the entity’s default when omitted. |
lines | list[dict] | List of {account_code, description, debit, credit, tax_code?} dicts. Stored as JSONB. Strict typing happens at GL handoff. |
GL-handoff fields
MirrorsExpenseRow. Populated by the post step, never written by runbook code directly.
| Field | Type | Purpose |
|---|---|---|
approved_at | datetime | None | Set by the HITL approval step. |
posted_to_gl | bool | True once the GL post succeeded. |
posted_journal_ref | str | None | Provider-assigned journal id. |
Why lines is a list of dicts (not a typed sub-model)
- Matches the existing
ledgers.journal_proposals.linesJSONB column shape — keeps the migration cheap. - Line shape evolves per-runbook.
nav-monthly-journalscarriesaccount_code/description/debit/credit/tax_codetoday. Other runbooks may addcost_centreortracking_categories. Loose dict here, strict typing happens atpropose_for_gltime when we land in the unifiedJournalEntryLineItem.
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
debitandcredit. - 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
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:
linesare merged into a single journal entry.descriptionandposting_datecome from the first row (caller’s responsibility to pre-validate they share these).
| Row | → | GL (JournalEntry) |
|---|---|---|
line account_code | → | JournalEntryLineItem.ledger_account.nominal_code |
line debit / credit | → | JournalEntryLineItem.type ("Debit" / "Credit") + total_amount |
line description | → | JournalEntryLineItem.description |
description | → | memo |
currency | → | currency |
posting_date (or last-day-of-period default) | → | posted_at (datetime, midnight UTC) |
"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.
Related
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.