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

@@ -7,8 +7,8 @@ import { useTheme } from "@mui/material/styles";
import { Modal, Button, TextField, Field } from "../ui";
import { useAlert } from "../context/AlertContext";
// Editable line-item. quantity/unit_price/vat_rate are held as the raw typed
// string while editing (so a field can be cleared — empty renders fine in a
// Editable line-item. quantity/unit_price are held as the raw typed string
// while editing (so a field can be cleared — empty renders fine in a
// type="number" input) and are coerced to numbers in handleEditGenerate before
// being handed to onGenerate (the parent posts them verbatim).
interface ConfirmationItem {
@@ -17,7 +17,6 @@ interface ConfirmationItem {
unit: string;
unit_price: string | number;
is_included_in_total: boolean;
vat_rate: string | number;
}
// Numeric shape handed to the parent (the raw editing strings are coerced in
@@ -28,21 +27,14 @@ interface GeneratedItem {
unit: string;
unit_price: number;
is_included_in_total: boolean;
vat_rate: number;
}
interface OrderConfirmationModalProps {
isOpen: boolean;
onClose: () => void;
onGenerate: (
lang: string,
applyVat: boolean,
items?: GeneratedItem[],
) => Promise<void>;
onGenerate: (lang: string, items?: GeneratedItem[]) => Promise<void>;
initialItems: ConfirmationItem[];
orderNumber: string;
defaultVatRate: number;
applyVat: boolean;
}
export default function OrderConfirmationModal({
@@ -51,15 +43,12 @@ export default function OrderConfirmationModal({
onGenerate,
initialItems,
orderNumber,
defaultVatRate,
applyVat,
}: OrderConfirmationModalProps) {
const alert = useAlert();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [step, setStep] = useState<"choose" | "edit">("choose");
const [lang, setLang] = useState<string>("cs");
const [applyVatState, setApplyVatState] = useState(applyVat);
const [items, setItems] = useState<ConfirmationItem[]>(initialItems);
const [loading, setLoading] = useState(false);
@@ -72,17 +61,16 @@ export default function OrderConfirmationModal({
if (!isOpen) return;
setStep("choose");
setLang("cs");
setApplyVatState(applyVat);
setItems(initialItems);
// initialItems/applyVat are captured at open time; intentionally not in the
// dep array so an unrelated parent re-render doesn't clobber the user's edits.
// initialItems are captured at open time; intentionally not in the dep
// array so an unrelated parent re-render doesn't clobber the user's edits.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
const handleUseExisting = async () => {
setLoading(true);
try {
await onGenerate(lang, applyVatState, undefined);
await onGenerate(lang, undefined);
// Only close on success — a generation error must keep the modal open.
onClose();
} catch (err) {
@@ -104,9 +92,8 @@ export default function OrderConfirmationModal({
unit: it.unit,
unit_price: Number(it.unit_price) || 0,
is_included_in_total: it.is_included_in_total,
vat_rate: Number(it.vat_rate) || 0,
}));
await onGenerate(lang, applyVatState, coercedItems);
await onGenerate(lang, coercedItems);
// Only close on success — on error keep the user's edited items intact.
onClose();
} catch (err) {
@@ -145,10 +132,9 @@ export default function OrderConfirmationModal({
unit: "ks",
unit_price: 0,
is_included_in_total: true,
vat_rate: defaultVatRate,
},
]);
}, [defaultVatRate]);
}, []);
return (
<Modal
@@ -186,27 +172,6 @@ export default function OrderConfirmationModal({
</Box>
</Field>
<Field label="DPH">
<Box sx={{ display: "flex", gap: 1 }}>
<Button
size="small"
variant={applyVatState ? "contained" : "outlined"}
color={applyVatState ? "primary" : "inherit"}
onClick={() => setApplyVatState(true)}
>
S DPH
</Button>
<Button
size="small"
variant={!applyVatState ? "contained" : "outlined"}
color={!applyVatState ? "primary" : "inherit"}
onClick={() => setApplyVatState(false)}
>
Bez DPH
</Button>
</Box>
</Field>
<Field label="Obsah potvrzení">
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
Jak chcete připravit potvrzení objednávky?
@@ -291,7 +256,7 @@ export default function OrderConfirmationModal({
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: 1,
}}
>
@@ -318,15 +283,6 @@ export default function OrderConfirmationModal({
}
slotProps={{ htmlInput: { step: "0.01" } }}
/>
<TextField
label="%DPH"
type="number"
value={item.vat_rate ?? ""}
onChange={(e) =>
updateItem(i, "vat_rate", e.target.value)
}
slotProps={{ htmlInput: { step: "1" } }}
/>
</Box>
</Box>
))}
@@ -356,7 +312,6 @@ export default function OrderConfirmationModal({
<th>Mn.</th>
<th>Jedn.</th>
<th>Cena</th>
<th>%DPH</th>
<th style={{ width: "40px" }} />
</tr>
</thead>
@@ -403,17 +358,6 @@ export default function OrderConfirmationModal({
slotProps={{ htmlInput: { step: "0.01" } }}
/>
</td>
<td>
<TextField
type="number"
value={item.vat_rate ?? ""}
onChange={(e) =>
updateItem(i, "vat_rate", e.target.value)
}
sx={{ width: 80 }}
slotProps={{ htmlInput: { step: "1" } }}
/>
</td>
<td>
<IconButton
size="small"

View File

@@ -9,8 +9,7 @@ export interface IssuedOrder {
status: string;
currency: string | null;
order_date: string | null;
subtotal: number;
vat_amount: number;
// NET total — issued orders carry no VAT (not tax documents).
total: number;
}
@@ -32,13 +31,10 @@ export interface IssuedOrderItem {
quantity: number | string | null;
unit: string | null;
unit_price: number | string | null;
vat_rate: number | string | null;
position?: number;
}
export interface IssuedOrderDetail extends IssuedOrder {
apply_vat: boolean | null;
vat_rate: number | string | null;
exchange_rate: number | string | null;
delivery_date: string | null;
language: string | null;

View File

@@ -157,8 +157,6 @@ export interface OfferDetailData {
valid_until: string;
currency: string;
language: string;
vat_rate: number;
apply_vat: boolean;
items?: OfferItemData[];
sections?: OfferSectionData[];
status: string;

View File

@@ -43,8 +43,6 @@ export interface OrderData {
status: string;
notes: string;
attachment_name?: string;
apply_vat: number | boolean;
vat_rate: number;
language?: string;
items: OrderItem[];
sections: OrderSection[];

View File

@@ -787,13 +787,12 @@ export default function InvoiceDetail() {
}));
}
// Pre-fill from order
// Pre-fill from order. Orders no longer carry VAT (not tax documents) —
// the invoice decides its own VAT: default rate from company settings,
// apply_vat on.
if (fromOrderId && orderDataQuery.data) {
const order = orderDataQuery.data;
const vatRate = numberOr(
order.vat_rate,
companySettings?.default_vat_rate ?? 21,
);
const vatRate = numberOr(companySettings?.default_vat_rate, 21);
setForm((prev) => ({
...prev,
customer_id: order.customer_id as number,
@@ -803,7 +802,7 @@ export default function InvoiceDetail() {
(order.currency as string) ||
companySettings?.default_currency ||
"CZK",
apply_vat: Number(order.apply_vat) || 0,
apply_vat: 1,
vat_rate: vatRate,
}));
const orderItems = order.items as Record<string, unknown>[] | undefined;

View File

@@ -57,7 +57,6 @@ import {
DateField,
Field,
StatusChip,
CheckboxField,
ConfirmDialog,
LoadingState,
PageEnter,
@@ -85,11 +84,6 @@ const TRANSITION_LABELS: Record<string, string> = {
cancelled: "Stornovat",
};
const VAT_OPTIONS = [0, 10, 12, 15, 21].map((v) => ({
value: String(v),
label: `${v}%`,
}));
const CURRENCY_FALLBACK = ["CZK", "EUR", "USD", "GBP"];
const BackIcon = (
@@ -151,20 +145,16 @@ interface OrderItem {
item_description: string;
// Held as the raw typed string while editing so the field can be cleared
// (empty renders fine in a type="number" input). Coerced via Number(x) || 0
// only where used (live totals + save payload). vat_rate stays numeric: it
// is edited via a Select, never a free-text number input.
// only where used (live totals + save payload).
quantity: string | number;
unit: string;
unit_price: string | number;
vat_rate: number;
}
interface OrderForm {
supplier_id: number | null;
supplier_name: string;
currency: string;
apply_vat: boolean;
vat_rate: number;
order_date: string;
delivery_date: string;
language: string;
@@ -182,7 +172,6 @@ function SortableOrderRow({
item,
index,
currency,
apply_vat,
readOnly,
onUpdate,
onRemove,
@@ -191,7 +180,6 @@ function SortableOrderRow({
item: OrderItem;
index: number;
currency: string;
apply_vat: boolean;
readOnly: boolean;
onUpdate: (
index: number,
@@ -290,7 +278,7 @@ function SortableOrderRow({
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: 1,
}}
>
@@ -317,21 +305,6 @@ function SortableOrderRow({
slotProps={{ htmlInput: { step: "any" } }}
InputProps={{ readOnly }}
/>
{apply_vat &&
(readOnly ? (
<TextField
label="DPH"
value={`${Number(item.vat_rate)}%`}
InputProps={{ readOnly: true }}
/>
) : (
<Select
label="DPH"
value={String(item.vat_rate)}
onChange={(val) => onUpdate(index, "vat_rate", Number(val))}
options={VAT_OPTIONS}
/>
))}
</Box>
<Box
sx={{
@@ -436,20 +409,6 @@ function SortableOrderRow({
sx={{ "& input": { textAlign: "right" } }}
/>
</TableCell>
{apply_vat ? (
<TableCell>
{readOnly ? (
<Box sx={{ textAlign: "center" }}>{Number(item.vat_rate)}%</Box>
) : (
<Select
value={String(item.vat_rate)}
onChange={(val) => onUpdate(index, "vat_rate", Number(val))}
sx={{ minWidth: "4.5rem" }}
options={VAT_OPTIONS}
/>
)}
</TableCell>
) : null}
<TableCell
align="right"
sx={{
@@ -496,7 +455,6 @@ export default function IssuedOrderDetail() {
quantity: 1,
unit: "ks",
unit_price: 0,
vat_rate: 21,
}),
[],
);
@@ -513,8 +471,6 @@ export default function IssuedOrderDetail() {
supplier_id: null,
supplier_name: "",
currency: "CZK",
apply_vat: true,
vat_rate: 21,
order_date: todayLocalStr(),
delivery_date: "",
language: "cs",
@@ -534,7 +490,6 @@ export default function IssuedOrderDetail() {
quantity: 1,
unit: "ks",
unit_price: 0,
vat_rate: 21,
},
]);
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -615,8 +570,6 @@ export default function IssuedOrderDetail() {
supplier_id: d.supplier_id ?? null,
supplier_name: d.supplier_name ?? "",
currency: d.currency || "CZK",
apply_vat: d.apply_vat !== false,
vat_rate: numberOr(d.vat_rate, 21),
order_date: normalizeDateStr(d.order_date),
delivery_date: normalizeDateStr(d.delivery_date),
language: d.language || "cs",
@@ -640,7 +593,6 @@ export default function IssuedOrderDetail() {
quantity: numberOr(it.quantity, 1),
unit: it.unit || "",
unit_price: Number(it.unit_price) || 0,
vat_rate: numberOr(it.vat_rate, numberOr(d.vat_rate, 21)),
}))
: [];
if (mapped.length > 0) setItems(mapped);
@@ -683,21 +635,14 @@ export default function IssuedOrderDetail() {
const editable = !isEdit || form.status === "draft" || form.status === "sent";
const canExport = hasPermission("orders.view");
// ─── Totals (live) ───
// ─── Totals (live, NET only — issued orders carry no VAT) ───
const totals = useMemo(() => {
let subtotal = 0;
const vatByRate: Record<number, number> = {};
let total = 0;
items.forEach((it) => {
const line = (Number(it.quantity) || 0) * (Number(it.unit_price) || 0);
subtotal += line;
if (form.apply_vat) {
const rate = Number(it.vat_rate) || 0;
vatByRate[rate] = (vatByRate[rate] || 0) + (line * rate) / 100;
}
total += (Number(it.quantity) || 0) * (Number(it.unit_price) || 0);
});
const totalVat = Object.values(vatByRate).reduce((s, v) => s + v, 0);
return { subtotal, vatByRate, totalVat, total: subtotal + totalVat };
}, [items, form.apply_vat]);
return { total };
}, [items]);
// ─── Mutations ───
const saveMutation = useApiMutation<Record<string, unknown>, { id: number }>({
@@ -774,8 +719,6 @@ export default function IssuedOrderDetail() {
const payload: Record<string, unknown> = {
supplier_id: form.supplier_id,
currency: form.currency,
vat_rate: form.vat_rate,
apply_vat: form.apply_vat,
order_date: form.order_date,
delivery_date: form.delivery_date || null,
language: form.language,
@@ -795,7 +738,6 @@ export default function IssuedOrderDetail() {
quantity: Number(it.quantity) || 0,
unit: it.unit,
unit_price: Number(it.unit_price) || 0,
vat_rate: it.vat_rate,
position: i,
})),
};
@@ -1175,7 +1117,7 @@ export default function IssuedOrderDetail() {
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr 1fr" },
gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" },
gap: 2,
}}
>
@@ -1202,31 +1144,6 @@ export default function IssuedOrderDetail() {
]}
/>
</Field>
<Field label="Sazba DPH">
<Select
value={String(form.vat_rate)}
disabled={!editable || !form.apply_vat}
onChange={(val) =>
setForm((prev) => ({ ...prev, vat_rate: Number(val) }))
}
options={VAT_OPTIONS}
/>
</Field>
<Field label="DPH">
<Box sx={{ display: "flex", alignItems: "center", height: 40 }}>
<CheckboxField
label="Uplatnit DPH"
checked={form.apply_vat}
disabled={!editable}
onChange={(v) =>
setForm((prev) => ({
...prev,
apply_vat: v,
}))
}
/>
</Box>
</Field>
</Box>
<Field label="Vystavil">
@@ -1291,7 +1208,6 @@ export default function IssuedOrderDetail() {
item={item}
index={index}
currency={form.currency}
apply_vat={form.apply_vat}
readOnly={!editable}
onUpdate={updateItem}
onRemove={removeItem}
@@ -1324,11 +1240,6 @@ export default function IssuedOrderDetail() {
<TableCell sx={{ width: "8rem" }} align="center">
Jedn. cena
</TableCell>
{form.apply_vat ? (
<TableCell sx={{ width: "5rem" }} align="center">
DPH
</TableCell>
) : null}
<TableCell sx={{ width: "8rem" }} align="right">
Celkem
</TableCell>
@@ -1342,7 +1253,6 @@ export default function IssuedOrderDetail() {
item={item}
index={index}
currency={form.currency}
apply_vat={form.apply_vat}
readOnly={!editable}
onUpdate={updateItem}
onRemove={removeItem}
@@ -1356,7 +1266,7 @@ export default function IssuedOrderDetail() {
</SortableContext>
</DndContext>
{/* Totals */}
{/* Totals (NET only — issued orders are not tax documents) */}
<Box
sx={{
mt: 2,
@@ -1367,34 +1277,6 @@ export default function IssuedOrderDetail() {
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, form.currency)}
</Typography>
</Box>
{form.apply_vat &&
Object.entries(totals.vatByRate).map(([rate, amount]) => (
<Box
key={rate}
sx={{ display: "flex", justifyContent: "space-between" }}
>
<Typography variant="body2" color="text.secondary">
DPH {rate}%:
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatCurrency(amount, form.currency)}
</Typography>
</Box>
))}
<Box
sx={{
display: "flex",
@@ -1406,7 +1288,7 @@ export default function IssuedOrderDetail() {
}}
>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
Celkem:
Celkem bez DPH:
</Typography>
<Typography
variant="body2"

View File

@@ -392,7 +392,7 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
fontSize: "0.9rem",
}}
>
<span>Celkem:</span>
<span>Celkem bez DPH:</span>
<Box
component="span"
sx={{ fontWeight: 700, color: "text.primary" }}

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"

View File

@@ -906,7 +906,7 @@ export default function Offers() {
fontSize: "0.9rem",
}}
>
<span>Celkem:</span>
<span>Celkem bez DPH:</span>
<Box
component="span"
sx={{ fontWeight: 700, color: "text.primary" }}

View File

@@ -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>

View File

@@ -184,8 +184,6 @@ export default function OrdersReceived({
customer_id: "",
customer_order_number: "",
currency: "CZK",
vat_rate: "21",
apply_vat: true,
scope_title: "",
scope_description: "",
notes: "",
@@ -219,8 +217,6 @@ export default function OrdersReceived({
customer_id: "",
customer_order_number: "",
currency: "CZK",
vat_rate: "21",
apply_vat: true,
scope_title: "",
scope_description: "",
notes: "",
@@ -255,8 +251,6 @@ export default function OrdersReceived({
fd.append("customer_id", createForm.customer_id);
fd.append("customer_order_number", createForm.customer_order_number);
fd.append("currency", createForm.currency);
fd.append("vat_rate", createForm.vat_rate);
fd.append("apply_vat", createForm.apply_vat ? "1" : "0");
fd.append("scope_title", createForm.scope_title);
fd.append("scope_description", createForm.scope_description);
fd.append("notes", createForm.notes);
@@ -274,8 +268,6 @@ export default function OrdersReceived({
: null,
customer_order_number: createForm.customer_order_number,
currency: createForm.currency,
vat_rate: createForm.vat_rate,
apply_vat: createForm.apply_vat,
scope_title: createForm.scope_title,
scope_description: createForm.scope_description,
notes: createForm.notes,
@@ -571,7 +563,7 @@ export default function OrdersReceived({
fontSize: "0.9rem",
}}
>
<span>Celkem:</span>
<span>Celkem bez DPH:</span>
<Box
component="span"
sx={{ fontWeight: 700, color: "text.primary" }}
@@ -669,19 +661,6 @@ export default function OrdersReceived({
/>
</Field>
</Box>
<Box sx={{ flex: "1 1 200px" }}>
<Field label="Sazba DPH (%)">
<TextField
type="number"
inputMode="numeric"
value={createForm.vat_rate}
onChange={(e) =>
setCreateForm({ ...createForm, vat_rate: e.target.value })
}
slotProps={{ htmlInput: { min: 0 } }}
/>
</Field>
</Box>
</Box>
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
@@ -757,12 +736,6 @@ export default function OrdersReceived({
/>
</Field>
<CheckboxField
label="Účtovat DPH"
checked={createForm.apply_vat}
onChange={(v) => setCreateForm({ ...createForm, apply_vat: v })}
/>
<CheckboxField
label="Vytvořit propojený projekt"
checked={createForm.create_project}