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),
)