Functional Weave
Code in TypeScript

manufacturing.bom-cost@1.0.0

impl/python.py

3,252 bytes · the Python 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.

from typing import Dict, Sequence, Tuple

from .manufacturing_bom_cost_types import CostRollup, ItemCost, RoutingOperation
from .manufacturing_bom_explode import BomLine, explode_bom  ← from manufacturing.bom-explode ^1.0.0 · built alongside by fune
from .math_rational import Rational, add_rational, multiply_rational, rational, rational_to_integer  ← from math.rational ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune

MAX_SAFE = 2**53 - 1


def bom_cost(
    lines: Sequence[BomLine],
    item: str,
    material_costs: Sequence[ItemCost],
    operations: Sequence[RoutingOperation],
    currency: str,
    mode: RoundingMode,
) -> CostRollup:
    """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.
    """
    zero = money(0, currency)
    needs: Dict[str, Tuple[Rational, bool]] = {item: (rational(1, 1), False)}
    for r in explode_bom(lines, item, 1):
        needs[r.item] = (r.quantity, r.leaf)

    costs: Dict[str, int] = {}
    for c in material_costs:
        assert_same_currency(zero, c.unit_cost)
        if c.item in costs:
            raise ValueError('duplicate material cost for "%s"' % (c.item,))
        if c.unit_cost.minor < 0:
            raise ValueError('unitCost of "%s" must not be negative, received %d' % (c.item, c.unit_cost.minor))
        costs[c.item] = c.unit_cost.minor

    material = rational(0, 1)
    for name, (quantity, leaf) in needs.items():
        cost = costs.get(name)
        if not leaf:
            if cost is not None:
                raise ValueError(
                    '"%s" is made from its own bill of materials, so its cost is rolled up; remove it from materialCosts'
                    % (name,)
                )
            continue
        if cost is None:
            raise ValueError('no material cost for "%s"' % (name,))
        material = add_rational(material, multiply_rational(quantity, rational(cost, 1)))

    labour = rational(0, 1)
    overhead = rational(0, 1)
    for op in operations:
        if isinstance(op.seconds, bool) or not isinstance(op.seconds, int) or op.seconds < 0 or op.seconds > MAX_SAFE:
            raise ValueError(
                'seconds of an operation on "%s" must be a whole number, not negative, received %r' % (op.item, op.seconds)
            )
        assert_same_currency(zero, op.labour_rate)
        assert_same_currency(zero, op.overhead_rate)
        if op.labour_rate.minor < 0 or op.overhead_rate.minor < 0:
            raise ValueError('the rates of an operation on "%s" must not be negative' % (op.item,))
        need = needs.get(op.item)
        if need is None:
            continue
        hours = multiply_rational(need[0], rational(op.seconds, 3600))
        labour = add_rational(labour, multiply_rational(hours, rational(op.labour_rate.minor, 1)))
        overhead = add_rational(overhead, multiply_rational(hours, rational(op.overhead_rate.minor, 1)))

    m = rational_to_integer(material, mode)
    l = rational_to_integer(labour, mode)
    o = rational_to_integer(overhead, mode)
    return CostRollup(
        material=money(m, currency),
        labour=money(l, currency),
        overhead=money(o, currency),
        total=money(m + l + o, currency),
    )