Functional Weave
Code in Python

manufacturing.bom-cost@1.0.0

impl/typescript.ts

3,210 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 { type BomLine, explodeBom } from "./manufacturing_bom_explode.ts";  ← from manufacturing.bom-explode ^1.0.0 · built alongside by fune
import { type Rational, addRational, multiplyRational, rational, rationalToInteger } from "./math_rational.ts";  ← from math.rational ^1.0.0 · built alongside by fune
import { type RoundingMode } from "./math_round_div.ts";  ← from math.round-div ^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 CostRollup, type ItemCost, type RoutingOperation } from "./manufacturing_bom_cost_types.ts";

/**
 * Standard cost of one unit, by cost element, rolled up through the BOM.
 * Every element is an exact fraction of a minor unit until it is rounded once.
 */
export function bomCost(
  lines: readonly BomLine[],
  item: string,
  materialCosts: readonly ItemCost[],
  operations: readonly RoutingOperation[],
  currency: string,
  mode: RoundingMode,
): CostRollup {
  const zero = money(0, currency);
  const needs = new Map<string, { quantity: Rational; leaf: boolean }>();
  needs.set(item, { quantity: rational(1, 1), leaf: false });
  for (const r of explodeBom(lines, item, 1)) {
    needs.set(r.item, { quantity: r.quantity, leaf: r.leaf });
  }

  const costs = new Map<string, number>();
  for (const c of materialCosts) {
    assertSameCurrency(zero, c.unitCost);
    if (costs.has(c.item)) {
      throw new RangeError(`duplicate material cost for "${c.item}"`);
    }
    if (c.unitCost.minor < 0) {
      throw new RangeError(`unitCost of "${c.item}" must not be negative, received ${c.unitCost.minor}`);
    }
    costs.set(c.item, c.unitCost.minor);
  }

  let material = rational(0, 1);
  for (const [name, need] of needs) {
    const cost = costs.get(name);
    if (!need.leaf) {
      if (cost !== undefined) {
        throw new RangeError(`"${name}" is made from its own bill of materials, so its cost is rolled up; remove it from materialCosts`);
      }
      continue;
    }
    if (cost === undefined) {
      throw new RangeError(`no material cost for "${name}"`);
    }
    material = addRational(material, multiplyRational(need.quantity, rational(cost, 1)));
  }

  let labour = rational(0, 1);
  let overhead = rational(0, 1);
  for (const op of operations) {
    if (!Number.isSafeInteger(op.seconds) || op.seconds < 0) {
      throw new RangeError(`seconds of an operation on "${op.item}" must be a whole number, not negative, received ${op.seconds}`);
    }
    assertSameCurrency(zero, op.labourRate);
    assertSameCurrency(zero, op.overheadRate);
    if (op.labourRate.minor < 0 || op.overheadRate.minor < 0) {
      throw new RangeError(`the rates of an operation on "${op.item}" must not be negative`);
    }
    const need = needs.get(op.item);
    if (need === undefined) continue;
    const hours = multiplyRational(need.quantity, rational(op.seconds, 3600));
    labour = addRational(labour, multiplyRational(hours, rational(op.labourRate.minor, 1)));
    overhead = addRational(overhead, multiplyRational(hours, rational(op.overheadRate.minor, 1)));
  }

  const m = rationalToInteger(material, mode);
  const l = rationalToInteger(labour, mode);
  const o = rationalToInteger(overhead, mode);
  return {
    material: money(m, currency),
    labour: money(l, currency),
    overhead: money(o, currency),
    total: money(m + l + o, currency),
  };
}