feat(pdf): add 15% VAT line + VAT-inclusive Total to PO PDF
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
a1714833a8
commit
7a6e32cadb
2 changed files with 35 additions and 30 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Document
|
||||
|
|
@ -219,18 +217,25 @@ export function PurchaseOrderDocument({
|
|||
})}
|
||||
</View>
|
||||
|
||||
{/* 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. */}
|
||||
<View style={styles.totals}>
|
||||
<View style={styles.totalLine}>
|
||||
<Text style={styles.totalLabel}>Subtotal:</Text>
|
||||
<Text style={styles.totalValue}>{formatZAR(subtotal)}</Text>
|
||||
</View>
|
||||
<View style={styles.totalLine}>
|
||||
<Text style={styles.totalLabel}>VAT (15%):</Text>
|
||||
<Text style={styles.totalValue}>{formatZAR(vat)}</Text>
|
||||
</View>
|
||||
<View style={styles.totalRule} />
|
||||
<View style={styles.totalLine}>
|
||||
<Text style={styles.totalFinalLabel}>Total:</Text>
|
||||
<Text style={styles.totalFinalValue}>{formatZAR(total)}</Text>
|
||||
<Text style={styles.totalFinalValue}>{formatZAR(totalInclVat)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
|
|
|
|||
Loading…
Reference in a new issue