"""793-2a 지킴 — 경쟁사 산근 → 우리 엔진 로직 줄 꼴(`M02_San_Emit`) · 엔진 셈 = 엑셀. STmate 233 블록을 로직 JSON 으로(EST 엑셀 = 812-1 엑셀 읽기기 → 원파일 길 · `test_812_1` 이 지킴) 내보내 `master_formula.run` → `master_cut.engine` 으로 셈 — 노 · 재 · 경 · 계 = 엑셀(STmate 계 꼬리 · EST 총괄표) = 참조 평가기(`M02_San_Eval`) · 줄마다(STmate 금액 줄 · EST 돈 · 값 줄) · 계표시(단계) 같음. 단가 전파 견본 = `_분석_649-G2` 4 장(보통인부 → 180,000) · 계약단가(낙찰률)는 B09 몫 — 엔진 계 × 낙찰률 원 버림 = 엑셀 머리만 확인. 엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만 · 없으면 skip). """ from __future__ import annotations import importlib import math from functools import lru_cache import helper_m02_hopyo_golden as golden import pytest from M01_MasterData import M01_MasterData_Store as m01 # noqa: F401 — scripts 길을 둠 from M02_MasterTemplete import M02_San_Book as book from M02_MasterTemplete import M02_San_Emit as em from M02_MasterTemplete import M02_San_Eval as ev from M02_MasterTemplete import M02_San_Stmate as stm mf = importlib.import_module("master_formula") mcut = importlib.import_module("master_cut") ms = importlib.import_module("master_formula_sum") STMATE = { # 파일: (블록, 금액 줄, 계표시 줄, 끝전) "봉화": (62, 349, 184, "비목버림"), "동부청영월": (69, 411, 230, "합버림_큰나머지"), "울진대흥": (47, 261, 128, "비목버림"), "영덕계류": (15, 86, 41, "비목버림"), "울진소광": (40, 208, 112, "비목버림"), } K = ("노무비", "재료비", "경비", "계") LABOR = {"종류": "노임", "이름": "보통인부", "규격": "", "열쇠비목": "L"} def engine(out: dict) -> dict: """내보낸 로직 → 엔진 셈 — 단가 줄은 SK 서버처럼 값을 얹음(요소 대신 값묶음 {비목: 단가}).""" logic = {**out["로직"], "키": "GX990001"} items = [] for item in logic["호표"]: one = {k: v for k, v in item.items() if k not in ("이음", "자원", "단가", "규격")} if "단가" in item: money = dict.fromkeys(K[:3], 0) | {item["비목"]: item["단가"]} one |= {"종류": "로직", "값묶음": money} del one["비목"] items.append(one | {"요소": None}) logic["호표"] = items master = mf.Master({"로직.json": {"그룹": "로직", "줄": [logic]}}) return mcut.engine(master, logic["키"], mf.run(master, logic["키"], {})) def hard(out: dict) -> list[str]: """못 옮김 표지만(「참고 · …」 사람 확인 표지 · 797-1a 는 뺌 — test_797_1a 가 지킴).""" return [m for m in out["원본"]["표지"] if em.NOTE not in m] def money(got: dict) -> tuple: return tuple(int(got[k]) for k in K) @lru_cache(maxsize=8) def _load(tag: str) -> dict: path = golden.FILES[tag] if not path.exists(): pytest.skip(f"실무 엑셀 없음 {path.name}") sheets = book.sheets(path.name, path.read_bytes()) return {"블록": book.stmate_blocks(sheets), "단가": book.stmate_prices(sheets)} def _stmate(tag: str, blk: dict) -> tuple[list, dict]: nodes = stm.parse_block(blk["몸"]) return nodes, em.stmate(blk, nodes, _load(tag)["단가"], STMATE[tag][3]) def _lines_equal(out: dict, got: dict) -> tuple[list, int, int]: """원본 글줄마다 엔진 줄 금액(계) = 엑셀 · 단계 = 계표시 줄 → (틀린 것, 줄 수, 단계 수).""" lines = {x["이름"]: x for x in got["줄"]} steps = {x["이름"]: x for x in got.get("단계", [])} bad, n_line, n_step = [], 0, 0 for g in out["원본"]["글줄"]: if g.get("값") is None or not g.get("자리") or g["자리"].startswith("mid:"): continue if g["자리"].startswith("단계:"): n_step += 1 have = float(steps[g["자리"][3:]]["계"]) tol = 0.15 else: n_line += 1 have = sum(float(lines[n]["금액"] or 0) for n in g.get("자리들") or [g["자리"]]) tol = 0.05 if abs(have - g["값"]) >= tol: bad.append((g["행"], g["글"].strip(), have, g["값"])) return bad, n_line, n_step @pytest.mark.parametrize("tag", list(STMATE)) def test_stmate_engine_equals_excel(tag: str) -> None: data = _load(tag) blocks, n_lines, n_steps, rule = STMATE[tag] n, wrong, lines, steps = 0, [], 0, 0 for blk in data["블록"]: if not any(ln["G"].strip() for ln in blk["몸"]): continue n += 1 nodes, out = _stmate(tag, blk) got = engine(out) ref = ev.stmate_block(blk, nodes, data["단가"], rule)["계"] want = out["원본"]["엑셀금액"] same = money(got) == tuple(round(want[k]) for k in K) same &= money(got) == (ref["L"], ref["M"], ref["S"], ref["T"]) bad, a, b = _lines_equal(out, got) bad += ms.check(out["로직"], blk["코드"]) + hard(out) lines, steps = lines + a, steps + b if deal := out["원본"].get("계약"): # B09 몫 — 엔진 계 × 낙찰률 원 버림 = 엑셀 머리 cut = [math.floor(round(int(got[k]) * deal["낙찰률"] / 100, 6)) for k in K[:3]] same &= cut == [deal["금액"][k] for k in K[:3]] if not same or bad: wrong.append((blk["코드"], money(got), bad[:3])) assert (n, wrong) == (blocks, []) assert (lines, steps) == (n_lines, n_steps) @pytest.mark.parametrize( "tag, code, labor, before, after", [ ("봉화", "D00102", "L00007", (34119, 11192, 341), (34697, 11192, 346)), ("동부청영월", "D00003", "L00002", (4764, 310, 397), (5097, 309, 397)), ("울진소광", "D01489", "L00016", (182223, 230359, 8152), (185067, 230359, 8209)), ], ) def test_stmate_price_propagates(tag, code, labor, before, after) -> None: blk = next(b for b in _load(tag)["블록"] if b["코드"] == code) _, out = _stmate(tag, blk) assert money(engine(out))[:3] == before assert em.reprice(out["로직"], {"코드": labor}, {"L": 180000}) >= 1 assert money(engine(out))[:3] == after def test_link_keys_m01() -> None: """호표 줄 = 793-2 꼴(종류 · 단가 · 이음 = 1차 줄 판정 M01 키) · 덧칸 자원 · 원본.""" tag = "봉화" blk = next(b for b in _load(tag)["블록"] if b["코드"] == "D00102") out = em.stmate(blk, stm.parse_block(blk["몸"]), _load(tag)["단가"], "비목버림", True) labor = next(x for x in out["로직"]["호표"] if x["이름"] == "보통인부") assert {k: v for k, v in labor.items() if k != "자원"} == { "이름": "보통인부", "규격": "", "단위": "인", "수량": "(2 / 50)", "종류": "인력", "비목": "노무비", "단가": 165545, "이음": "LB000002", "번지": "A02", } assert labor["자원"] == { "꼴": "STmate", "갈래": "인력", "코드": "L00007", "이름": "보통인부", "규격": None, "단위": "인", "단가": {"L": 165545, "M": 0, "S": 0}, "비목": "L", "옮김": None, } src = out["원본"] assert {k: src[k] for k in ("시트", "갈래", "코드", "행")} == { "시트": "단가산출근거", "갈래": "STmate", "코드": "D00102", "행": [2912, 2937], } assert src["엑셀금액"] == {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652} assert next(g for g in src["글줄"] if g.get("자리") == "보통인부")["값"] == 6621.8 # ── 엑셀 없이 도는 문법 견본(793-1 `test_stmate_grammar_sample` + 다시 대입 · 뒤 줄) ── def test_stmate_grammar_sample_engine() -> None: prices = { "M00081": {"T": 11468, "L": 0, "M": 11468, "S": 0, "meta": {}, "이름": "합판(내수)"}, "L00007": {"T": 165545, "L": 165545, "M": 0, "S": 0, "meta": {}, "이름": "보통인부"}, "L00099": {"T": 2, "L": 2, "M": 0, "S": 0, "meta": {"UNT": "L%"}, "이름": "공구"}, } text = [ "'합판(내수) :'&M00081& * 1.03 * 0.327 :=A01", ";;Q = 2.5 / 3 =", "'보통인부' &L00007& * [Q] / 4 :=A02", "&PA01M:M& * 11'%' =", "&PA02L:S& * 1'%' = // 공구손료", "(=)", "&L00099& * 2 =", "(-=)", "'별도' &M00081& * 2 =", "Q = 1 / 3 =", "&L00099& * [Q] =", "'뒤 인부' &L00007& * 0.01 =", ] rows = [{"행": i, "G": g} for i, g in enumerate(text)] nodes = stm.parse_block(rows) block = {"코드": "D00001", "이름": "견본", "꼬리": [], "몸": rows} block["머리"] = {"A": "", "B": "", "C": 0, "D": 0, "E": 0, "F": 0} out = em.stmate(block, nodes, prices, "비목버림") logic = out["로직"] assert logic["중간"] == [ {"이름": "q", "식": "(2.5 / 3)", "표시": 2}, {"이름": "q_2", "식": "(1 / 3)", "표시": 2}, # 다시 대입 = 새 이름 ] assert ms.check(logic, "견본") == [] and out["원본"]["표지"] == [] got = engine(out) lines, tot = ev.run_stmate(nodes, prices) want = ev.block_total(tot, "비목버림") assert money(got) == (want["L"], want["M"], want["S"], want["T"]) by = {x["이름"]: x for x in got["줄"]} assert by["보통인부"]["비목"]["노무비"] == 34350.5 # 165,545 × 0.83 / 4 assert by["공구"]["비목"]["노무비"] == 687 # 노무 누계 × 2 % · (-=) 로 합에서 뺌 assert logic["덧줄"][-1]["식"] == "(노무비 - 줄.'뒤 인부') * ((q_2)) / 100" # 뒤 줄을 뺀 누계 assert ( float(by["공구_2"]["비목"]["노무비"]) == lines[10]["값"]["L"] == 113.3 ) # 34,350.5 × 0.33 %