- #1 resolveTaxTreatment: an unconfigured (empty) reclaim-countries list no
longer auto-classifies every supplier — incl. the admin's own domestic one —
as foreign; defer auto-classification until the setting is set (+ test).
- #2 pending re-bills on customer erase: eraseCustomer now returns the
customer's not-yet-billed inbound docs to the inbox (null customer + unsorted)
so they aren't billable to an anonymized account. (NB: picpeak has no hard
customer delete — erase anonymizes in place — so the orphan/404 premise can't
occur; this is hardening.)
- #4 VatRateSelect: when >1 configured code shares the same rate, fall through
to the legacy "(not configured)" option instead of silently picking the first.
- #5 unwindBilledLine: delete the (mutable, never-issued) invoice when the
unwound re-bill was its only line, instead of leaving a net-zero survivor.
- #6 isInvoiceMutable: clarify in a comment that invoices have no 'draft' status
(the editable state is 'scheduled' w/o send-at) — no behaviour change.
- nit: collapse normalizeCurrency's tautological ternary.
- Fix VAT picker i18n: t('vat.legacyRate') → 'ledger.vat.legacyRate' (the key's
real home), so the legacy label localizes instead of always showing English.
- Remove dead i18n keys left by the settings refactor (businessProfile.field VAT
/hourly + profileFields.title/savedToast).
VAT supplier-country reclaim default:
- Migration 134 adds inbound_documents.supplier_country.
- categorizeInbound auto-derives tax_treatment via resolveTaxTreatment:
explicit treatment wins; else country in the reclaim list → domestic,
outside it → foreign_vat_non_reclaimable, unknown → domestic. Consumes the
previously-stored-but-unused accounting_vat_reclaim_countries.
- Triage modal gains a Supplier country dropdown (saved via updateInbound).
+5 unit tests for resolveTaxTreatment.
Configurable default output VAT code for new invoices:
- New accounting_default_output_vat_code setting (PUT wired; getSettings/type).
- Settings → Accounting dropdown to pick it.
- Invoice + quote editors seed their VAT picker (rate + code) from it on a
blank new document — skipping edits/conversions, never clobbering a touched
value. New docs no longer silently start at 0%.
i18n en + de.
Address three incoming-invoice issues:
1. Re-categorization: a categorized invoice can now be changed again (e.g.
passthrough → company expense). New "Re-categorize" button pre-fills the
triage modal from the existing disposition/customer/markup/note.
categorizeInbound is re-runnable — it unwinds any prior re-bill line
(removes the invoice line + recomputes totals) before applying the new
disposition, and refuses (INVOICE_LOCKED) when the re-bill is on an
already-issued invoice.
2. Note field: new `note` column (migration 132 — 126 is already on beta)
captured in triage and shown in the read-only view.
3. Re-bill like hours: rebill/passthrough now persist customer_account_id.
Per-event customers accumulate as PENDING items, surfaced in a new
"Pending re-bills" card and bundled into one invoice via "Bill these"
(mirrors unbilled-hours billing). Monthly/manual customers keep
auto-consolidating onto their running draft. Passthrough (durchlaufend)
can now also attach to a customer with optional markup.
Adds backend unit tests for buildInboundLineItem + isInvoiceMutable and
en/de translations (other locales fall back to English defaults).
Implements the split decided in review:
Incoming invoices (external) - the inbound_documents row IS the payable:
- categorizeInbound now UPDATES the document (disposition + tax_treatment +
booking event_id (null=company) + category), no derived expense row, so a
supplier invoice appears only in the incoming-invoices surface.
- rebillInbound mints the client invoice from the document (base = invoice
total + markup) and links it on the doc.
- markInboundSupplierPayment records supplier payment ON the incoming invoice
(mark-paid lives here now).
Expenses (internal) - own costs only:
- createExpense: kind = amount|mileage|per_diem; amount = quantity x rate
(rate from accounting settings, per-entry override; snapshotted); optional
proof file; booked to an event or the company; require-proof enforced from
settings. No supplier payment, always own-cost.
- listExpenses returns internal rows only (inbound_document_id IS NULL).
Routes: per-flag gating (incomingInvoices vs expenses; categories on the
accounting master); supplier-payment + re-bill moved under /inbound/:id/*;
POST/PATCH expenses accept a multipart proof upload; GET /:id/proof streams it
(PDF download-only, image inline). getAccountingSettings reads app_settings.
Verified: node -c, require-graph, 12 unit tests (markup + expense amount/build).
Frontend rework (service + the two UIs + settings tab + category i18n) follows.
Covers the silently-regressable money + classification bits of the re-bill
flow (the maintainer's "thin CRM test coverage" concern). Pure functions via a
new expenseService._internal export — no DB, no date-harness pitfalls:
- computeMarkupMinor: percent rounding, flat, none/null.
- resolveMarkup precedence: override > expense clause > none.
- buildExpenseInsert: bad-disposition guard, tax_treatment/status defaults,
declined -> status+reason, markup field matches type, parked -> status.
11 tests, all green (npx jest expenseService.markup).