dodil/erp-brick-manufacturing
DODIL ERP brick: manufacturing
tests/test_domain.py
"""The business rules with no database and no HTTP: the BOM aggregate and settlement arithmetic, from the stories' own numbers."""
from datetime import date
from decimal import Decimal
import pytest
from brick_manufacturing.domain.bom import Bom, BomLine
from brick_manufacturing.domain.errors import OutOfBounds, RuleBroken
from brick_manufacturing.domain.settlement import settle
from brick_manufacturing.domain.values import quantity
D = Decimal
def roast() -> Bom:
bom = Bom("ROAST-ETH:production", "ROAST-ETH", "production", "", "UK01")
bom.write_version(1, valid_from=date(2026, 11, 16), output_quantity=D("84"), output_unit_code="KGM", output_quantity_base=D("84"),
output_base_unit_code="KGM", edges={},
lines=[BomLine(0, 1, "component", "GREEN-ETH", D("100"), "KGM", D("100"), "KGM")])
return bom
def test_the_planned_yield_is_output_per_input_and_shown_never_typed():
assert roast().versions[0].planned_yield_pct == D("84.0000")
def test_validity_is_computed_from_released_versions_so_they_never_overlap():
bom = roast()
bom.release_bom(1, today=date(2026, 11, 10), by="a", sub="a")
bom.write_version(2, valid_from=date(2026, 12, 1), output_quantity=D("85"), output_unit_code="KGM", output_quantity_base=D("85"),
output_base_unit_code="KGM", edges={}, lines=[BomLine(0, 1, "component", "GREEN-ETH", D("100"), "KGM", D("100"), "KGM")])
bom.release_bom(2, today=date(2026, 11, 10), by="a", sub="a")
assert [bom.valid_on(date(2026, 11, d)).version for d in (16, 30)] == [1, 1]
assert bom.valid_on(date(2026, 12, 1)).version == 2 and bom.valid_on(date(2026, 11, 15)) is None
bom.obsolete_bom(2, valid_to=date(2026, 12, 31))
assert bom.valid_on(date(2027, 1, 1)) is None
def test_explode_scales_by_the_requested_over_the_output_and_rounds_once_per_line():
bom = roast()
bom.release_bom(1, today=date(2026, 11, 10), by="a", sub="a")
v, comps = bom.explode(D("1"), date(2026, 11, 16))
assert comps == [("GREEN-ETH", D("1.1905"), "KGM")] # 100 / 84, rounded half up at 4 places
assert bom.explode(D("168"), date(2026, 11, 16))[1] == [("GREEN-ETH", D("200.0000"), "KGM")]
def test_a_cycle_through_another_bom_is_found_by_a_walk():
bom = roast()
with pytest.raises(RuleBroken, match="contain itself"):
Bom("X:production", "X", "production").write_version(
1, valid_from=date(2026, 12, 1), output_quantity=D("1"), output_unit_code="KGM", output_quantity_base=D("1"),
output_base_unit_code="KGM", edges={"GREEN-ETH": {"Y"}, "Y": {"X"}},
lines=[BomLine(0, 1, "component", "GREEN-ETH", D("1"), "KGM", D("1"), "KGM")])
assert bom
@pytest.mark.parametrize("v", ["0", "-1", "1e15", "1.00001", "x", None, 1.5, True, "NaN", "Infinity"])
def test_quantities_outside_numeric_19_4_are_out_of_bounds(v):
with pytest.raises(OutOfBounds):
quantity(v)
def test_the_stories_settlement_numbers_with_activity_absorbed():
"""MFG-15: WIP debit 620.00 + 180.00 activity = 800.00, credit 774.67; material usage 11.07, activity 14.26, sum 25.33."""
s = settle([{"planned_base": D("100"), "issued_base": D("100"), "issued_value": D("620.00")}], outputs_value=D("774.67"),
activity_cost=D("180.00"), output_base=D("82.5"), planned_output_base=D("84"))
assert (s.wip_debit, s.wip_credit, s.balance) == (D("800.00"), D("774.67"), D("25.33"))
assert (s.standard_material, s.material_usage_variance, s.activity_variance) == (D("608.93"), D("11.07"), D("14.26"))
assert s.material_usage_variance + s.activity_variance + s.abnormal_scrap == s.balance
def test_normal_roasting_loss_in_the_planned_yield_is_no_variance():
s = settle([{"planned_base": D("100"), "issued_base": D("100"), "issued_value": D("620.00")}], outputs_value=D("788.75"),
activity_cost=D("168.75"), output_base=D("84"), planned_output_base=D("84"))
assert s.material_usage_variance == D("0.00") and s.activity_variance == D("0.00") and s.balance == D("0.00")
def test_a_balance_no_ledger_can_hold_is_out_of_bounds():
with pytest.raises(OutOfBounds):
settle([{"planned_base": D("1"), "issued_base": D("1"), "issued_value": D("9999999999999999.99")}], outputs_value=D("0"),
activity_cost=D("9999999999999999.99"), output_base=D("1"), planned_output_base=D("1"))