"""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