inventory.valuation-weighted-average@1.0.0
impl/typescript.ts
2,758 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { epochDayFromIso } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { type RoundingMode, roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, assertSameCurrency, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type AverageCostValuation, type StockMovement } from "./inventory_valuation_weighted_average_types.ts";
/**
* Perpetual weighted average cost. The stock's value is held exactly and each
* issue takes value x issued / on hand, rounded once, so the last issue takes
* exactly what is left and an empty item is worth nothing.
*/
export function weightedAverageValuation(movements: readonly StockMovement[], currency: string, mode: RoundingMode): AverageCostValuation {
const zero = money(0, currency);
roundDiv(0, 1, mode); // an unknown mode fails even on a ledger with no issues
let quantity = 0;
let value = 0;
let costOfSales = 0;
const issueCosts: Money[] = [];
let previousDay: number | null = null;
let previousDate = "";
for (const line of movements) {
const day = epochDayFromIso(line.date);
if (previousDay !== null && day < previousDay) {
throw new RangeError(`movements must be in date order: ${line.date} comes after ${previousDate}`);
}
previousDay = day;
previousDate = line.date;
if (!Number.isInteger(line.quantity) || line.quantity === 0) {
throw new RangeError(`quantity must be a non-zero whole number, received ${line.quantity} on ${line.date}`);
}
if (line.quantity > 0) {
if (line.unitCost === null || line.unitCost === undefined) {
throw new RangeError(`a receipt needs a unitCost: ${line.quantity} on ${line.date}`);
}
assertSameCurrency(zero, line.unitCost);
if (line.unitCost.minor < 0) {
throw new RangeError(`unitCost must not be negative, received ${line.unitCost.minor} on ${line.date}`);
}
quantity += line.quantity;
value += line.quantity * line.unitCost.minor;
continue;
}
if (line.unitCost !== null && line.unitCost !== undefined) {
throw new RangeError(`an issue takes its cost from stock, so its unitCost must be null: ${line.quantity} on ${line.date}`);
}
const issued = -line.quantity;
if (issued > quantity) {
throw new RangeError(`insufficient stock: an issue of ${issued} on ${line.date} exceeds the ${quantity} on hand`);
}
const cost = roundDiv(value * issued, quantity, mode);
issueCosts.push(money(cost, currency));
costOfSales += cost;
value -= cost;
quantity -= issued;
}
return {
closingQuantity: quantity,
closingValue: money(value, currency),
costOfSales: money(costOfSales, currency),
issueCosts,
averageUnitCost: money(quantity === 0 ? 0 : roundDiv(value, quantity, mode), currency),
};
}