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 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 12:04:07 +02:00
parent 700fb47bbc
commit f68a4dafc4
3 changed files with 81 additions and 22 deletions

View File

@@ -507,6 +507,35 @@ describe("renderIssuedOrderHtml", () => {
expect(html).not.toContain("alert(1)"); 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('<td class="right">42,00</td>');
expect(html).toContain('<td class="right total-cell">242,00</td>');
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('<td class="right">42,00</td>');
expect(html).toContain('<td class="right total-cell">200,00</td>');
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", () => { 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); const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer);
// Footer: Vystavil <name> from authData. No e-mail line. // Footer: Vystavil <name> from authData. No e-mail line.

View File

@@ -207,6 +207,7 @@ const translations: Record<Lang, Record<string, string>> = {
subtotal: "Mezisoučet:", subtotal: "Mezisoučet:",
vat_label: "DPH", vat_label: "DPH",
total: "Celkem", total: "Celkem",
total_no_vat: "Celkem bez DPH",
amounts_in: "Částky jsou uvedeny v", amounts_in: "Částky jsou uvedeny v",
notes: "Poznámky", notes: "Poznámky",
delivery_terms: "Dodací podmínky:", delivery_terms: "Dodací podmínky:",
@@ -234,6 +235,7 @@ const translations: Record<Lang, Record<string, string>> = {
subtotal: "Subtotal:", subtotal: "Subtotal:",
vat_label: "VAT", vat_label: "VAT",
total: "Total", total: "Total",
total_no_vat: "Total excl. VAT",
amounts_in: "Amounts are in", amounts_in: "Amounts are in",
notes: "Notes", notes: "Notes",
delivery_terms: "Delivery terms:", delivery_terms: "Delivery terms:",
@@ -336,14 +338,19 @@ export function renderIssuedOrderHtml(
? `<div class="item-sub">${escapeHtml(it.item_description)}</div>` ? `<div class="item-sub">${escapeHtml(it.item_description)}</div>`
: "" : ""
}`; }`;
// Without "Uplatnit DPH" the VAT columns are dropped entirely (the
// header does the same) instead of printing meaningless 0% / 0.00.
const vatCells = applyVat
? `
<td class="center">${Math.floor(rate)}%</td>
<td class="right">${formatNum(lineVat)}</td>`
: "";
return `<tr> return `<tr>
<td class="row-num">${i + 1}</td> <td class="row-num">${i + 1}</td>
<td class="desc">${descHtml}</td> <td class="desc">${descHtml}</td>
<td class="center">${formatNum(qty, qtyDecimals)}${it.unit ? ` / ${escapeHtml(it.unit)}` : ""}</td> <td class="center">${formatNum(qty, qtyDecimals)}${it.unit ? ` / ${escapeHtml(it.unit)}` : ""}</td>
<td class="right">${formatNum(unitPrice)}</td> <td class="right">${formatNum(unitPrice)}</td>
<td class="right">${formatNum(lineSubtotal)}</td> <td class="right">${formatNum(lineSubtotal)}</td>${vatCells}
<td class="center">${applyVat ? Math.floor(rate) : 0}%</td>
<td class="right">${formatNum(lineVat)}</td>
<td class="right total-cell">${formatNum(lineTotal)}</td> <td class="right total-cell">${formatNum(lineTotal)}</td>
</tr>`; </tr>`;
}) })
@@ -769,13 +776,17 @@ ${indentCSS}
<thead> <thead>
<tr> <tr>
<th class="center" style="width:3%">${escapeHtml(t.col_no)}</th> <th class="center" style="width:3%">${escapeHtml(t.col_no)}</th>
<th style="width:36%">${escapeHtml(t.col_desc)}</th> <th style="width:${applyVat ? 36 : 46}%">${escapeHtml(t.col_desc)}</th>
<th class="center" style="width:10%">${escapeHtml(t.col_qty)}</th> <th class="center" style="width:10%">${escapeHtml(t.col_qty)}</th>
<th class="right" style="width:10%">${escapeHtml(t.col_unit_price)}</th> <th class="right" style="width:10%">${escapeHtml(t.col_unit_price)}</th>
<th class="right" style="width:10%">${escapeHtml(t.col_price)}</th> <th class="right" style="width:10%">${escapeHtml(t.col_price)}</th>${
applyVat
? `
<th class="center" style="width:5%">${escapeHtml(t.col_vat_pct)}</th> <th class="center" style="width:5%">${escapeHtml(t.col_vat_pct)}</th>
<th class="right" style="width:10%">${escapeHtml(t.col_vat)}</th> <th class="right" style="width:10%">${escapeHtml(t.col_vat)}</th>`
<th class="right" style="width:16%">${escapeHtml(t.col_total)}</th> : ""
}
<th class="right" style="width:${applyVat ? 16 : 21}%">${escapeHtml(t.col_total)}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -783,17 +794,21 @@ ${indentCSS}
</tbody> </tbody>
</table> </table>
<!-- Soucty --> <!-- Soucty (bez DPH jen souhrnny radek - mezisoucet by ho jen opakoval) -->
<div class="totals-wrapper"> <div class="totals-wrapper">
<div class="totals"> <div class="totals">${
applyVat
? `
<div class="detail-rows"> <div class="detail-rows">
<div class="row"> <div class="row">
<span class="label">${escapeHtml(t.subtotal)}</span> <span class="label">${escapeHtml(t.subtotal)}</span>
<span class="value">${formatNum(subtotal)} ${escapeHtml(currency)}</span> <span class="value">${formatNum(subtotal)} ${escapeHtml(currency)}</span>
</div>${vatDetailHtml} </div>${vatDetailHtml}
</div> </div>`
: ""
}
<div class="grand"> <div class="grand">
<span class="label">${escapeHtml(t.total)}</span> <span class="label">${escapeHtml(applyVat ? t.total : t.total_no_vat)}</span>
<span class="value">${formatNum(totalToPay)} ${escapeHtml(currency)}</span> <span class="value">${formatNum(totalToPay)} ${escapeHtml(currency)}</span>
</div> </div>
<div class="currency-note">${escapeHtml(t.amounts_in)} ${escapeHtml(currency)}</div> <div class="currency-note">${escapeHtml(t.amounts_in)} ${escapeHtml(currency)}</div>

View File

@@ -200,6 +200,7 @@ const translations: Record<string, Record<string, string>> = {
subtotal: "Mezisoučet:", subtotal: "Mezisoučet:",
vat_label: "DPH", vat_label: "DPH",
total: "Celkem", total: "Celkem",
total_no_vat: "Celkem bez DPH",
amounts_in: "Částky jsou uvedeny v", amounts_in: "Částky jsou uvedeny v",
notes: "Poznámky", notes: "Poznámky",
issued_by: "Vystavil:", issued_by: "Vystavil:",
@@ -228,6 +229,7 @@ const translations: Record<string, Record<string, string>> = {
subtotal: "Subtotal:", subtotal: "Subtotal:",
vat_label: "VAT", vat_label: "VAT",
total: "Total", total: "Total",
total_no_vat: "Total excl. VAT",
amounts_in: "Amounts are in", amounts_in: "Amounts are in",
notes: "Notes", notes: "Notes",
issued_by: "Issued by:", issued_by: "Issued by:",
@@ -388,14 +390,19 @@ export default async function ordersPdfRoutes(
const lineTotal = lineSubtotal + lineVat; const lineTotal = lineSubtotal + lineVat;
const qtyDecimals = const qtyDecimals =
Math.floor(item.quantity) === item.quantity ? 0 : 2; 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
? `
<td class="center">${Math.floor(item.vat_rate)}%</td>
<td class="right">${formatNum(lineVat)}</td>`
: "";
return `<tr> return `<tr>
<td class="row-num">${i + 1}</td> <td class="row-num">${i + 1}</td>
<td class="desc">${escapeHtml(item.description)}</td> <td class="desc">${escapeHtml(item.description)}</td>
<td class="center">${formatNum(item.quantity, qtyDecimals)}${item.unit ? ` / ${escapeHtml(item.unit)}` : ""}</td> <td class="center">${formatNum(item.quantity, qtyDecimals)}${item.unit ? ` / ${escapeHtml(item.unit)}` : ""}</td>
<td class="right">${formatNum(item.unit_price)}</td> <td class="right">${formatNum(item.unit_price)}</td>
<td class="right">${formatNum(lineSubtotal)}</td> <td class="right">${formatNum(lineSubtotal)}</td>${vatCells}
<td class="center">${applyVat ? Math.floor(item.vat_rate) : 0}%</td>
<td class="right">${formatNum(lineVat)}</td>
<td class="right total-cell">${formatNum(lineTotal)}</td> <td class="right total-cell">${formatNum(lineTotal)}</td>
</tr>`; </tr>`;
}) })
@@ -848,13 +855,17 @@ ${indentCSS}
<thead> <thead>
<tr> <tr>
<th class="center" style="width:3%">${escapeHtml(t.col_no)}</th> <th class="center" style="width:3%">${escapeHtml(t.col_no)}</th>
<th style="width:36%">${escapeHtml(t.col_desc)}</th> <th style="width:${applyVat ? 36 : 46}%">${escapeHtml(t.col_desc)}</th>
<th class="center" style="width:10%">${escapeHtml(t.col_qty)}</th> <th class="center" style="width:10%">${escapeHtml(t.col_qty)}</th>
<th class="right" style="width:10%">${escapeHtml(t.col_unit_price)}</th> <th class="right" style="width:10%">${escapeHtml(t.col_unit_price)}</th>
<th class="right" style="width:10%">${escapeHtml(t.col_price)}</th> <th class="right" style="width:10%">${escapeHtml(t.col_price)}</th>${
applyVat
? `
<th class="center" style="width:5%">${escapeHtml(t.col_vat_pct)}</th> <th class="center" style="width:5%">${escapeHtml(t.col_vat_pct)}</th>
<th class="right" style="width:10%">${escapeHtml(t.col_vat)}</th> <th class="right" style="width:10%">${escapeHtml(t.col_vat)}</th>`
<th class="right" style="width:16%">${escapeHtml(t.col_total)}</th> : ""
}
<th class="right" style="width:${applyVat ? 16 : 21}%">${escapeHtml(t.col_total)}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -862,17 +873,21 @@ ${indentCSS}
</tbody> </tbody>
</table> </table>
<!-- Soucty --> <!-- Soucty (bez DPH jen souhrnny radek - mezisoucet by ho jen opakoval) -->
<div class="totals-wrapper"> <div class="totals-wrapper">
<div class="totals"> <div class="totals">${
applyVat
? `
<div class="detail-rows"> <div class="detail-rows">
<div class="row"> <div class="row">
<span class="label">${escapeHtml(t.subtotal)}</span> <span class="label">${escapeHtml(t.subtotal)}</span>
<span class="value">${formatNum(subtotal)} ${escapeHtml(currency)}</span> <span class="value">${formatNum(subtotal)} ${escapeHtml(currency)}</span>
</div>${vatDetailHtml} </div>${vatDetailHtml}
</div> </div>`
: ""
}
<div class="grand"> <div class="grand">
<span class="label">${escapeHtml(t.total)}</span> <span class="label">${escapeHtml(applyVat ? t.total : t.total_no_vat)}</span>
<span class="value">${formatNum(totalToPay)} ${escapeHtml(currency)}</span> <span class="value">${formatNum(totalToPay)} ${escapeHtml(currency)}</span>
</div> </div>
<div class="currency-note">${escapeHtml(t.amounts_in)} ${escapeHtml(currency)}</div> <div class="currency-note">${escapeHtml(t.amounts_in)} ${escapeHtml(currency)}</div>