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:
@@ -144,9 +144,10 @@ export default function OrderDetail() {
|
||||
return () => window.removeEventListener("beforeunload", handler);
|
||||
}, [isDirty]);
|
||||
|
||||
// NET only — received orders are not tax documents (no VAT on them).
|
||||
const totals = useMemo(() => {
|
||||
if (!order?.items) return { subtotal: 0, vatAmount: 0, total: 0 };
|
||||
const subtotal = order.items.reduce((sum, item) => {
|
||||
if (!order?.items) return { total: 0 };
|
||||
const total = order.items.reduce((sum, item) => {
|
||||
if (Number(item.is_included_in_total)) {
|
||||
return (
|
||||
sum + (Number(item.quantity) || 0) * (Number(item.unit_price) || 0)
|
||||
@@ -154,10 +155,7 @@ export default function OrderDetail() {
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
const vatAmount = Number(order.apply_vat)
|
||||
? subtotal * ((Number(order.vat_rate) || 0) / 100)
|
||||
: 0;
|
||||
return { subtotal, vatAmount, total: subtotal + vatAmount };
|
||||
return { total };
|
||||
}, [order]);
|
||||
|
||||
const statusMutation = useApiMutation<{ status: string }, unknown>({
|
||||
@@ -236,14 +234,12 @@ export default function OrderDetail() {
|
||||
|
||||
const handleGenerateConfirmation = async (
|
||||
lang: string,
|
||||
applyVat: boolean,
|
||||
customItems?: Array<{
|
||||
description: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
unit_price: number;
|
||||
is_included_in_total: boolean;
|
||||
vat_rate: number;
|
||||
}>,
|
||||
) => {
|
||||
setConfirmationLoading(true);
|
||||
@@ -253,7 +249,7 @@ export default function OrderDetail() {
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ lang, applyVat, items: customItems }),
|
||||
body: JSON.stringify({ lang, items: customItems }),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
@@ -625,7 +621,7 @@ export default function OrderDetail() {
|
||||
empty={<EmptyState title="Žádné položky." />}
|
||||
/>
|
||||
|
||||
{/* Totals */}
|
||||
{/* Totals (NET only — orders are not tax documents) */}
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
@@ -636,30 +632,6 @@ export default function OrderDetail() {
|
||||
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(totals.subtotal, order.currency)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{Number(order.apply_vat) > 0 && (
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
DPH ({order.vat_rate}%):
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
|
||||
>
|
||||
{formatCurrency(totals.vatAmount, order.currency)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -671,7 +643,7 @@ export default function OrderDetail() {
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
Celkem k úhradě:
|
||||
Celkem bez DPH:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
@@ -829,11 +801,8 @@ export default function OrderDetail() {
|
||||
unit: it.unit || "",
|
||||
unit_price: Number(it.unit_price) || 0,
|
||||
is_included_in_total: Number(it.is_included_in_total) !== 0,
|
||||
vat_rate: Number(order.vat_rate) || 21,
|
||||
}))}
|
||||
orderNumber={order.order_number}
|
||||
defaultVatRate={Number(order.vat_rate) || 21}
|
||||
applyVat={!!order.apply_vat}
|
||||
/>
|
||||
)}
|
||||
</PageEnter>
|
||||
|
||||
Reference in New Issue
Block a user