feat(accounting): re-categorize incoming invoices, note field, pending re-bill pool

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).
This commit is contained in:
Luca
2026-06-18 12:23:13 +02:00
parent 8deb7e0741
commit 36a8e42f90
8 changed files with 553 additions and 76 deletions
+14 -2
View File
@@ -3520,6 +3520,7 @@
"saveCategorize": "Speichern",
"saveCategorizePay": "Speichern & als bezahlt markieren",
"categorize": "Kategorisieren",
"recategorize": "Neu kategorisieren",
"view": "Ansehen",
"empty": "Noch keine Dokumente — oben eines erfassen.",
"untitled": "Unbenanntes Dokument",
@@ -3551,7 +3552,10 @@
"eventId": "Event-ID (optional)",
"markup": "Zuschlag",
"reference": "Zahlungsreferenz",
"referenceHint": "QR-/ESR-Referenz oder Mitteilung"
"referenceHint": "QR-/ESR-Referenz oder Mitteilung",
"note": "Notiz",
"noteHint": "Interne Notiz zu dieser Rechnung (optional)",
"passthroughCustomerHint": "Optional — einen Kunden zuordnen, um diese durchlaufende Position weiterzuverrechnen; leer lassen, um sie nur auf das Event zu buchen."
}
},
"expenseStatus": {
@@ -3584,7 +3588,15 @@
"paid": "Bezahlt",
"paidToast": "Als bezahlt markiert.",
"categorizedToast": "Kategorisiert.",
"categorizedPaidToast": "Kategorisiert und als bezahlt markiert."
"categorizedPaidToast": "Kategorisiert und als bezahlt markiert.",
"pendingRebill": "Weiterverrechnung offen",
"pendingTitle": "Offene Weiterverrechnungen",
"pendingBody": "Kategorisierte Rechnungen, die auf die Weiterverrechnung warten. Posten eines Kunden zu einer Rechnung bündeln.",
"pendingCount": "{{count}} Posten",
"pendingCount_other": "{{count}} Posten",
"billPending": "Verrechnen",
"bundledToast": "{{count}} Weiterverrechnung zu einer Rechnung gebündelt.",
"bundledToast_other": "{{count}} Weiterverrechnungen zu einer Rechnung gebündelt."
},
"expense": {
"kind": "Art",
+14 -2
View File
@@ -3520,6 +3520,7 @@
"saveCategorize": "Save",
"saveCategorizePay": "Save & mark paid",
"categorize": "Categorize",
"recategorize": "Re-categorize",
"view": "View",
"empty": "No documents yet — capture one above.",
"untitled": "Untitled document",
@@ -3551,7 +3552,10 @@
"eventId": "Event ID (optional)",
"markup": "Markup",
"reference": "Payment reference",
"referenceHint": "QR / ESR reference or message"
"referenceHint": "QR / ESR reference or message",
"note": "Note",
"noteHint": "Internal note for this invoice (optional)",
"passthroughCustomerHint": "Optional — attach a client to re-bill this passthrough; leave empty to only book it to the event."
}
},
"expenseStatus": {
@@ -3584,7 +3588,15 @@
"paid": "Paid",
"paidToast": "Marked as paid.",
"categorizedToast": "Categorized.",
"categorizedPaidToast": "Categorized and marked paid."
"categorizedPaidToast": "Categorized and marked paid.",
"pendingRebill": "Pending re-bill",
"pendingTitle": "Pending re-bills",
"pendingBody": "Categorized invoices waiting to be re-billed. Bundle a clients items into one invoice.",
"pendingCount": "{{count}} item",
"pendingCount_other": "{{count}} items",
"billPending": "Bill these",
"bundledToast": "Bundled {{count}} re-bill into one invoice.",
"bundledToast_other": "Bundled {{count}} re-bills into one invoice."
},
"expense": {
"kind": "Type",