feat(pdf): add poSubtotal helper for PO PDF totals
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
40d069b5aa
commit
b3d9eeb5ce
2 changed files with 55 additions and 1 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue