diff --git a/src/lib/pdf/format.test.ts b/src/lib/pdf/format.test.ts index 5c79237..e54126c 100644 --- a/src/lib/pdf/format.test.ts +++ b/src/lib/pdf/format.test.ts @@ -5,6 +5,7 @@ import { addDays, quoteSubtotal, vatAmount, + poSubtotal, QUOTE_VALID_DAYS, effectiveQuoteDate, effectiveValidityDays, @@ -109,6 +110,8 @@ describe("effectiveQuoteDate", () => { }); }); + + describe("effectiveValidityDays", () => { it("uses quote.validity_days when set to a number", () => { expect(effectiveValidityDays({ validity_days: 60 })).toBe(60); @@ -119,4 +122,35 @@ describe("effectiveValidityDays", () => { it("falls back to QUOTE_VALID_DAYS (30) when undefined", () => { expect(effectiveValidityDays({})).toBe(QUOTE_VALID_DAYS); }); -}); \ No newline at end of file +}); + +describe("poSubtotal", () => { + it("sums order_qty × cost_price when order_qty is set", () => { + expect(poSubtotal([{ order_qty: 3, quantity: 1, cost_price: 100 }])).toBe(300); + }); + it("falls back to quantity when order_qty is missing", () => { + expect(poSubtotal([{ quantity: 4, cost_price: 50 }])).toBe(200); + }); + it("falls back to quantity when order_qty is 0 (blank treated as missing)", () => { + expect(poSubtotal([{ order_qty: 0, quantity: 2, cost_price: 75 }])).toBe(150); + }); + it("sums across multiple items", () => { + expect(poSubtotal([ + { order_qty: 3, quantity: 1, cost_price: 100 }, + { order_qty: 5, quantity: 1, cost_price: 40 }, + ])).toBe(500); + }); + it("rounds to 2 decimal places (avoids floating-point dust)", () => { + expect(poSubtotal([ + { quantity: 3, cost_price: 33.33 }, + { quantity: 3, cost_price: 33.33 }, + { quantity: 3, cost_price: 33.34 }, + ])).toBe(300); + }); + it("returns 0 for an empty array", () => { + expect(poSubtotal([])).toBe(0); + }); + it("treats non-numeric cost_price as 0", () => { + expect(poSubtotal([{ quantity: 5, cost_price: NaN }])).toBe(0); + }); +}); diff --git a/src/lib/pdf/format.ts b/src/lib/pdf/format.ts index e2bf1a3..1f49cf0 100644 --- a/src/lib/pdf/format.ts +++ b/src/lib/pdf/format.ts @@ -39,6 +39,26 @@ export function quoteSubtotal(items: { quantity: number; selling_price: number } return items.reduce((sum, it) => sum + it.quantity * it.selling_price, 0); } +/** + * Sum of (order_qty ?? quantity) * cost_price across PO items. Differs + * from `quoteSubtotal()` in using supplier-facing cost and order quantity. + * A blank or zero order_qty falls back to quantity so it never zeroes a line. + * Rounds to 2dp so the printed table doesn't show floating-point dust. + */ +export function poSubtotal( + items: { order_qty?: number | null; quantity: number; cost_price: number }[] +): number { + return Math.round( + items.reduce( + (sum, it) => + sum + + (Number(it.order_qty) || Number(it.quantity) || 0) * + (Number(it.cost_price) || 0), + 0 + ) * 100 + ) / 100; +} + /** ZAR VAT is 15% — round to 2dp so the printed total matches the line items. */ export function vatAmount(subtotal: number): number { return Math.round(subtotal * 0.15 * 100) / 100;