From f68a4dafc4beafcdbbdf83a3965556574584be3d Mon Sep 17 00:00:00 2001 From: BOHA Date: Wed, 10 Jun 2026 12:04:07 +0200 Subject: [PATCH] feat(orders-pdf): without 'Uplatnit DPH' hide VAT columns, total reads 'Celkem bez DPH' Applies to both the issued-order (PO) PDF and the order-confirmation PDF: when apply_vat is off the %DPH and DPH columns are dropped entirely (widths redistributed) instead of printing 0% / 0,00, the redundant Mezisoucet row is omitted, and the grand total is labeled 'Celkem bez DPH' / 'Total excl. VAT'. The per-line DPH cell already contained only the line VAT (never netto+VAT) - now pinned by a regression test (2x100 @ 21%: DPH cell 42,00, Celkem cell 242,00). Co-Authored-By: Claude Fable 5 --- src/__tests__/issued-orders.test.ts | 29 +++++++++++++++++++++ src/routes/admin/issued-orders-pdf.ts | 37 +++++++++++++++++++-------- src/routes/admin/orders-pdf.ts | 37 +++++++++++++++++++-------- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/src/__tests__/issued-orders.test.ts b/src/__tests__/issued-orders.test.ts index 023e18e..78c75a2 100644 --- a/src/__tests__/issued-orders.test.ts +++ b/src/__tests__/issued-orders.test.ts @@ -507,6 +507,35 @@ describe("renderIssuedOrderHtml", () => { expect(html).not.toContain("alert(1)"); }); + it("with VAT: keeps the VAT columns and the DPH cell holds ONLY the line VAT", () => { + const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer); + expect(html).toContain("%DPH"); + expect(html).toContain(">DPH<"); + // 2 × 100 @ 21 %: DPH cell = 42,00 (VAT only), Celkem cell = 242,00. + expect(html).toContain('42,00'); + expect(html).toContain('242,00'); + expect(html).not.toContain("Celkem bez DPH"); + }); + + it("without VAT: hides the VAT columns and labels the total 'Celkem bez DPH'", () => { + const html = renderIssuedOrderHtml( + { ...order, apply_vat: false }, + items, + null, + null, + "cs", + issuer, + ); + expect(html).not.toContain("%DPH"); + expect(html).not.toContain(">DPH<"); + // No per-line VAT cell, line total = netto. + expect(html).not.toContain('42,00'); + expect(html).toContain('200,00'); + expect(html).toContain("Celkem bez DPH"); + // The subtotal detail row is dropped (it would duplicate the grand total). + expect(html).not.toContain("Mezisoučet"); + }); + it("footer shows the logged-in user's name, no e-mail, no Schválil column", () => { const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer); // Footer: Vystavil from authData. No e-mail line. diff --git a/src/routes/admin/issued-orders-pdf.ts b/src/routes/admin/issued-orders-pdf.ts index 9250b30..39e3d58 100644 --- a/src/routes/admin/issued-orders-pdf.ts +++ b/src/routes/admin/issued-orders-pdf.ts @@ -207,6 +207,7 @@ const translations: Record> = { subtotal: "Mezisoučet:", vat_label: "DPH", total: "Celkem", + total_no_vat: "Celkem bez DPH", amounts_in: "Částky jsou uvedeny v", notes: "Poznámky", delivery_terms: "Dodací podmínky:", @@ -234,6 +235,7 @@ const translations: Record> = { subtotal: "Subtotal:", vat_label: "VAT", total: "Total", + total_no_vat: "Total excl. VAT", amounts_in: "Amounts are in", notes: "Notes", delivery_terms: "Delivery terms:", @@ -336,14 +338,19 @@ export function renderIssuedOrderHtml( ? `
${escapeHtml(it.item_description)}
` : "" }`; + // Without "Uplatnit DPH" the VAT columns are dropped entirely (the + // header does the same) instead of printing meaningless 0% / 0.00. + const vatCells = applyVat + ? ` + ${Math.floor(rate)}% + ${formatNum(lineVat)}` + : ""; return ` ${i + 1} ${descHtml} ${formatNum(qty, qtyDecimals)}${it.unit ? ` / ${escapeHtml(it.unit)}` : ""} ${formatNum(unitPrice)} - ${formatNum(lineSubtotal)} - ${applyVat ? Math.floor(rate) : 0}% - ${formatNum(lineVat)} + ${formatNum(lineSubtotal)}${vatCells} ${formatNum(lineTotal)} `; }) @@ -769,13 +776,17 @@ ${indentCSS} ${escapeHtml(t.col_no)} - ${escapeHtml(t.col_desc)} + ${escapeHtml(t.col_desc)} ${escapeHtml(t.col_qty)} ${escapeHtml(t.col_unit_price)} - ${escapeHtml(t.col_price)} + ${escapeHtml(t.col_price)}${ + applyVat + ? ` ${escapeHtml(t.col_vat_pct)} - ${escapeHtml(t.col_vat)} - ${escapeHtml(t.col_total)} + ${escapeHtml(t.col_vat)}` + : "" + } + ${escapeHtml(t.col_total)} @@ -783,17 +794,21 @@ ${indentCSS} - +
-
+
${ + applyVat + ? `
${escapeHtml(t.subtotal)} ${formatNum(subtotal)} ${escapeHtml(currency)}
${vatDetailHtml} -
+
` + : "" + }
- ${escapeHtml(t.total)} + ${escapeHtml(applyVat ? t.total : t.total_no_vat)} ${formatNum(totalToPay)} ${escapeHtml(currency)}
${escapeHtml(t.amounts_in)} ${escapeHtml(currency)}
diff --git a/src/routes/admin/orders-pdf.ts b/src/routes/admin/orders-pdf.ts index d8a7ea1..ebbac4c 100644 --- a/src/routes/admin/orders-pdf.ts +++ b/src/routes/admin/orders-pdf.ts @@ -200,6 +200,7 @@ const translations: Record> = { subtotal: "Mezisoučet:", vat_label: "DPH", total: "Celkem", + total_no_vat: "Celkem bez DPH", amounts_in: "Částky jsou uvedeny v", notes: "Poznámky", issued_by: "Vystavil:", @@ -228,6 +229,7 @@ const translations: Record> = { subtotal: "Subtotal:", vat_label: "VAT", total: "Total", + total_no_vat: "Total excl. VAT", amounts_in: "Amounts are in", notes: "Notes", issued_by: "Issued by:", @@ -388,14 +390,19 @@ export default async function ordersPdfRoutes( const lineTotal = lineSubtotal + lineVat; const qtyDecimals = Math.floor(item.quantity) === item.quantity ? 0 : 2; + // Without "Uplatnit DPH" the VAT columns are dropped entirely + // (the header does the same) instead of printing 0% / 0.00. + const vatCells = applyVat + ? ` + ${Math.floor(item.vat_rate)}% + ${formatNum(lineVat)}` + : ""; return ` ${i + 1} ${escapeHtml(item.description)} ${formatNum(item.quantity, qtyDecimals)}${item.unit ? ` / ${escapeHtml(item.unit)}` : ""} ${formatNum(item.unit_price)} - ${formatNum(lineSubtotal)} - ${applyVat ? Math.floor(item.vat_rate) : 0}% - ${formatNum(lineVat)} + ${formatNum(lineSubtotal)}${vatCells} ${formatNum(lineTotal)} `; }) @@ -848,13 +855,17 @@ ${indentCSS} ${escapeHtml(t.col_no)} - ${escapeHtml(t.col_desc)} + ${escapeHtml(t.col_desc)} ${escapeHtml(t.col_qty)} ${escapeHtml(t.col_unit_price)} - ${escapeHtml(t.col_price)} + ${escapeHtml(t.col_price)}${ + applyVat + ? ` ${escapeHtml(t.col_vat_pct)} - ${escapeHtml(t.col_vat)} - ${escapeHtml(t.col_total)} + ${escapeHtml(t.col_vat)}` + : "" + } + ${escapeHtml(t.col_total)} @@ -862,17 +873,21 @@ ${indentCSS} - +
-
+
${ + applyVat + ? `
${escapeHtml(t.subtotal)} ${formatNum(subtotal)} ${escapeHtml(currency)}
${vatDetailHtml} -
+
` + : "" + }
- ${escapeHtml(t.total)} + ${escapeHtml(applyVat ? t.total : t.total_no_vat)} ${formatNum(totalToPay)} ${escapeHtml(currency)}
${escapeHtml(t.amounts_in)} ${escapeHtml(currency)}