An invoice saved with no amount landed as amount_incl = 0 (PocketBase
stores a number field as zero, not null), so the PO read "R 0,00 invoiced"
and the money never booked. There were two ways to give it an amount and
neither worked: the Upload-invoice modal let a blank amount save silently,
while the double-click correction form refused to save without a value AND
a written reason, and sat behind Edit mode.
- lib/purchase-invoice-amount.ts holds the rules, shared by the screen and
the API so they cannot disagree. R0 is not an amount: a blank amount and
a figure of zero are both refused, so an invoice can never land as zero.
- The API now separates PRICING an invoice that never had an amount (no
reason owed, still logged) from CORRECTING one that was booked (Full user
+ a reason, as before).
- Double-clicking an amount on a PO opens the ordinary invoice modal on
that invoice, with the amount field waiting. The inline correction form is
gone — one window, one rule. There is no pencil button: a booked figure
must take a deliberate second gesture, so a single stray tap never opens
a form that can rewrite money.
- The PO-lines table freezes its Description column with an opaque
background and fades its right edge, so it passes the mobile checklist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Menu: old Suppliers leaf hidden (route kept), Suppliers 2 renamed Suppliers
- PO invoice status: new 'over' state (never closed), live over-PO warning when typing
- Admin correction of invoice amount/date/number with mandatory reason, logged to purchase_invoice_edits
- Payable now carries unpaid balance forward month to month
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Supplier invoices on purchase orders (several per PO), global Upload invoice with search and drag-drop
- PO invoice status line (red/orange/teal), week/month filter, group by supplier, drag-drop PO upload
- Suppliers 2: spend, payable by terms, cash hidden by default, manual payments with surplus/shortfall
- Fix multi-page PO parsing (per-page rows, header filter direction)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Splits the Running Costs group into six purpose-built pages, each with its own
collection and its own source data, plus a coordinate parser for purchase orders.
Insurance (R503 330/yr, 20 vehicles)
A premium can move every month or three, so it is stored as a RATE PERIOD in
insurance_rates and each month's cost is resolved against the rate in force
that month. cost_lines insurance rows are derived from those periods. An absent
rate is null, never 0 — nobody has entered the date, which is not the same as
being insured for free.
Financing (20 agreements, R428 585/mo)
Instalment from the sheet; start date, paid-off date and the settled toggle are
human-entered and preserved across re-seeds. Months-left and outstanding are
null until a date is entered, never R0 on a live debt. The two paid-off
agreements (WB02, DT01) show as settled and are excluded from the run-rate.
Purchases + Oil Stock
Uploads PO PDFs the way /income/invoices does. The parser reads columns by
x-position, because plain text extraction reorders them, and refuses any PO
whose lines do not sum to the sub-total. A cost code either names a machine
("SS-02" -> SS02) or a stock store ("MPLANT, ENGOIL") — never both, never
guessed. Oil POs move litres into oil_stock_movements instead of onto a machine;
stock is the sum of movements so the first usage is a subtraction from history.
Operators (19) and Mechanics (2)
Both are people with an employee number, separately reconcilable by the seed so
someone added in the app survives. Mechanics get a badge QR; a mechanic with no
employee number cannot be printed, because the code is what the QR encodes.
Cross-cutting
- The seed no longer deletes operators, mechanics or oil items: people are added
in the app, and those rows exist in no file.
- Re-pointing allocations that PocketBase clears because `assets` is rebuilt
with new ids each seed (operators, purchase lines).
- One QR encoder for screen and PDF, so a printed code cannot drift from the one
on screen. pdf-lib writes real vector PDFs; pdfjs-dist only reads them.
- Every figure is null-safe: unknown is never rendered as zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>