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
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,
source_entry_id FK entry(id) ON DELETE SET NULL,
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_id PK FK month(id) ON DELETE CASCADE,
month_entry_id FK month_entry(id) ON DELETE SET NULL,
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
preserves `origin_name`, `origin_planned`, and `notes`. `source_entry_id`
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.
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
@ -170,11 +208,18 @@ POST /month/{year_month}/activate Planning -> Active
POST /month/{year_month}/close Active -> Closed (requires applied zero = 0)
POST /month/{year_month}/reopen Closed -> Active
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
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:
* The debt target card when a Debt Minimums row was added / edited / deleted
@ -186,10 +231,19 @@ accurate without a reload.
## HTMX conventions
* Month entry rows carry three inline inputs (`name`, `planned`,
`applied`) plus a notes input, each with `hx-post` and
`hx-trigger="change"`. Each input sends only its own field; the server
accepts any subset.
* Month entry rows carry inline inputs for `name` and `planned` plus a
notes input inside the expanded body. Each input has `hx-post` and
`hx-trigger="change"`. Each input sends only its own field; the
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}"` +
`hx-swap="outerHTML"`.
* 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
rotates on `[open]`
* `.section` with a small-caps header and an inline subtotal
* `table.entries` with a 4-column grid (name / planned / applied /
actions) and a 2px progress bar on each row's bottom border
* Month entry rows are `<details class="entry-block">` blocks with a
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
* `.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
**Flexible** (food, subscriptions, other).
* `/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.
* **Zero Amount header** on both pages. Green when every dollar is
assigned, amber when unassigned income remains, red when over-budget.
@ -31,6 +34,10 @@ real spending on the applied side.
number on month pages.
* **Per-entry notes** — free-text annotation on every row. Copied through
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
@ -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
backup script with alembic auto-hook, zero-amount header, section groups
with collapsible headers, sinking funds section, per-entry notes, month
lifecycle with the balance gate, UI redesign in Barlow Condensed + logo.
See the [Roadmap](Roadmap) for what is next.
lifecycle with the balance gate, UI redesign in Barlow Condensed + logo,
backing transaction ledger with `applied` derived from postings. See the
[Roadmap](Roadmap) for what is next.
## Code

@ -133,6 +133,7 @@ Applied migrations at time of writing:
| `03ebe3c07262` | add month snapshot tables (`month`, `month_entry`, `month_debt_target`) |
| `ec804bdf366d` | add `notes` column to `entry` and `month_entry` |
| `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
and the backup hook fires between each hop.

@ -13,19 +13,31 @@
| 13 | Notes field per entry | 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 |
| 19 | Backing transaction ledger: replace applied field with Postings | 2026-04-17 |
## Deferred
Things we have explicitly called out as future work. File an issue when
the time comes.
### Transaction log behind applied
### Constrain posting dates to the month
Replace the hand-edited `applied` value with a log of dated
transactions per entry per month. `applied` becomes a computed sum.
Implies a new `month_transaction` table, a UI for entering
transactions, and a migration path that preserves existing applied
values as an opening balance.
Postings are free-dated today (a May 3 transaction can live on an
April entry). Most users would expect the date to fall within the
month of the entry. A small validator + UI hint would tighten this
without closing the escape hatch for one-off overrides.
### 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