78 lines
2.8 KiB
Python
78 lines
2.8 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
|