"""144-2~4 B09 바탕 단가 셈 — 엑셀 검산 대조 · 거울(`B09_Estimation_Calc_Base.py` ↔ `.ts`). 견본 = 기번3 공통 엑셀(`2024년 간선임도사업(기번3).xlsx`) 같은 입력: 노무비목록표 E10~E12 · 환율및기초자료 G6~G8 · 경비목록표 E4 · E5 · E7 · E13 · E16 · E21 · 자재단가대비표 5~12줄 · 중기사용료 1 · 2 · 4 · 11 · 14 · 16 호표(F11 126,000 …) """ import json import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B09_Estimation.B09_Estimation_Calc_Base import ( # noqa: E402 compute_base, round_half, rounddown, roundup, ) IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) FACTOR = {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"} LABOR = [ {"key": "LB000048", "code": "1048", "name": "건설기계운전사", "price": 267360}, {"key": "LB000049", "code": "1049", "name": "화물차운전사", "price": 226709}, {"key": "LB000050", "code": "1050", "name": "일반기계운전사", "price": 161142}, {"key": "LB000002", "code": "1002", "name": "보통인부", "price": None}, ] def _m(key, name, price, loss, fuel, lph, misc, crew_code): return {"key": key, "name": name, "spec": "", "price": price, "loss": loss, "fuel": fuel, "fuel_lph": lph, "misc_pct": misc, "crew": 1 if crew_code else None, "crew_code": crew_code} # fmt: skip MACHINES = [ _m("X00001", "불도우저19ton", 184499, 1811, "경유", 25, 16, "1048"), _m("X00003", "굴삭기(0.2m3)", 62550, 2085, "경유", 5, 21, "1048"), _m("X00005", "굴삭기(0.7m3)", 110926, 2085, "경유", 11.6, 22, "1048"), _m("X00032", "덤프자동덮개", 1546, 2684, None, None, None, None), _m("X00048", "콘크리트믹서0.45m3", 5409, 3039, "휘발유", 3.9, 2, "1050"), _m("X00074", "덤프트럭2.5ton(토사)", 20793, 2967, "경유", 2.9, None, "1049"), _m("X09999", "기준가 없는 기계", None, 2000, "중유", 3, 10, "1048"), ] def _mt(key, name, **prices): return {"key": key, "name": name, "spec": "", "unit": "", "origin": "M01", "prices": prices} MATERIALS = [ _mt("MO1", "휘발유", 관급=1369, 물가정보=0, 물가자료=0), _mt("MO2", "경유", 관급=1272), _mt("M3", "각재", 관급=500661, 물가정보=829341, 물가자료=748503), _mt("M4", "못", 관급=1503, 물가정보=1935, 물가자료=1665), _mt("M8", "화강석", 관급=0, 물가정보=288000, 물가자료=288000), _mt("M9", "사용자 값 줄", 물가자료=1000), _mt("b08:sand", "골재"), ] INPUTS = { "labor": LABOR, "machines": MACHINES, "materials": MATERIALS, "fuel_keys": {"경유": "MO2", "휘발유": "MO1"}, "driver_factor": FACTOR, "fx": [ {"key": "FX000001", "name": "USD", "unit": "원/USD", "value": 1414.9, "date": "2026-08-14"} ], } USER = {"materials": {"M9": {"applied": 950, "supply": "관급"}, "M4": {"supply": "별도"}, "b08:sand": {"applied": None}}} # fmt: skip def _run_ts(inputs, user): calc = ROOT / "B09_Estimation" / "B09_Estimation_Calc_Base.ts" body = ( f"\nconst i = {json.dumps(inputs, ensure_ascii=False)};" f"\nconst u = {json.dumps(user, ensure_ascii=False)};" "\nconsole.log(JSON.stringify(computeBase(i, u)));" ) script = IMPORTS.sub("", calc.read_text(encoding="utf-8")) + body result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_끝수_엑셀_자리(): assert rounddown(33412.7689, 1) == 33412.7 and rounddown(126000.9) == 126000 assert rounddown(-1.25, 1) == -1.2 # 0 쪽으로 assert roundup(69849380, -3) == 69850000 and roundup(69850000, -3) == 69850000 assert round_half(2.5) == 3 and round_half(0.125, 2) == 0.13 and round_half(-2.5) == -3 assert rounddown(0.1 + 0.2, 1) == 0.3 # 떠도는 끝수는 버리지 않음 def test_144_2_노무_경비_환율_기초자료(): s = compute_base(INPUTS, {}) assert [r["price"] for r in s["labor"]] == [267360, 226709, 161142, None] assert ("labor", "LB000002") in {(g["tab"], g["key"]) for g in s["gaps"]} hours = {d["key"]: d["hour"] for d in s["basis"]["drivers"]} assert hours == {"1048": 55700, "1050": 33571, "1049": 47231} # 기초자료 G6 · G8 · G7 assert s["basis"]["drivers"][0]["factor"] == "1/8 × 16/12 × 25/20" assert [r["price"] for r in s["expense"]][:3] == [ 184499, 62550, 110926, ] # 경비목록표 E4 · E5 · E7 assert s["expense"][0]["unit"] == "천원" assert s["basis"]["fx"][0]["value"] == 1414.9 def test_144_3_자재단가대비_최저_사용자_관급(): s = compute_base(INPUTS, USER) rows = {r["key"]: r for r in s["compare"]} # 엑셀 O 열 = 최저 · P 열 출처(0 은 뺌 · 같은 값이면 앞 칸 — 화강석 「물정」) assert (rows["MO1"]["applied"], rows["MO1"]["applied_from"]) == (1369, "관급") assert (rows["M3"]["applied"], rows["M3"]["lowest_from"]) == (500661, "관급") assert rows["M4"]["applied"] == 1503 and rows["M4"]["supply"] == "별도" assert (rows["M8"]["applied"], rows["M8"]["applied_from"]) == (288000, "물가정보") assert (rows["M9"]["lowest"], rows["M9"]["applied"], rows["M9"]["applied_from"]) == ( 1000, 950, "사용자", ) assert rows["M9"]["supply"] == "관급" assert rows["b08:sand"]["applied"] is None # 출처 없음 · 사용자 빈칸 = 빈칸 material = {r["key"]: r["price"] for r in s["material"]} assert material == {r["key"]: r["applied"] for r in s["compare"]} # 재료비목록표 = 적용 단가 gaps = [(g["tab"], g["key"]) for g in s["gaps"] if g["tab"] == "compare"] assert gaps == [("compare", "b08:sand")] def test_144_4_중기사용료_엑셀_검산(): s = compute_base(INPUTS, {}) got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]} assert got["X00001"] == (126000, 55700, 36888, 33412) # 중기목록표 4줄 assert got["X00003"] == (76436, 55700, 7695, 13041) assert got["X00005"] == (96829, 55700, 18001, 23128) assert got["X00032"] == (414, 0, 0, 414) # 덮개 — 운전사 · 연료 없음 assert got["X00048"] == (40659, 33571, 5445, 1643) # 일반기계운전사 · 휘발유 · 잡유 2 % assert got["X00074"][1:3] == (47231, 3688) # 화물차운전사 · 잡품 없음 assert got["X09999"] == (None, 55700, None, None) # 기준가 · 중유 단가 없음 = 그 비목 · 합 빈칸 block = s["machine_blocks"][0] assert [ln["amount"] for ln in block["lines"]] == [ 33412.7, 55700, 31800, 5088, ] # 중기사용료 F7~F10 assert block["lines"][0]["qty"] == 0.1811 and block["lines"][0]["ref"] == { "tab": "expense", "key": "X00001", } why = {g["why"] for g in s["gaps"] if g["key"] == "X09999"} assert why == {"연료(중유) 단가 없음", "기계 기준가 없음", "손료 기준가 · 계수 없음"} def test_거울_TS_가_같은_값(): for user in ({}, USER): assert _run_ts(INPUTS, user) == compute_base(INPUTS, user)