Files
accounted/lib/sales-orders/progress.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

72 lines
2.5 KiB
TypeScript

import type { SalesOrderItem, SalesOrderProgress } from '@/types'
/**
* Quantity arithmetic on order lines.
*
* Postgres stores quantities as exact numeric; JavaScript does not. A
* remaining quantity computed as `7.5 - 6.9` in doubles is 0.5999999999999996,
* which the DB then refuses ("0.6 > remaining") or, if sent as-is, lands as
* an invoice quantity the customer never ordered. Every derived quantity is
* therefore rounded to QTY_DECIMALS before it is compared or persisted, and
* comparisons use QTY_EPSILON so 0.1 + 0.2 style drift never decides a case
* that the exact numeric would decide the other way.
*/
export const QTY_DECIMALS = 6
const QTY_SCALE = 10 ** QTY_DECIMALS
export const QTY_EPSILON = 1 / QTY_SCALE / 2
export function roundQty(n: number): number {
if (n === 0) return 0
return Math.round((n + Number.EPSILON) * QTY_SCALE) / QTY_SCALE
}
/** a > b beyond quantity precision. */
export function qtyGreater(a: number, b: number): boolean {
return a - b > QTY_EPSILON
}
/**
* Derived per-axis progress of an order. Delivery and invoicing are
* independent: an order is often partially delivered and partially
* invoiced at the same time, which is why neither is a header status.
*
* Only product lines with a positive quantity count; text rows and zero
* quantity rows carry no progress.
*/
function progressOf(items: SalesOrderItem[], pick: (item: SalesOrderItem) => number): SalesOrderProgress {
const lines = items.filter((i) => i.line_type !== 'text' && i.quantity > 0)
if (lines.length === 0) return 'none'
let any = false
let all = true
for (const line of lines) {
const done = pick(line)
if (done > 0) any = true
if (qtyGreater(line.quantity, done)) all = false
}
if (all) return 'full'
return any ? 'partial' : 'none'
}
export function deliveryProgress(items: SalesOrderItem[]): SalesOrderProgress {
return progressOf(items, (i) => i.delivered_qty)
}
export function invoicingProgress(items: SalesOrderItem[]): SalesOrderProgress {
return progressOf(items, (i) => i.invoiced_qty ?? 0)
}
/** Attach invoiced_qty / remaining_qty (both rounded to quantity precision) from the RPC rows. */
export function withInvoicedQuantities(
items: SalesOrderItem[],
invoiced: Map<string, number>,
): SalesOrderItem[] {
return items.map((item) => {
const invoicedQty = roundQty(invoiced.get(item.id) ?? 0)
return {
...item,
invoiced_qty: invoicedQty,
remaining_qty: Math.max(0, roundQty(item.quantity - invoicedQty)),
}
})
}