inventory.valuation-fifo@1.0.0
impl/typescript.ts
3,072 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 { assertSameCurrency, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type FifoValuation, type StockLayer, type StockMovement } from "./inventory_valuation_fifo_types.ts";
/**
* Value stock first in, first out, and cost each issue from the oldest layers.
*
* Every cost is a whole quantity times a unit cost, so nothing is rounded and
* closing value plus cost of sales always equals what was received.
*/
export function fifoValuation(movements: readonly StockMovement[], currency: string): FifoValuation {
const zero = money(0, currency);
const layers: { date: string; quantity: number; unitCost: StockLayer["unitCost"] }[] = [];
const issueCosts = [];
let costOfSales = 0;
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}`);
}
layers.push({ date: line.date, quantity: line.quantity, unitCost: line.unitCost });
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}`);
}
let wanted = -line.quantity;
const onHand = layers.reduce((sum, layer) => sum + layer.quantity, 0);
if (wanted > onHand) {
throw new RangeError(`insufficient stock: an issue of ${wanted} on ${line.date} exceeds the ${onHand} on hand`);
}
let cost = 0;
while (wanted > 0) {
const oldest = layers[0];
const used = Math.min(wanted, oldest.quantity);
cost += used * oldest.unitCost.minor;
oldest.quantity -= used;
wanted -= used;
if (oldest.quantity === 0) layers.shift();
}
issueCosts.push(money(cost, currency));
costOfSales += cost;
}
let closingQuantity = 0;
let closingValue = 0;
for (const layer of layers) {
closingQuantity += layer.quantity;
closingValue += layer.quantity * layer.unitCost.minor;
}
return {
closingQuantity,
closingValue: money(closingValue, currency),
costOfSales: money(costOfSales, currency),
issueCosts,
layers: layers.map((layer) => ({ date: layer.date, quantity: layer.quantity, unitCost: layer.unitCost })),
};
}