Adds an Invoices sub-menu under /income with a summary table (one row per invoice, click to expand the full detail) and the data model behind it. Data: - invoices + invoice_lines PocketBase collections, created idempotently by scripts/setup-invoice-collections.mjs - line items transcribed from PHI 166/167/171 with per-invoice arithmetic validation in the seed; a bad invoice is refused without blocking the rest - invoice-*.json in data/source are auto-imported, so adding an invoice is one file Booking: - manual book-in per line (Booked to), grouped by category - POST /api/invoice-lines/:id/book moves the income row rather than adding a second one, so a booking can be changed freely - scripts/book-invoice.mjs is idempotent: rows are stamped with monthly_income.invoice_line and replaced on re-run - booked amount follows lib/invoice-booking-policy.json (bookInclusiveVat), read by both the app and the CLI so they cannot disagree Money: - the R24,205.13 column on these invoices is PLUS VAT, not escalation. The seed now rejects VAT that disagrees with the stated rate. - booked amounts are VAT-inclusive per the client ruling of 2026-10-01; the VAT stays visible rather than hidden. Uploads: - drag-and-drop PDF attached to invoices.source_pdf - GET /api/invoices/:id/document proxies it — the collection is rule-locked, so a direct PocketBase /api/files link 404s in a browser Fixes a month-aggregation bug this introduced: buildIncomeRows assigned into months[i], so a machine with two income rows in one month (one per invoice, which is normal multi-site use) showed only the last. SS01 September read R20,700 instead of R35,305 while its own YTD included both. Months now accumulate, per-month profit is computed once after accumulation, and lib/rows.test.ts pins it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
24 lines
No EOL
1.2 KiB
TypeScript
24 lines
No EOL
1.2 KiB
TypeScript
import policy from "./invoice-booking-policy.json";
|
|
|
|
// Single source of truth for the amount an invoice line contributes to a machine's
|
|
// income. The flag itself lives in ./invoice-booking-policy.json because the CLI
|
|
// booking script must obey exactly the same rule — two copies of a financial
|
|
// constant will eventually disagree.
|
|
//
|
|
// Client ruling, 2026-10-01: amounts are booked VAT-INCLUSIVE, i.e. the figure the
|
|
// debtor actually pays. Set `bookInclusiveVat: false` in the JSON to book ex-VAT
|
|
// instead, then re-run `npm run seed && node scripts/book-invoice.mjs`.
|
|
export const BOOK_INCLUSIVE_VAT = policy.bookInclusiveVat;
|
|
|
|
// The amount to post to monthly_income for one invoice line.
|
|
export function bookedAmount(amountExcl: number, vat: number): number {
|
|
const r = (n: number) => Math.round((n || 0) * 100) / 100;
|
|
return BOOK_INCLUSIVE_VAT ? r(amountExcl) + r(vat) : r(amountExcl);
|
|
}
|
|
|
|
// Label for whichever mode is active, so the UI never says "ex VAT" while booking
|
|
// the inclusive figure (or the reverse).
|
|
export const BOOKED_LABEL = BOOK_INCLUSIVE_VAT ? "incl VAT" : "ex VAT";
|
|
export const BOOKED_LONG_LABEL = BOOK_INCLUSIVE_VAT
|
|
? "Booked to income (incl VAT)"
|
|
: "Booked to income (ex VAT)"; |