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

# journal_proposals

> ntro.subledger.types.journal_proposals.JournalProposalRow — one HITL-reviewed journal entry awaiting GL commit. Used by nav-monthly-journals and any runbook whose subledger fans into a single GL journal entry.

`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`](https://github.com/ntropii-com).

```
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=...)
```

```python theme={null}
from ntro.subledger.types.journal_proposals import JournalProposalRow
```

The class is auto-registered as the `"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

Mirrors `ExpenseRow`. 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)

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`

```python theme={null}
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:

| 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)                                  |

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.

## Related

<CardGroup cols={2}>
  <Card title="Subledgers overview" icon="table" href="/sdk/subledgers">
    `Row` base + `SubledgerStatus` lifecycle.
  </Card>

  <Card title="expenses" icon="receipt" href="/sdk/subledgers/types/expenses">
    The other shipped platform type.
  </Card>

  <Card title="Accounting" icon="calculator" href="/sdk/capabilities/accounting">
    `JournalLine` / `JournalProposal` proposal-builder helpers.
  </Card>

  <Card title="General ledgers" icon="landmark" href="/sdk/capabilities/general-ledgers">
    Posting the `JournalProposal` via `provider.journal_entries.create`.
  </Card>
</CardGroup>
