feat(vat)!: remove VAT entirely from offers, orders and issued orders

Accounting rule: nabidky, objednavky (incl. confirmation PDF) and objednavky
vydane are NOT tax documents - VAT belongs only on invoices. Per user
decision this is a FULL removal including DB columns.

- migration: DROP orders.vat_rate/apply_vat, issued_orders.vat_rate/apply_vat,
  issued_order_items.vat_rate, quotations.vat_rate/apply_vat (applied to
  dev + test DBs)
- services: net-only totals everywhere (computeIssuedOrderTotals(items) ->
  {total}; enrichOrder/enrichQuotation net; per-currency list totals net)
- PDFs (offers, order confirmation, issued order): no VAT columns/summary,
  single 'Celkem bez DPH' / 'Total excl. VAT' total, note under totals:
  'Ceny jsou uvedeny bez DPH. DPH bude uctovano dle platnych predpisu.';
  duplicate Cena/Celkem column merged (desc width 56%); orders-pdf render
  extracted as exported renderOrderConfirmationHtml for testability
- frontend: Uplatnit DPH checkboxes, VAT selects and per-item VAT columns
  removed from OfferDetail/OrderDetail/IssuedOrderDetail/
  OrderConfirmationModal/ReceivedOrders manual-create; list footers read
  'Celkem bez DPH'; invoice-from-order prefill now takes the company default
  VAT (invoice decides its own VAT)
- tests: suites reworked to net math; new pdf-vat-note.test.ts pins the
  exact cs+en note text and VAT-free layout on all three PDFs

Invoices and received invoices keep their VAT handling unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 13:13:16 +02:00
parent 396a1d37ec
commit 7398cad466
29 changed files with 558 additions and 1013 deletions

View File

@@ -118,8 +118,6 @@ interface OfferForm {
valid_until: string;
currency: string;
language: string;
vat_rate: number;
apply_vat: boolean;
}
const emptyForm: OfferForm = {
@@ -131,8 +129,6 @@ const emptyForm: OfferForm = {
valid_until: "",
currency: "CZK",
language: "EN",
vat_rate: 21,
apply_vat: false,
};
const emptyScopeSection = (): ScopeSection => ({
@@ -585,10 +581,6 @@ export default function OfferDetail() {
prev.currency === "CZK"
? companySettings.default_currency || "CZK"
: prev.currency,
vat_rate:
prev.vat_rate === 21
? (companySettings.default_vat_rate ?? 21)
: prev.vat_rate,
}));
}
}, [companySettings, isEdit]);
@@ -639,8 +631,6 @@ export default function OfferDetail() {
valid_until: d.valid_until ? d.valid_until.substring(0, 10) : "",
currency: d.currency || companySettings?.default_currency || "CZK",
language: d.language || "EN",
vat_rate: d.vat_rate ?? companySettings?.default_vat_rate ?? 21,
apply_vat: !!d.apply_vat,
};
setForm(formData);
const mappedItems =
@@ -790,7 +780,8 @@ export default function OfferDetail() {
setItems((prev) => prev.filter((_, i) => i !== index));
};
const subtotal = items.reduce((sum, item) => {
// NET only — offers are not tax documents (no VAT on them).
const total = items.reduce((sum, item) => {
if (item.is_included_in_total) {
return (
sum + (Number(item.quantity) || 0) * (Number(item.unit_price) || 0)
@@ -798,8 +789,6 @@ export default function OfferDetail() {
}
return sum;
}, 0);
const vatAmount = form.apply_vat ? subtotal * (form.vat_rate / 100) : 0;
const total = subtotal + vatAmount;
const handleSave = async (targetStatus?: string) => {
const newErrors: Record<string, string> = {};
@@ -1389,44 +1378,6 @@ export default function OfferDetail() {
/>
</Field>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr" },
gap: 2,
}}
>
<Field label="Sazba DPH">
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<Select
value={String(form.vat_rate)}
onChange={(val) => updateForm("vat_rate", parseFloat(val) || 0)}
disabled={readOnly}
sx={{ flex: 1 }}
options={(
companySettings?.available_vat_rates || [0, 10, 12, 15, 21]
).map((r) => ({ value: String(r), label: `${r}%` }))}
/>
<Box
component="label"
sx={{
display: "flex",
alignItems: "center",
whiteSpace: "nowrap",
cursor: readOnly ? "default" : "pointer",
}}
>
<Checkbox
checked={form.apply_vat}
onChange={(e) => updateForm("apply_vat", e.target.checked)}
disabled={readOnly}
/>
<Box component="span">Uplatnit DPH</Box>
</Box>
</Box>
</Field>
</Box>
</Card>
{/* Items Section with drag-and-drop */}
@@ -1596,7 +1547,7 @@ export default function OfferDetail() {
</SortableContext>
</DndContext>
{/* Totals */}
{/* Totals (NET only — offers are not tax documents) */}
<Box
sx={{
mt: 2,
@@ -1607,30 +1558,6 @@ export default function OfferDetail() {
gap: 0.5,
}}
>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="body2" color="text.secondary">
Mezisoučet:
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatCurrency(subtotal, form.currency)}
</Typography>
</Box>
{form.apply_vat && (
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="body2" color="text.secondary">
DPH ({form.vat_rate}%):
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatCurrency(vatAmount, form.currency)}
</Typography>
</Box>
)}
<Box
sx={{
display: "flex",
@@ -1642,7 +1569,7 @@ export default function OfferDetail() {
}}
>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
Celkem:
Celkem bez DPH:
</Typography>
<Typography
variant="body2"