"""707-2 원가계산서 요율 · 707-3 기초 단가표 불러오기 — 실무 xlsx 넷(EST 거창 · STmate 영월 · 울진 대흥 · 울진 소광) · 이음 규칙 · 덮개 지킴(새 판 [업데이트] 뒤). 총액 대조 = 엑셀 비목 합 + 실은 요율 · 손입력으로 우리 식(`compute_total`) → 엑셀 총공사비: 대흥 = 같음 · 영월 = 「국가지점번호판검증수수료」 762,000 원만 다름(우리 표에 줄 없음) · 거창 = 같음(708-2 퇴직공제부금비 셈 뒤) · 소광 = 산출근거 빈칸 율 넷 · 폐기물처리 줄. """ import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B09_Estimation import B09_Estimation_Import_Prices as ip # noqa: E402 from B09_Estimation import B09_Estimation_Import_Rates as ir # noqa: E402 from B09_Estimation import B09_Estimation_Store as store # noqa: E402 from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402 from B09_Estimation.B09_Estimation_Rates import rate_table # noqa: E402 K = ROOT / "resources" / "knowledge" / "original" / "실무문서" FILES = { "거창": "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", "영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", "대흥": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx", "소광": "25년 산불진화임도(기번8)(울진소광)/" "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", } def _file(tag): path = K / FILES[tag] if not path.is_file(): pytest.skip(f"실무 xlsx 없음 — {tag}") return path.name, path.read_bytes() @pytest.fixture(scope="module") def rates(): return {tag: ir.read_rates(*_file(tag)) for tag in FILES} def test_707_2_요율_짝(rates): dh = rates["대흥"] assert dh["kind"] == "STmate" and rates["거창"]["kind"] == "EST" assert len(dh["rates"]) == 13 # 율 줄 10 + 일반관리비 · 이윤 · 부가세 assert (dh["rates"]["indirect_labor"], dh["rates"]["safety"], dh["rates"]["profit"]) == ( 14.5, 1.86, 15.0, ) assert dh["estimate"]["safety_base"] == 5349000 # 「x1.86% + 5,349,000」 assert dh["estimate"]["procurement_fee_pct"] == 0.54 # 375,160 ÷ 69,474,220 gc = rates["거창"] # EST 요율 칸 · 건강보험 글 「13.595%」 대신 칸 3.595 assert (gc["rates"]["health"], gc["rates"]["care"], gc["rates"]["retirement"]) == ( 3.595, 13.14, 2.3, ) assert gc["estimate"]["safety_base"] == 0 # 글에 + 기초액 없음 assert gc["notes"] == [] # 708-2 퇴직공제부금비 = 셈(「셈 없음」 없음) sk = rates["소광"] assert sum("율 없음" in n for n in sk["notes"]) == 4 # 산출근거 빈칸 — 지금 율 그대로 assert any("페기물처리" in n for n in sk["notes"]) def test_707_2_총액_대조(rates): got = {tag: ir.check(r, {}, rate_table()) for tag, r in rates.items()} assert got["대흥"]["grand"] == got["대흥"]["excel_grand"] == 1201879000 assert got["대흥"]["differ"] == [] assert got["영월"]["excel_grand"] - got["영월"]["grand"] == 762000 # 국가지점번호판 줄 몫 assert set(got["영월"]["differ"]) == {"도급공사비", "총공사비"} # 708-2 — 퇴직공제부금비 5,603,907(직노 × 2.3 %)가 순공사원가에 듦 → 총공사비 = 엑셀 assert got["거창"]["grand"] == got["거창"]["excel_grand"] == 744653000 assert got["거창"]["differ"] == [] lines = {ln["key"]: ln for ln in got["거창"]["lines"]} assert lines["retirement"]["ours"] == lines["retirement"]["excel"] == 5603907 def test_707_3_단가표_읽기(): counts = {} for tag in FILES: got = ip.read_prices(*_file(tag)) counts[tag] = tuple(len(got[k]) for k in ip.KINDS) assert counts == {"거창": (17, 52, 22), "영월": (16, 55, 29), "대흥": (12, 39, 25), "소광": (14, 35, 24)} # fmt: skip BASE = { "labor": [ {"key": "LB1", "name": "석공", "spec": "", "unit": "인", "price": 268908}, {"key": "LB2", "name": "보통인부", "spec": "", "unit": "인", "price": 172068}, ], "machine": [ { "key": "EQ1", "name": "굴착기(무한궤도)", "spec": "0.7㎥", "labor": 1, "material": 2, "expense": 3, }, { "key": "EQ2", "name": "굴착기(무한궤도)", "spec": "0.2㎥", "labor": 1, "material": 2, "expense": 3, }, ], # fmt: skip "compare": [ {"key": "MO1", "name": "휘발유", "spec": "전국평균", "unit": "L", "applied": 1863.25}, {"key": "MT1", "name": "레미콘", "spec": "25-18-80", "unit": "㎥", "applied": 92330}, ], } def _m(name, spec, n): return {"name": name, "spec": spec, "unit": "hr", "labor": n, "material": n, "expense": n} READ = { "labor": [ {"name": "석 공", "spec": "", "unit": "인", "price": 258935}, {"name": "보통인부", "spec": "", "unit": "인", "price": 172068}, # 책과 같음 {"name": "벌 목 부", "spec": "", "unit": "인", "price": 253243}, ], "machine": [ _m("굴삭기(무한궤도)", "0.7㎥", 10), _m("굴삭기(타이어형)", "0.7㎥", 11), # 타이어 = 다른 기계 _m("굴삭기+브레이카", "0.7㎥", 12), _m("굴삭기(무한궤도)", "0.2㎥", 13), _m("굴삭기(무한궤도)", "0.2㎥", 14), # 같은 책 줄에 둘 = 못 고름 ], "material": [ {"name": "휘 발 유", "spec": "", "unit": "ℓ", "price": 1473}, # 이름 · 단위(ℓ = L) {"name": "레미콘(관급)", "spec": "25-18-80", "unit": "m3", "price": 92330}, ], } def test_707_3_이음_규칙(): user = {"labor_cover": {"LB2": 1}, "materials": {"MO1": {"pick": "유가", "supply": "관급"}}} out = ip.propose(READ, BASE, user) patch = out["patch"] assert patch["labor_cover"] == {"LB1": 258935} # 같은 값 = 옛 덮개 걷음 assert patch["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}} assert patch["materials"]["MO1"] == {"supply": "관급", "applied": 1473} # pick 뺌 assert "MT1" not in patch["materials"] # 「(관급)」 이름 다름 = 짝 없음 assert "굴삭기(무한궤도) 0.2㎥(짝 여럿)" in out["missing"]["machine"] assert out["counts"]["machine"] == {"read": 5, "linked": 1, "covered": 1, "missing": 4, "book": 2} # fmt: skip assert out["counts"]["labor"]["linked"] == 2 and out["missing"]["labor"] == ["벌 목 부"] def test_707_3_덮개_새_판_뒤_남음(tmp_path): """[업데이트] = 단가 책만 새로 굳힘 — 사용자 값의 노임 · 기계 덮개는 그대로 · 셈은 덮개.""" cover = {"labor_cover": {"LB1": 258935, "": None}, "machine_cover": {"EQ1": {"labor": 10, "material": 10, "expense": 10}, "EQ2": {"labor": 1}}} # fmt: skip saved = store.write_user(tmp_path, cover) assert saved["labor_cover"] == {"LB1": 258935} # 빈 값 버림 assert saved["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}} for price in (268908, 280000): # 옛 판 → 새 판 책 store.write_book(tmp_path, {"rows": {"LB1": {"key": "LB1", "price": price}}}) user = store.read_user(tmp_path) assert user["labor_cover"] == saved["labor_cover"] inputs = {"labor": [{"key": "LB1", "name": "석공", "price": price}]} row = compute_base(inputs, user)["labor"][0] assert (row["price"], row["book"], row["covered"]) == (258935, price, True)