docs: refresh wiki for posting ledger (applied now derived from postings)

archeious 2026-04-17 17:49:42 -06:00
parent fe7edfe973
commit 1276499df5
4 changed files with 101 additions and 24 deletions

@ -68,17 +68,36 @@ month
month_entry month_entry
id, month_id FK month(id) ON DELETE CASCADE, id, month_id FK month(id) ON DELETE CASCADE,
section, name, planned, applied, notes NULL, section, name, planned, notes NULL,
origin_name NULL, origin_planned NULL, origin_name NULL, origin_planned NULL,
source_entry_id FK entry(id) ON DELETE SET NULL, source_entry_id FK entry(id) ON DELETE SET NULL,
created_at, updated_at created_at, updated_at
posting
id, month_entry_id FK month_entry(id) ON DELETE CASCADE,
occurred_on DATE, amount NUMERIC(10,2),
description NULL, payee NULL,
created_at, updated_at
month_debt_target (singleton per month) month_debt_target (singleton per month)
month_id PK FK month(id) ON DELETE CASCADE, month_id PK FK month(id) ON DELETE CASCADE,
month_entry_id FK month_entry(id) ON DELETE SET NULL, month_entry_id FK month_entry(id) ON DELETE SET NULL,
updated_at updated_at
``` ```
`month_entry.applied` is a derived Python property, not a column:
```python
class MonthEntry(Base):
@property
def applied(self) -> Decimal:
return sum((p.amount for p in self.postings), Decimal("0")).quantize(...)
```
The relationship uses `lazy="selectin"` so a page that iterates
`month.entries` fires one extra SELECT for all postings at once,
rather than N+1 lazy loads.
Creating a month copies every budget entry into `month_entry` and Creating a month copies every budget entry into `month_entry` and
preserves `origin_name`, `origin_planned`, and `notes`. `source_entry_id` preserves `origin_name`, `origin_planned`, and `notes`. `source_entry_id`
links back to the budget but is informational only; the snapshot is links back to the budget but is informational only; the snapshot is
@ -97,7 +116,26 @@ Computed from `(name, planned)` vs `(origin_name, origin_planned)`:
tag in indigo; progress bar tinted indigo. tag in indigo; progress bar tinted indigo.
Notes changes do NOT flip the deviation state. Notes are annotation, not Notes changes do NOT flip the deviation state. Notes are annotation, not
plan drift. plan drift. Posting activity also does NOT flip deviation; postings are
normal month flow.
## Postings (transaction ledger)
Each `month_entry` owns a list of postings. A posting is a single
debit or credit in the ledger:
* `occurred_on` (required) — ISO date; not constrained to the month
(a posting dated May 3 may live under the April entry if you want).
* `amount` (required) — can be negative for refunds or corrections.
* `description`, `payee` — optional free-text.
Some entries will have one posting per month (mortgage, rent); others
will have many (groceries). The UI exposes them inside each expanded
entry row with an inline add form.
User-facing language is "transactions" throughout; the schema and ORM
use "posting" (classical accounting term, avoids collision with
SQLAlchemy's own `Session.transaction` concept).
### Per-month debt target ### Per-month debt target
@ -164,17 +202,24 @@ POST /debt-target set / clear debt target
### Month (`src/quartermaster/routes_month.py`) ### Month (`src/quartermaster/routes_month.py`)
``` ```
GET /month/{year_month} view page or create-flow GET /month/{year_month} view page or create-flow
POST /month/{year_month}/create snapshot the budget (lands in Planning) POST /month/{year_month}/create snapshot the budget (lands in Planning)
POST /month/{year_month}/activate Planning -> Active POST /month/{year_month}/activate Planning -> Active
POST /month/{year_month}/close Active -> Closed (requires applied zero = 0) POST /month/{year_month}/close Active -> Closed (requires applied zero = 0)
POST /month/{year_month}/reopen Closed -> Active POST /month/{year_month}/reopen Closed -> Active
POST /month/{year_month}/sections/{section}/entries add month entry POST /month/{year_month}/sections/{section}/entries add month entry
POST /month/{year_month}/entries/{entry_id} update (name / planned / applied / notes) POST /month/{year_month}/entries/{entry_id} update (name / planned / notes)
DELETE /month/{year_month}/entries/{entry_id} remove month entry DELETE /month/{year_month}/entries/{entry_id} remove month entry
POST /month/{year_month}/target set / clear per-month target POST /month/{year_month}/entries/{entry_id}/postings add a transaction
POST /month/{year_month}/postings/{posting_id} update a transaction
DELETE /month/{year_month}/postings/{posting_id} delete a transaction
POST /month/{year_month}/target set / clear per-month target
``` ```
`update_month_entry` no longer accepts an `applied` field. Applied
is managed by posting create / update / delete; direct writes are
not allowed.
All mutation routes return the section partial plus OOB swaps for: All mutation routes return the section partial plus OOB swaps for:
* The debt target card when a Debt Minimums row was added / edited / deleted * The debt target card when a Debt Minimums row was added / edited / deleted
@ -186,10 +231,19 @@ accurate without a reload.
## HTMX conventions ## HTMX conventions
* Month entry rows carry three inline inputs (`name`, `planned`, * Month entry rows carry inline inputs for `name` and `planned` plus a
`applied`) plus a notes input, each with `hx-post` and notes input inside the expanded body. Each input has `hx-post` and
`hx-trigger="change"`. Each input sends only its own field; the server `hx-trigger="change"`. Each input sends only its own field; the
accepts any subset. server accepts any subset.
* Applied is rendered as static text (not an input). To change
applied, the user expands the row's `<details>` and edits or adds
postings.
* Posting rows inside the expanded body have four inline inputs each
(date, description, payee, amount) also using `hx-post` +
`hx-trigger="change"`. The add-posting form is a normal `<form>`
that POSTs and re-renders the section on success.
* Per-section add forms (adding a new entry) are hidden behind a
small `+ add <section>` disclosure so the default view is dense.
* Section partials swap via `hx-target="#section-{section}"` + * Section partials swap via `hx-target="#section-{section}"` +
`hx-swap="outerHTML"`. `hx-swap="outerHTML"`.
* OOB swaps use `hx-swap-oob="outerHTML"` on the top-level element of * OOB swaps use `hx-swap-oob="outerHTML"` on the top-level element of
@ -218,8 +272,10 @@ accurate without a reload.
* `details.group` collapsible groups with a hairline CSS chevron that * `details.group` collapsible groups with a hairline CSS chevron that
rotates on `[open]` rotates on `[open]`
* `.section` with a small-caps header and an inline subtotal * `.section` with a small-caps header and an inline subtotal
* `table.entries` with a 4-column grid (name / planned / applied / * Month entry rows are `<details class="entry-block">` blocks with a
actions) and a 2px progress bar on each row's bottom border 5-column summary grid (caret / name / planned / applied / actions).
The 2px progress bar rides the summary's bottom border.
* Budget entry rows use `table.entries` with a 4-column grid.
* `.target-section` with a burgundy left bar and `↳` margin glyph * `.target-section` with a burgundy left bar and `↳` margin glyph
* `.state-badge` tracked-caps label with bullet separators * `.state-badge` tracked-caps label with bullet separators

14
Home.md

@ -23,7 +23,10 @@ real spending on the applied side.
minimums + Primary Debt Target), **Savings** (sinking funds), and minimums + Primary Debt Target), **Savings** (sinking funds), and
**Flexible** (food, subscriptions, other). **Flexible** (food, subscriptions, other).
* `/month/YYYY-MM` — a snapshot of the budget for a specific calendar * `/month/YYYY-MM` — a snapshot of the budget for a specific calendar
month. Adds an `applied` column per entry to track actuals. Rows carry month. Each entry becomes a `<details>` block: the summary row shows
planned (click-to-edit) and applied (derived); expand the row to see
its backing ledger of transactions (postings) and add more. Applied
is always `sum(postings.amount)`, never typed directly. Rows carry
deviation tags when edited or added post-snapshot. deviation tags when edited or added post-snapshot.
* **Zero Amount header** on both pages. Green when every dollar is * **Zero Amount header** on both pages. Green when every dollar is
assigned, amber when unassigned income remains, red when over-budget. assigned, amber when unassigned income remains, red when over-budget.
@ -31,6 +34,10 @@ real spending on the applied side.
number on month pages. number on month pages.
* **Per-entry notes** — free-text annotation on every row. Copied through * **Per-entry notes** — free-text annotation on every row. Copied through
at snapshot time; editable inline. at snapshot time; editable inline.
* **Backing transaction ledger** — every month entry owns a list of
postings (date, amount, optional description and payee). Applied is
derived from the ledger. One posting for fixed items like rent,
many for variable ones like groceries.
## Month lifecycle ## Month lifecycle
@ -64,8 +71,9 @@ where leftover applied dollars belong; filling it is the user's job.
Shipped: initial scaffold, monthly snapshot with deviation tags, database Shipped: initial scaffold, monthly snapshot with deviation tags, database
backup script with alembic auto-hook, zero-amount header, section groups backup script with alembic auto-hook, zero-amount header, section groups
with collapsible headers, sinking funds section, per-entry notes, month with collapsible headers, sinking funds section, per-entry notes, month
lifecycle with the balance gate, UI redesign in Barlow Condensed + logo. lifecycle with the balance gate, UI redesign in Barlow Condensed + logo,
See the [Roadmap](Roadmap) for what is next. backing transaction ledger with `applied` derived from postings. See the
[Roadmap](Roadmap) for what is next.
## Code ## Code

@ -133,6 +133,7 @@ Applied migrations at time of writing:
| `03ebe3c07262` | add month snapshot tables (`month`, `month_entry`, `month_debt_target`) | | `03ebe3c07262` | add month snapshot tables (`month`, `month_entry`, `month_debt_target`) |
| `ec804bdf366d` | add `notes` column to `entry` and `month_entry` | | `ec804bdf366d` | add `notes` column to `entry` and `month_entry` |
| `a4ec4f8f6e9f` | add month lifecycle columns (`state`, `activated_at`, `closed_at`) | | `a4ec4f8f6e9f` | add month lifecycle columns (`state`, `activated_at`, `closed_at`) |
| `cc60e7f73a1c` | add `posting` ledger table, seed opening-balance postings, drop `month_entry.applied` |
After pulling new code, `uv run alembic upgrade head` walks the chain After pulling new code, `uv run alembic upgrade head` walks the chain
and the backup hook fires between each hop. and the backup hook fires between each hop.

@ -13,19 +13,31 @@
| 13 | Notes field per entry | 2026-04-17 | | 13 | Notes field per entry | 2026-04-17 |
| 15 | Month lifecycle: Planning, Active, Closed with reconciliation gate | 2026-04-17 | | 15 | Month lifecycle: Planning, Active, Closed with reconciliation gate | 2026-04-17 |
| 17 | UI redesign: condensed-sans ledger style with logo in the zero hero | 2026-04-17 | | 17 | UI redesign: condensed-sans ledger style with logo in the zero hero | 2026-04-17 |
| 19 | Backing transaction ledger: replace applied field with Postings | 2026-04-17 |
## Deferred ## Deferred
Things we have explicitly called out as future work. File an issue when Things we have explicitly called out as future work. File an issue when
the time comes. the time comes.
### Transaction log behind applied ### Constrain posting dates to the month
Replace the hand-edited `applied` value with a log of dated Postings are free-dated today (a May 3 transaction can live on an
transactions per entry per month. `applied` becomes a computed sum. April entry). Most users would expect the date to fall within the
Implies a new `month_transaction` table, a UI for entering month of the entry. A small validator + UI hint would tighten this
transactions, and a migration path that preserves existing applied without closing the escape hatch for one-off overrides.
values as an opening balance.
### Bank-statement reconciliation
A view that ingests a CSV or OFX export and matches rows against
existing postings. Would need a "cleared" flag on postings and a
match-by-amount + date workflow.
### Recurring posting schedules
For regular bills (mortgage on the 1st, subscription on the 15th),
let the user register a recurrence rule on a month_entry so the
posting appears automatically when activating a new month.
### Closed-month "archived" visual treatment ### Closed-month "archived" visual treatment