feat(accounting): re-bill proof attachment, CRM panel & hours↔re-bills cross-add (#979)
Closes #866. Three features, all behind the `incomingInvoices` feature flag: 1. Attach the stored supplier proof PDF to the client-invoice email when a captured invoice is re-billed/passed through, as a SEPARATE attachment so invoice immutability holds. Global default (off), per-customer tri-state override, and per-file selection in a new Send dialog. A missing proof at issue time stamps inbound_documents.proof_attach_error rather than silently dropping, and never blocks the send. Proof filename is a configurable template with {INVOICE} {SUPPLIER} {YEAR} {MONTH} {SEQ}/{SEQ:0Nd} tokens. 2. Re-bills & passthrough panel under CRM → Customer, grouped Open/Sent/Paid with status derived from the linked invoice lifecycle rather than a duplicated column. 3. Cross-add dialog rolling open hours and open re-bills into one invoice, symmetric from both entry points. The two stay distinct, contiguous line groups — never merged into shared line items. Migration 169 is additive, hasColumn-guarded and idempotent. Review (two rounds) closed two concerns: - Storno stranding: nothing cleared inbound_documents.billed_invoice_id when a covering invoice was cancelled, so a Storno'd re-bill showed as Open in the new panel while every billing path filters on that column being NULL — the supplier cost could never be re-billed. releaseRebillsForCancelledInvoice now detaches the linkage on both invoice-cancel paths, with a regression test on the issued-cancel path. - Permission gating: the new controls rendered on data presence alone while their endpoints require accounting.view / accounting.manage / customers.edit. Now gated at both the query and render layers. Known follow-up: two cross-add counter queries are gated on a permission their endpoint does not check (HoursSection.tsx:174, CustomerCrmPanels.tsx:270) — degrades safely, one line each.
This commit is contained in:
@@ -2042,7 +2042,11 @@
|
||||
"hourlyRate": "Standard-Stundensatz",
|
||||
"hourlyRatePlaceholder": "z. B. 120.00",
|
||||
"hourlyRateHint": "Verrechnungs-Fallback, wenn ein Kunde keinen eigenen Satz hat (Stundenerfassung). In {{currency}}, in Hauptwährungseinheiten. Leer lassen, um einen Satz pro Kunde oder pro Eintrag zu verlangen."
|
||||
}
|
||||
},
|
||||
"rebillAttachProof": "Lieferantenbeleg bei Weiterverrechnungen standardmäßig anhängen",
|
||||
"rebillAttachProofHint": "Wenn eine erfasste Lieferantenrechnung weiterverrechnet oder durchlaufend berechnet wird, wird ihr gespeichertes PDF der Rechnungs-E-Mail als separater Beleg beigefügt. Dies ist die Voreinstellung — eine kundenspezifische Einstellung und eine Auswahl pro Datei im Senden-Dialog können sie jederzeit ändern.",
|
||||
"rebillProofNameFormat": "Dateiname-Format für Belege",
|
||||
"rebillProofNameFormatHint": "Dateiname des angehängten Beleg-PDFs. Platzhalter: {INVOICE}, {SUPPLIER}, {YEAR}, {MONTH}, {SEQ} (oder {SEQ:03d}). Leer lassen für den Standard „Beleg-{INVOICE}“. Bei mehreren Belegen pro Rechnung wird automatisch ein Index angehängt. Behalten Sie ein Präfix wie „Beleg-“, damit der Beleg nicht genauso heißt wie das Rechnungs-PDF."
|
||||
},
|
||||
"slideshow": {
|
||||
"title": "Diashow",
|
||||
@@ -4119,7 +4123,8 @@
|
||||
"toast": {
|
||||
"created": "Eintrag erfasst",
|
||||
"deleted": "Eintrag gelöscht",
|
||||
"billed": "Stunden verrechnet"
|
||||
"billed": "Stunden verrechnet",
|
||||
"billedCombined": "Rechnung aus Stunden und Weiterverrechnungen erstellt."
|
||||
},
|
||||
"noRate": {
|
||||
"title": "Kein Stundensatz hinterlegt",
|
||||
@@ -4272,7 +4277,9 @@
|
||||
"contractsSection": "Verträge",
|
||||
"noContracts": "Noch keine Verträge für diesen Kunden.",
|
||||
"billsSection": "Rechnungen",
|
||||
"noBills": "Noch keine Rechnungen für diesen Kunden."
|
||||
"noBills": "Noch keine Rechnungen für diesen Kunden.",
|
||||
"rebillsSection": "Weiterverrechnungen & Durchlaufposten",
|
||||
"noRebills": "Noch keine weiterverrechneten oder durchlaufenden Lieferantenrechnungen für diesen Kunden."
|
||||
},
|
||||
"billing": {
|
||||
"section": "Abrechnungsrhythmus",
|
||||
@@ -4297,7 +4304,12 @@
|
||||
"title": "Offen für die Rechnung dieses Monats",
|
||||
"titleManual": "Offen – wird auf manuelle Auslösung versendet",
|
||||
"periodRange": "{{number}} · {{from}} – {{to}}"
|
||||
}
|
||||
},
|
||||
"rebillAttachProof": "Lieferantenbeleg bei Weiterverrechnungen anhängen",
|
||||
"rebillAttachProofInherit": "Mandanten-Standard verwenden",
|
||||
"rebillAttachProofOn": "Immer anhängen",
|
||||
"rebillAttachProofOff": "Nie anhängen",
|
||||
"rebillAttachProofHint": "Überschreibt die globale Voreinstellung für diesen Kunden. Der Senden-Dialog erlaubt weiterhin die Auswahl einzelner Belege bei jedem Versand."
|
||||
},
|
||||
"reactivate": {
|
||||
"button": "Reaktivieren",
|
||||
@@ -4483,7 +4495,8 @@
|
||||
"pendingCount_other": "{{count}} Posten",
|
||||
"billPending": "Verrechnen",
|
||||
"bundledToast": "{{count}} Weiterverrechnung zu einer Rechnung gebündelt.",
|
||||
"bundledToast_other": "{{count}} Weiterverrechnungen zu einer Rechnung gebündelt."
|
||||
"bundledToast_other": "{{count}} Weiterverrechnungen zu einer Rechnung gebündelt.",
|
||||
"amountRequired": "Rechnungsbetrag vor der Weiterverrechnung eingeben (0 ist erlaubt)."
|
||||
},
|
||||
"expense": {
|
||||
"kind": "Art",
|
||||
@@ -5213,6 +5226,18 @@
|
||||
"swiss": "Swiss QR-Bill",
|
||||
"epc": "EPC QR (SEPA)",
|
||||
"profileDefault": "Standard aus Geschäftsprofil verwenden"
|
||||
},
|
||||
"send": {
|
||||
"title": "Rechnung senden",
|
||||
"proofIntro": "Diese Rechnung verrechnet erfasste Lieferantenrechnungen weiter. Wählen Sie, welche Lieferantenbelege der E-Mail beigefügt werden — die Rechnung als PDF wird immer angehängt.",
|
||||
"proofsLabel": "Lieferantenbelege",
|
||||
"selectAll": "Alle auswählen",
|
||||
"selectNone": "Keine",
|
||||
"unknownSupplier": "Lieferant",
|
||||
"modePassthrough": "durchlaufend",
|
||||
"modeRebill": "Weiterverrechnung",
|
||||
"noProofFile": "Keine gespeicherte Belegdatei",
|
||||
"sendWithCount": "Mit {{count}} Beleg(en) senden"
|
||||
}
|
||||
},
|
||||
"businessProfile": {
|
||||
@@ -5784,5 +5809,34 @@
|
||||
"syncBusy": "Es läuft bereits eine Synchronisierung.",
|
||||
"syncFailed": "Synchronisierung fehlgeschlagen.",
|
||||
"actionFailed": "Aktion fehlgeschlagen."
|
||||
},
|
||||
"rebills": {
|
||||
"createInvoice": "Rechnung aus Weiterverrechnungen erstellen",
|
||||
"unknownSupplier": "Lieferant",
|
||||
"costLabel": "Kosten {{amount}}",
|
||||
"proofError": "Beleg nicht angehängt: {{err}}",
|
||||
"status": {
|
||||
"open": "Offen",
|
||||
"sent": "Versendet",
|
||||
"paid": "Bezahlt"
|
||||
},
|
||||
"mode": {
|
||||
"passthrough": "Durchlaufend",
|
||||
"rebill": "Weiterverrechnung"
|
||||
},
|
||||
"toast": {
|
||||
"billed": "Rechnung aus Weiterverrechnungen erstellt.",
|
||||
"billedCombined": "Rechnung aus Weiterverrechnungen und Stunden erstellt.",
|
||||
"billFailed": "Rechnung konnte nicht erstellt werden"
|
||||
}
|
||||
},
|
||||
"crossAdd": {
|
||||
"title": "Weitere offene Posten hinzufügen?",
|
||||
"body": "Dieser Kunde hat außerdem {{count}} {{label}}. Zur selben Rechnung hinzufügen? Sie bleiben eine separate Gruppe — Stunden und Weiterverrechnungen werden nie in einer Position vermischt.",
|
||||
"addBoth": "Beide hinzufügen",
|
||||
"hours": "offene Stunden",
|
||||
"rebills": "offene Weiterverrechnungen",
|
||||
"hoursOnly": "Nur die Stunden",
|
||||
"rebillsOnly": "Nur die Weiterverrechnungen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1587,7 +1587,11 @@
|
||||
"hourlyRate": "Default hourly rate",
|
||||
"hourlyRatePlaceholder": "e.g. 120.00",
|
||||
"hourlyRateHint": "Billing fallback used when a customer has no own rate (hours logging). In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate."
|
||||
}
|
||||
},
|
||||
"rebillAttachProof": "Attach the supplier proof to re-billed invoices by default",
|
||||
"rebillAttachProofHint": "When a captured supplier invoice is re-billed or passed through, attach its stored PDF to the client-invoice email as a separate proof. This is the default — a per-customer override and a per-file choice in the Send dialog can change it each time.",
|
||||
"rebillProofNameFormat": "Proof filename format",
|
||||
"rebillProofNameFormatHint": "Filename for the attached proof PDF. Tokens: {INVOICE}, {SUPPLIER}, {YEAR}, {MONTH}, {SEQ} (or {SEQ:03d}). Leave blank for the default “Beleg-{INVOICE}”. When several proofs ride one invoice, an index is appended automatically. Keep a prefix like “Beleg-” so the proof isn’t named identically to the invoice PDF."
|
||||
},
|
||||
"slideshow": {
|
||||
"title": "Slideshow",
|
||||
@@ -4119,7 +4123,8 @@
|
||||
"toast": {
|
||||
"created": "Entry logged",
|
||||
"deleted": "Entry deleted",
|
||||
"billed": "Hours billed"
|
||||
"billed": "Hours billed",
|
||||
"billedCombined": "Invoice created from hours and re-bills."
|
||||
},
|
||||
"noRate": {
|
||||
"title": "No hourly rate configured",
|
||||
@@ -4272,7 +4277,9 @@
|
||||
"noContracts": "No contracts for this customer yet.",
|
||||
"billsSection": "Invoices",
|
||||
"noBills": "No invoices for this customer yet.",
|
||||
"manageEvents": "Manage galleries"
|
||||
"manageEvents": "Manage galleries",
|
||||
"rebillsSection": "Re-bills & passthrough",
|
||||
"noRebills": "No re-billed or passed-through supplier invoices for this customer yet."
|
||||
},
|
||||
"billing": {
|
||||
"section": "Billing cadence",
|
||||
@@ -4297,7 +4304,12 @@
|
||||
"title": "Pending in this month's bill",
|
||||
"titleManual": "Pending — ships on manual trigger",
|
||||
"periodRange": "{{number}} · {{from}} – {{to}}"
|
||||
}
|
||||
},
|
||||
"rebillAttachProof": "Attach supplier proof to re-billed invoices",
|
||||
"rebillAttachProofInherit": "Use tenant default",
|
||||
"rebillAttachProofOn": "Always attach",
|
||||
"rebillAttachProofOff": "Never attach",
|
||||
"rebillAttachProofHint": "Overrides the global default for this customer. The Send dialog still lets you pick individual proofs each time an invoice goes out."
|
||||
},
|
||||
"reactivate": {
|
||||
"button": "Reactivate",
|
||||
@@ -4483,7 +4495,8 @@
|
||||
"pendingCount_other": "{{count}} items",
|
||||
"billPending": "Bill these",
|
||||
"bundledToast": "Bundled {{count}} re-bill into one invoice.",
|
||||
"bundledToast_other": "Bundled {{count}} re-bills into one invoice."
|
||||
"bundledToast_other": "Bundled {{count}} re-bills into one invoice.",
|
||||
"amountRequired": "Enter the invoice amount before re-billing (0 is allowed)."
|
||||
},
|
||||
"expense": {
|
||||
"kind": "Type",
|
||||
@@ -5211,6 +5224,18 @@
|
||||
"overdue": "Overdue",
|
||||
"cancelled": "Cancelled",
|
||||
"skipped": "Skipped (empty month)"
|
||||
},
|
||||
"send": {
|
||||
"title": "Send invoice",
|
||||
"proofIntro": "This invoice re-bills captured supplier invoices. Choose which supplier proofs to attach to the email — the invoice PDF is always attached.",
|
||||
"proofsLabel": "Supplier proofs",
|
||||
"selectAll": "Select all",
|
||||
"selectNone": "None",
|
||||
"unknownSupplier": "Supplier",
|
||||
"modePassthrough": "passthrough",
|
||||
"modeRebill": "re-bill",
|
||||
"noProofFile": "No stored proof file",
|
||||
"sendWithCount": "Send with {{count}} proof(s)"
|
||||
}
|
||||
},
|
||||
"businessProfile": {
|
||||
@@ -5782,5 +5807,34 @@
|
||||
"syncBusy": "A sync is already running.",
|
||||
"syncFailed": "Sync failed.",
|
||||
"actionFailed": "Action failed."
|
||||
},
|
||||
"rebills": {
|
||||
"createInvoice": "Create invoice from re-bills",
|
||||
"unknownSupplier": "Supplier",
|
||||
"costLabel": "cost {{amount}}",
|
||||
"proofError": "Proof not attached: {{err}}",
|
||||
"status": {
|
||||
"open": "Open",
|
||||
"sent": "Sent",
|
||||
"paid": "Paid"
|
||||
},
|
||||
"mode": {
|
||||
"passthrough": "Passthrough",
|
||||
"rebill": "Re-bill"
|
||||
},
|
||||
"toast": {
|
||||
"billed": "Invoice created from re-bills.",
|
||||
"billedCombined": "Invoice created from re-bills and hours.",
|
||||
"billFailed": "Failed to create invoice"
|
||||
}
|
||||
},
|
||||
"crossAdd": {
|
||||
"title": "Add other open items?",
|
||||
"body": "This customer also has {{count}} {{label}}. Add them to the same invoice? They stay as a separate group — hours and re-bills are never mixed into one line.",
|
||||
"addBoth": "Add both",
|
||||
"hours": "open hours",
|
||||
"rebills": "open re-bills",
|
||||
"hoursOnly": "Just the hours",
|
||||
"rebillsOnly": "Just the re-bills"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user