docs: refresh wiki for posting ledger (applied now derived from postings)
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
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.
|
||||||
|
|
|
||||||
24
Roadmap.md
24
Roadmap.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue