From 2205b0bd687587ff2b54fbddb4770bf69a5ef3b2 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:49:50 +0200 Subject: [PATCH] fix(pdf): correct multi-page invoice/quote layout + drop IBAN dup under Swiss QR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before drawing the line-items table, the renderer inflated page 1's bottom margin to reserve room for the bottom-pinned totals block, but the `finally` restored it on whichever page the table *ended* on — leaving page 1 permanently short on any multi-page document. On long invoices and quotes this caused: - the table to break far too early (only ~6 items on page 1, large blank gap beneath) - the page-number stamp to land below page 1's phantom bottom margin, auto-paginating a stray blank trailing page and desyncing the "Seite X von Y" labels (page 1 unnumbered, the blank page labelled "Seite 1 von N") Let the table paginate with the document's normal margins so each page fills to the bottom; the existing desiredTotalsY check already advances to a fresh page when the last item row would collide with the pinned totals block. Also suppress the IBAN block under the totals when a Swiss QR-bill slip is appended: the slip already prints the account/IBAN in human-readable form, so it was pure duplication. The EPC QR path keeps the block (its QR lives on a trailing page, so on-page bank details still help). --- backend/src/services/pdfService.js | 51 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/backend/src/services/pdfService.js b/backend/src/services/pdfService.js index c12d607b..613f386f 100644 --- a/backend/src/services/pdfService.js +++ b/backend/src/services/pdfService.js @@ -916,7 +916,14 @@ function drawPaymentBlock(doc, ctx, x, y, width) { const showSkontoHere = isQuote ? (issuer?.quoteShowSkonto !== false) : reminderLevel === 0; - const showIbanHere = !isQuote; + // Invoices show the IBAN block in the right column EXCEPT when a + // Swiss QR-bill slip is appended: that slip already prints the + // account/IBAN ("Konto / Zahlbar an") in human-readable form, so + // repeating "Der Betrag ist auf die folgende Bankverbindung zu + // überweisen: …" under the totals is pure duplication. The EPC QR + // path keeps the block — its QR lives on a trailing page, so having + // the bank details on the invoice page itself still helps. + const showIbanHere = !isQuote && ctx.qrFormat !== 'swiss'; // If the quote has nothing to print in either column, bail out // early — don't render a bare "Payment conditions:" header with @@ -1610,29 +1617,25 @@ function renderDocument(type, context) { doc.y = y; doc.x = leftX; - // Force the items table to auto-paginate BEFORE it can collide - // with the totals + payment block at the page bottom. We - // compute the same anchor as below, then temporarily inflate - // the page's bottom margin so swissqrbill's Table sees a - // shorter usable area and breaks to a new page when items - // would otherwise spill into the totals zone. The header row - // is already marked `header: true` so it auto-repeats on the - // continuation page. - const _origBottomMargin = doc.page.margins.bottom; - const _itemsBottomReserve = PAGE.marginBottom - + 30 // FOOTER_RESERVE - + (ctx.paymentTerm ? 80 : 50) // PAYMENT_BLOCK_HEIGHT - + 12 // gap between totals + payment - + 90 // TOTALS_BLOCK_HEIGHT - + 20; // small breathing room - doc.page.margins.bottom = _itemsBottomReserve; - try { - drawLineItems(doc, ctx); - } finally { - // Restore even if drawLineItems threw — keeps subsequent - // pages on the document's normal margin geometry. - doc.page.margins.bottom = _origBottomMargin; - } + // Let the items table paginate with the document's NORMAL + // margins so each page fills to the bottom. The header row is + // marked `header: true` so it auto-repeats on every + // continuation page. Totals/payment placement is handled below: + // they're pinned to a fixed anchor near the page bottom, and if + // the last item row spilled past that anchor we advance to a + // fresh page before drawing them (see the desiredTotalsY check). + // + // We deliberately do NOT inflate the bottom margin here to + // "reserve" the totals zone on every page. That older approach + // shortened the usable area on EVERY page (not just the last), + // so a long invoice broke far too early — only a handful of + // line items rendered on page 1 with a large blank gap beneath. + // Worse, the inflated margin was set on the page active when the + // table started but restored on whichever page the table ended, + // leaving page 1 permanently short: the page-number stamp later + // landed below that page's phantom bottom margin and spawned a + // stray blank trailing page (which then desynced "Seite X von Y"). + drawLineItems(doc, ctx); // y after the table — used only to detect whether the items // overflowed past the totals anchor below. We don't use it as // the totals position directly because the totals block is