fix(pdf): correct multi-page invoice/quote layout + drop IBAN dup under Swiss QR

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).
This commit is contained in:
Luca
2026-06-28 14:49:50 +02:00
parent e6f29b6345
commit 2205b0bd68
+27 -24
View File
@@ -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