165cebdb5c
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.
39 lines
2.1 KiB
JavaScript
39 lines
2.1 KiB
JavaScript
/**
|
|
* renderProofName — the configurable Beleg proof-attachment filename template.
|
|
* Pure function; no DB. Covers token substitution, the multi-proof index
|
|
* fallback, padding, and filesystem-safe sanitisation.
|
|
*/
|
|
const { renderProofName } = require('../../src/services/invoice/rebillProofs');
|
|
|
|
describe('renderProofName', () => {
|
|
const base = { invoiceNumber: 'R-2026-0042', supplierName: 'ACME AG', seq: 1, hasMulti: false, issueDate: '2026-08-03' };
|
|
|
|
it('defaults to Beleg-<invoice>.pdf', () => {
|
|
expect(renderProofName('Beleg-{INVOICE}', base)).toBe('Beleg-R-2026-0042.pdf');
|
|
expect(renderProofName('', base)).toBe('Beleg-R-2026-0042.pdf');
|
|
expect(renderProofName(null, base)).toBe('Beleg-R-2026-0042.pdf');
|
|
});
|
|
|
|
it('substitutes every token incl. padded SEQ and date parts', () => {
|
|
expect(renderProofName('{SUPPLIER}-{INVOICE}-{YEAR}{MONTH}-{SEQ:03d}', { ...base, seq: 7 }))
|
|
.toBe('ACME-AG-R-2026-0042-202608-007.pdf');
|
|
});
|
|
|
|
it('appends an index for multiple proofs only when the template has no {SEQ}', () => {
|
|
// No {SEQ} + multi → auto-suffixed with the index.
|
|
expect(renderProofName('Beleg-{INVOICE}', { ...base, seq: 2, hasMulti: true })).toBe('Beleg-R-2026-0042-2.pdf');
|
|
// Single proof → no suffix.
|
|
expect(renderProofName('Beleg-{INVOICE}', { ...base, seq: 1, hasMulti: false })).toBe('Beleg-R-2026-0042.pdf');
|
|
// Explicit {SEQ} → no double index even when multi.
|
|
expect(renderProofName('Beleg-{INVOICE}-{SEQ}', { ...base, seq: 2, hasMulti: true })).toBe('Beleg-R-2026-0042-2.pdf');
|
|
});
|
|
|
|
it('sanitises unsafe characters and slashes, and always ends in a single .pdf', () => {
|
|
expect(renderProofName('Beleg {INVOICE}', { ...base, invoiceNumber: '2026/0042' })).toBe('Beleg-2026-0042.pdf');
|
|
// Author-supplied extension is stripped and re-added (no double .pdf).
|
|
expect(renderProofName('{INVOICE}.pdf', base)).toBe('R-2026-0042.pdf');
|
|
// Falls back to 'Beleg' if the template renders empty after sanitising.
|
|
expect(renderProofName('{SUPPLIER}', { ...base, supplierName: '///' })).toBe('Beleg.pdf');
|
|
});
|
|
});
|