117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
"""Phase 5: event-sourced inventory ledger (no GUI)."""
|
|
from woodshop.cutplan import build_cut_plan
|
|
from woodshop.inventory import Ledger, plan_consumption
|
|
from woodshop.scene import Scene
|
|
|
|
|
|
def test_purchase_then_consume_on_hand():
|
|
led = Ledger()
|
|
led.purchase("2x4", 5)
|
|
led.purchase("ply-3/4", 2)
|
|
assert led.on_hand() == {("2x4", "spruce"): 5, ("ply-3/4", "spruce-ply"): 2}
|
|
led.record_build("table", 1, consumed={"2x4": 2}, offcuts=[])
|
|
assert led.on_hand()[("2x4", "spruce")] == 3
|
|
|
|
|
|
def test_adjustment_corrects_count():
|
|
led = Ledger()
|
|
led.purchase("2x4", 5)
|
|
led.adjust("2x4", -1, reason="broke one")
|
|
assert led.on_hand()[("2x4", "spruce")] == 4
|
|
|
|
|
|
def test_inventory_is_species_aware():
|
|
led = Ledger()
|
|
led.purchase("1x4", 3, material="oak")
|
|
led.purchase("1x4", 2, material="spruce")
|
|
oh = led.on_hand()
|
|
assert oh[("1x4", "oak")] == 3 and oh[("1x4", "spruce")] == 2
|
|
# consuming oak doesn't touch spruce
|
|
led.record_build("x", 1, consumed={("1x4", "oak"): 1}, offcuts=[])
|
|
assert led.on_hand()[("1x4", "oak")] == 2 and led.on_hand()[("1x4", "spruce")] == 2
|
|
|
|
|
|
def test_record_build_keeps_and_discards_offcuts():
|
|
led = Ledger()
|
|
offcuts = [{"stock": "2x4", "length_in": 20, "width_in": 3.5, "is_sheet": False},
|
|
{"stock": "2x4", "length_in": 8, "width_in": 3.5, "is_sheet": False}]
|
|
bid = led.record_build("shelf", 1, consumed={"2x4": 1}, offcuts=offcuts,
|
|
dispositions=["keep", "burned"])
|
|
bin_ = led.offcut_bin()
|
|
assert len(bin_) == 1 and bin_[0].id == "b1-o1" # kept the 20"
|
|
stats = led.stats()
|
|
assert stats["offcuts_kept"] == 1 and stats["offcuts_burned"] == 1
|
|
|
|
|
|
def test_consume_offcut_removes_it_from_bin():
|
|
led = Ledger()
|
|
led.record_build("a", 1, consumed={}, offcuts=[
|
|
{"stock": "2x4", "length_in": 20, "width_in": 3.5, "is_sheet": False}])
|
|
oid = led.offcut_bin()[0].id
|
|
led.consume_offcut(oid)
|
|
assert led.offcut_bin() == []
|
|
|
|
|
|
def test_plan_consumption_from_cutplan():
|
|
s = Scene()
|
|
s.place("2x4", 60) # leaves a reusable ~35" offcut on a 96" stick
|
|
plan = build_cut_plan(s)
|
|
consumed, offcuts = plan_consumption(plan)
|
|
assert consumed.get(("2x4", "spruce")) == 1
|
|
assert offcuts and offcuts[0]["stock"] == "2x4" and offcuts[0]["length_in"] > 12
|
|
|
|
|
|
def test_available_stock_combines_full_and_offcuts():
|
|
led = Ledger()
|
|
led.purchase("2x4", 2)
|
|
led.record_build("a", 1, consumed={}, offcuts=[
|
|
{"stock": "2x4", "length_in": 20, "width_in": 3.5, "is_sheet": False}])
|
|
avail = led.available_stock()
|
|
assert sum(1 for p in avail if p.is_offcut) == 1
|
|
assert sum(1 for p in avail if not p.is_offcut and p.stock == "2x4") == 2
|
|
|
|
|
|
def test_ledger_save_load_roundtrip(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
|
|
led = Ledger()
|
|
led.purchase("2x4", 3, price=3.98)
|
|
led.save()
|
|
again = Ledger.load()
|
|
assert again.on_hand()[("2x4", "spruce")] == 3
|
|
assert again.stats()["spent"] == round(3 * 3.98, 2)
|
|
|
|
|
|
def test_stats_aggregate_across_projects():
|
|
led = Ledger()
|
|
led.record_build("table", 2, consumed={}, offcuts=[])
|
|
led.record_build("shelf", 1, consumed={}, offcuts=[])
|
|
st = led.stats()
|
|
assert st["units_built"] == 3 and st["builds"] == 2
|
|
assert st["by_project"] == {"table": 2, "shelf": 1}
|
|
|
|
|
|
def test_planner_consumes_offcuts_before_buying():
|
|
from woodshop.inventory import Piece
|
|
s = Scene()
|
|
s.place("2x4", 30)
|
|
# one 96" offcut on hand → the 30" piece should use it, buying 0 new sticks
|
|
offcut = Piece(id="oc1", stock="2x4", length_in=96, width_in=3.5,
|
|
is_sheet=False, is_offcut=True)
|
|
plan = build_cut_plan(s, available=[offcut])
|
|
assert plan.score["stock_count"] == 0 # bought nothing
|
|
assert plan.score["owned_count"] == 1
|
|
owned = [sp for sp in plan.stock_pieces if sp.owned]
|
|
assert owned and owned[0].placements # the 30" sits in the offcut
|
|
|
|
|
|
def test_oak_project_not_satisfied_by_spruce_onhand():
|
|
"""Codex #2: a generic spruce 1x4 on-hand must NOT satisfy an oak 1x4 cut."""
|
|
led = Ledger()
|
|
led.purchase("1x4", 5, material="spruce")
|
|
s = Scene()
|
|
s.place("1x4", 30)
|
|
s.set_material("p1", "oak")
|
|
plan = build_cut_plan(s, available=led.available_stock())
|
|
assert plan.score["stock_count"] == 1 # must buy oak
|
|
assert plan.score["owned_count"] == 0
|