woodshop/tests/test_prices.py

123 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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