123 lines
4.5 KiB
Python
123 lines
4.5 KiB
Python
"""Tests for the cost-estimate price book (deterministic math; no network)."""
|
||
from woodshop import prices as P
|
||
from woodshop.cutplan import ShopSettings, build_cut_plan
|
||
from woodshop.scene import Scene
|
||
|
||
|
||
def test_estimate_sums_sticks_and_applies_hst():
|
||
s = Scene()
|
||
for _ in range(3):
|
||
s.place("2x4", 40) # 3 × 40" -> 2 sticks
|
||
plan = build_cut_plan(s)
|
||
est = P.estimate(plan, {"2x4": 4.00}, hst=0.15)
|
||
line = next(ln for ln in est.lines if ln.stock == "2x4")
|
||
assert line.qty == 2 and line.unit_price == 4.00 and line.total == 8.00
|
||
assert est.subtotal == 8.00
|
||
assert est.tax == 1.20
|
||
assert est.total == 9.20
|
||
|
||
|
||
def test_plywood_priced_per_sheet():
|
||
s = Scene()
|
||
s.place("ply-3/4", 40, width_in=20)
|
||
s.place("ply-3/4", 40, width_in=20) # both fit one sheet
|
||
plan = build_cut_plan(s)
|
||
est = P.estimate(plan, {"ply-3/4": 63.98}, hst=0.0)
|
||
line = next(ln for ln in est.lines if ln.stock == "ply-3/4")
|
||
assert line.qty == 1 and line.unit_price == 63.98 and est.total == 63.98
|
||
|
||
|
||
def test_lumber_price_scales_with_stick_length():
|
||
s = Scene()
|
||
s.place("2x4", 40)
|
||
plan = build_cut_plan(s, settings=ShopSettings(stick_len_in=192)) # 16' sticks
|
||
est = P.estimate(plan, {"2x4": 4.00}) # priced per 8' stick
|
||
line = next(ln for ln in est.lines if ln.stock == "2x4")
|
||
assert line.unit_price == 8.00 # 192/96 × $4
|
||
|
||
|
||
def test_missing_price_is_flagged_not_invented():
|
||
s = Scene()
|
||
s.place("2x4", 40)
|
||
plan = build_cut_plan(s)
|
||
est = P.estimate(plan, {}) # empty book
|
||
assert est.missing == ["2x4"]
|
||
assert est.subtotal == 0.0
|
||
assert "No price on file" in P.format_estimate(est)
|
||
|
||
|
||
def test_save_and_load_roundtrip(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
||
P.save_prices({"2x4": 3.49, "ply-3/4": 59.99})
|
||
loaded = P.load_prices()
|
||
assert loaded["2x4"] == 3.49 and loaded["ply-3/4"] == 59.99
|
||
# untouched stocks still come from defaults
|
||
assert loaded["2x6"] == P.DEFAULT_PRICES["2x6"]
|
||
|
||
|
||
def test_corrupt_price_file_falls_back_to_defaults(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
||
path = P._config_path()
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text("{ not json")
|
||
assert P.load_prices()["2x4"] == P.DEFAULT_PRICES["2x4"]
|
||
|
||
|
||
def test_format_estimate_empty():
|
||
assert "nothing to buy" in P.format_estimate(P.estimate(build_cut_plan(Scene())))
|
||
|
||
|
||
def test_parse_price_reads_json_ld():
|
||
html = ('<script type="application/ld+json">'
|
||
'{"@type":"Product","offers":{"@type":"Offer","price":"3.98"}}</script>')
|
||
assert P._parse_price(html) == 3.98
|
||
|
||
|
||
def test_parse_price_none_when_absent():
|
||
assert P._parse_price("<html><body>no price here</body></html>") is None
|
||
|
||
|
||
def test_material_multiplier_scales_price():
|
||
from woodshop.scene import Scene
|
||
s = Scene()
|
||
s.place("1x4", 24)
|
||
s.set_material("p1", "oak")
|
||
plan = build_cut_plan(s)
|
||
est = P.estimate(plan, prices={"1x4": 5.0}, hst=0.0,
|
||
multipliers={"oak": 3.0, "spruce": 1.0})
|
||
line = next(ln for ln in est.lines if ln.stock == "1x4")
|
||
assert line.material == "oak"
|
||
assert line.unit_price == 15.0 # 5 × 3.0
|
||
assert "oak" in line.label
|
||
|
||
|
||
def test_default_species_priced_at_base():
|
||
from woodshop.scene import Scene
|
||
s = Scene()
|
||
s.place("2x4", 24) # default spruce, mult 1.0
|
||
plan = build_cut_plan(s)
|
||
est = P.estimate(plan, prices={"2x4": 4.0}, hst=0.0)
|
||
line = next(ln for ln in est.lines if ln.stock == "2x4")
|
||
assert line.unit_price == 4.0 and line.label == "2x4"
|
||
|
||
|
||
def test_mixed_species_same_stock_priced_separately():
|
||
from woodshop.scene import Scene
|
||
s = Scene()
|
||
s.place("1x4", 24)
|
||
s.place("1x4", 24)
|
||
s.set_material("p2", "walnut")
|
||
plan = build_cut_plan(s) # groups (1x4,spruce) and (1x4,walnut)
|
||
est = P.estimate(plan, prices={"1x4": 5.0}, hst=0.0,
|
||
multipliers={"spruce": 1.0, "walnut": 5.0})
|
||
prices = {ln.material: ln.unit_price for ln in est.lines}
|
||
assert prices["spruce"] == 5.0 and prices["walnut"] == 25.0
|
||
|
||
|
||
def test_multipliers_save_load(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
||
P.save_material_multipliers({"oak": 3.5, "walnut": 6.0})
|
||
loaded = P.load_material_multipliers()
|
||
assert loaded["oak"] == 3.5 and loaded["walnut"] == 6.0
|
||
assert loaded["spruce"] == P.DEFAULT_MATERIAL_MULTIPLIERS["spruce"]
|