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:
@@ -54,18 +54,16 @@ function amountFor(
|
||||
}
|
||||
|
||||
describe("getOfferTotals per-currency aggregation", () => {
|
||||
it("sums offer TOTAL incl. VAT per currency over the full filtered set", async () => {
|
||||
// Two CZK offers + one EUR. 21% VAT applied on the whole subtotal
|
||||
// (enrichQuotation math).
|
||||
// CZK #1: 2 x 1000 = 2000 net -> +21% = 2420
|
||||
// CZK #2: 1 x 500 = 500 net -> +21% = 605 => CZK total 3025
|
||||
// EUR : 3 x 100 = 300 net -> +21% = 363 => EUR total 363
|
||||
it("sums offer NET total per currency over the full filtered set", async () => {
|
||||
// Two CZK offers + one EUR. NET only (enrichQuotation math — offers are
|
||||
// not tax documents, no VAT anywhere).
|
||||
// CZK #1: 2 x 1000 = 2000
|
||||
// CZK #2: 1 x 500 = 500 => CZK total 2500
|
||||
// EUR : 3 x 100 = 300 => EUR total 300
|
||||
const mk = async (currency: string, qty: number, price: number) => {
|
||||
const res = await createOffer({
|
||||
status: "draft", // draft so no offer number is consumed
|
||||
currency,
|
||||
vat_rate: 21,
|
||||
apply_vat: true,
|
||||
project_code: OFFER_MARKER,
|
||||
items: [{ description: "X", quantity: qty, unit_price: price }],
|
||||
});
|
||||
@@ -79,8 +77,8 @@ describe("getOfferTotals per-currency aggregation", () => {
|
||||
await mk("EUR", 3, 100);
|
||||
|
||||
const { totals } = await getOfferTotals({ search: OFFER_MARKER });
|
||||
expect(amountFor(totals, "CZK")).toBe(3025);
|
||||
expect(amountFor(totals, "EUR")).toBe(363);
|
||||
expect(amountFor(totals, "CZK")).toBe(2500);
|
||||
expect(amountFor(totals, "EUR")).toBe(300);
|
||||
});
|
||||
|
||||
it("respects the where: a status filter narrows the result", async () => {
|
||||
@@ -88,8 +86,6 @@ describe("getOfferTotals per-currency aggregation", () => {
|
||||
const res = await createOffer({
|
||||
status,
|
||||
currency: "CZK",
|
||||
vat_rate: 21,
|
||||
apply_vat: true,
|
||||
project_code: OFFER_MARKER,
|
||||
items: [{ description: "X", quantity: 1, unit_price: 1000 }],
|
||||
});
|
||||
@@ -103,23 +99,23 @@ describe("getOfferTotals per-currency aggregation", () => {
|
||||
await mk("draft");
|
||||
await mk("invalidated");
|
||||
|
||||
// Marker only: all three count -> 3 x 1210 = 3630.
|
||||
// Marker only: all three count -> 3 x 1000 = 3000 (net).
|
||||
const all = await getOfferTotals({ search: OFFER_MARKER });
|
||||
expect(amountFor(all.totals, "CZK")).toBe(3630);
|
||||
expect(amountFor(all.totals, "CZK")).toBe(3000);
|
||||
|
||||
// Status filter narrows to the two drafts -> 2 x 1210 = 2420.
|
||||
// Status filter narrows to the two drafts -> 2 x 1000 = 2000.
|
||||
const onlyDraft = await getOfferTotals({
|
||||
search: OFFER_MARKER,
|
||||
status: "draft",
|
||||
});
|
||||
expect(amountFor(onlyDraft.totals, "CZK")).toBe(2420);
|
||||
expect(amountFor(onlyDraft.totals, "CZK")).toBe(2000);
|
||||
|
||||
// Invalidated alone -> 1210.
|
||||
// Invalidated alone -> 1000.
|
||||
const onlyInvalid = await getOfferTotals({
|
||||
search: OFFER_MARKER,
|
||||
status: "invalidated",
|
||||
});
|
||||
expect(amountFor(onlyInvalid.totals, "CZK")).toBe(1210);
|
||||
expect(amountFor(onlyInvalid.totals, "CZK")).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user