64 lines
2.6 KiB
TypeScript
64 lines
2.6 KiB
TypeScript
|
|
/**
|
||
|
|
* The two quantities on an `order_items` row, and which one to use where.
|
||
|
|
*
|
||
|
|
* `order_items` carries two numbers that are equal on almost every row and
|
||
|
|
* mean very different things on the rows where they aren't:
|
||
|
|
*
|
||
|
|
* `quantity` — what the CUSTOMER asked for. Drives the invoice, the
|
||
|
|
* delivery note, and the "ordered" column on the picking
|
||
|
|
* slip. This is the number the customer agreed to pay for.
|
||
|
|
*
|
||
|
|
* `order_qty` — what WE order from the supplier for this line. Drives the
|
||
|
|
* PO PDF, the PO email and the PO total. The process page
|
||
|
|
* labels this field "what we tell the supplier".
|
||
|
|
*
|
||
|
|
* They diverge in two real situations:
|
||
|
|
*
|
||
|
|
* - **Supplier MOQ.** Customer wants 4, the supplier won't sell fewer than
|
||
|
|
* 10. `quantity = 4`, `order_qty = 10`. We bill 4 and book 6 into stock.
|
||
|
|
* - **Shorting.** Customer wants 100, supplier A can only supply 40. The
|
||
|
|
* operator sets `order_qty = 40` and splits the remaining 60 onto a new
|
||
|
|
* line for supplier B (`splitOrderItemRemainder`).
|
||
|
|
*
|
||
|
|
* Getting this wrong is a money bug in both directions: reading `order_qty`
|
||
|
|
* as the customer quantity over-bills on MOQ lines and under-bills on shorted
|
||
|
|
* lines. Reading `quantity` as the supplier quantity puts the wrong number on
|
||
|
|
* a purchase order.
|
||
|
|
*
|
||
|
|
* Background and the full list of miscounts this module was written to fix:
|
||
|
|
* `docs/superpowers/specs/2026-08-30-quantity-handling-audit.md`.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** The subset of an order item these helpers need. */
|
||
|
|
export type QtyBearingItem = {
|
||
|
|
quantity?: number | null;
|
||
|
|
order_qty?: number | null;
|
||
|
|
};
|
||
|
|
|
||
|
|
function finite(value: number | null | undefined): number | null {
|
||
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* What the customer buys and is billed for.
|
||
|
|
*
|
||
|
|
* Falls back to `order_qty` only for legacy rows that never had a
|
||
|
|
* `quantity` — historically the two were written identically at creation,
|
||
|
|
* so the fallback is safe and keeps old orders invoicing correctly.
|
||
|
|
*/
|
||
|
|
export function customerQty(item: QtyBearingItem): number {
|
||
|
|
return finite(item.quantity) ?? finite(item.order_qty) ?? 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* What we order from the supplier — the number that prints on the PO.
|
||
|
|
*
|
||
|
|
* A literal `0` is meaningful here and is returned as-is: the operator
|
||
|
|
* setting 0 means "order nothing from this supplier on this line". Only an
|
||
|
|
* unset `order_qty` falls back to the customer quantity, which is the
|
||
|
|
* common case where the operator never edited the supplier-side number.
|
||
|
|
*/
|
||
|
|
export function supplierQty(item: QtyBearingItem): number {
|
||
|
|
return finite(item.order_qty) ?? finite(item.quantity) ?? 0;
|
||
|
|
}
|