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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user