diff --git a/src/lib/pdf/format.ts b/src/lib/pdf/format.ts index 75ae708..753cecf 100644 --- a/src/lib/pdf/format.ts +++ b/src/lib/pdf/format.ts @@ -57,7 +57,7 @@ export function quoteSubtotal(items: { quantity: number; selling_price: number } * `Subtotal` and the base of the `VAT (15%)` line. */ export function poSubtotal( - items: { order_qty?: number | null; quantity: number; cost_price: number }[] + items: { order_qty?: number | null; quantity: number | null; cost_price: number }[] ): number { return Math.round( items.reduce( diff --git a/src/lib/pdf/purchase-order-document.tsx b/src/lib/pdf/purchase-order-document.tsx index 32f426b..bbeeb6d 100644 --- a/src/lib/pdf/purchase-order-document.tsx +++ b/src/lib/pdf/purchase-order-document.tsx @@ -1,7 +1,7 @@ import { Document, Page, View, Text, Image, StyleSheet } from "@react-pdf/renderer"; import type { Letterhead } from "@/lib/letterhead"; import type { OrderItem, Order, Supplier, PurchaseOrder } from "@/lib/types"; -import { formatZAR, formatDateLong } from "./format"; +import { formatZAR, formatDateLong, poSubtotal, vatAmount } from "./format"; import { defaultAccentFor } from "@/lib/letterhead"; /** @@ -60,9 +60,13 @@ export interface PurchaseOrderDocumentProps { * The customer's identity stays internal. * - **No footer text** — keeps the document visually quiet and * prevents accidental leakage of branch-specific marketing copy. - * - **No VAT line** — we pay what the supplier quotes, no markup - * math here. Different from the quote (customer-facing) where - * VAT is added on top of selling price. + * - **VAT (15%) added to subtotal** — prints the supplier-quoted + * subtotal, then VAT (15%), then a VAT-inclusive Total. Same + * shape as the quote's totals block so the two documents feel + * like siblings. The VAT-inclusive Total is the figure the + * office uses for budget / VAT-return reconciliation; the stored + * `purchase_orders.total_amount` stays VAT-ex (no PB schema + * change). * * Description column uses `po_description` (supplier-facing copy), * NOT `description` (internal/operator copy). Falling back to @@ -87,27 +91,21 @@ export function PurchaseOrderDocument({ (letterhead.display_name?.toUpperCase().includes("CNL") ? "CNL" : "ISHERWOOD") ); - // Subtotal = sum of (qty × cost) per line. Qty uses the same - // supplier-facing resolution as the line rows below — `order_qty` - // when the operator shorted or bumped a line, else falls back to - // the customer-facing `quantity` so a blank order_qty never zeroes - // a line. This keeps the printed line totals summing exactly to the - // Subtotal. We round to 2 dp so the table doesn't show floating-point - // dust (1.0000000003 R etc). - const subtotal = Math.round( - items.reduce( - (sum, it) => - sum + - (Number(it.order_qty ?? it.quantity) || 0) * - (Number(it.cost_price) || 0), - 0 - ) * 100 - ) / 100; - // Re-use the supplied snapshot if it differs from the live - // computation by < 0.01 — caller might have included an explicit - // total_amount that we want to honour (e.g. from the regenerate - // API where the PO record already has total_amount set). - const total = Math.abs(totalAmount - subtotal) < 0.01 ? totalAmount : subtotal; + // Subtotal = sum of (qty × cost) per line, using the + // `order_qty ?? quantity` supplier-facing fallback. Logic lives + // in `poSubtotal()` (src/lib/pdf/format.ts) so it's independently + // testable. This keeps the printed line totals summing exactly to the + // Subtotal. + const subtotal = poSubtotal(items); + // 15% VAT on top of the supplier-quoted subtotal — same helper the + // quote PDF uses, so both documents compute VAT identically. Total + // = subtotal + vat, rounded to 2dp so the line matches the printed + // figures exactly. The `totalAmount` prop (carried for API backwards + // compat with the existing route) is intentionally not used here: + // it holds the VAT-ex subtotal and there's nothing meaningful to + // reconcile against once the printed Total is `subtotal + vat`. + const vat = vatAmount(subtotal); + const totalInclVat = Math.round((subtotal + vat) * 100) / 100; return ( - {/* Totals — just Subtotal + Total. No VAT (we're buying, the - supplier's price is what we pay). Rule above Total mirrors - the quote's totals block so the two feel like siblings. */} + {/* Totals — Subtotal + VAT (15%) + rule + Total. Mirrors the + quote's totals block so the two PDFs feel like siblings + when filed side-by-side. `Subtotal` is the VAT-ex sum + (stored on `purchase_orders.total_amount`); the printed + `Total` is `subtotal × 1.15` — the figure the office + uses for budget / VAT-return reconciliation. */} Subtotal: {formatZAR(subtotal)} + + VAT (15%): + {formatZAR(vat)} + Total: - {formatZAR(total)} + {formatZAR(totalInclVat)}