woodshop/tests/test_inventory.py

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