feat(pdf): add poSubtotal helper for PO PDF totals

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-06 11:54:23 +02:00
parent 40d069b5aa
commit b3d9eeb5ce
2 changed files with 55 additions and 1 deletions

View file

@ -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);
});
});
});
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);
});
});

View file

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