"""791-1 엔진 셈 의미 지킴 시험 — 금액번지 · 소계 단계 · 제외 · 줄 자리 · 블록 끝전 · 결과 조정 · 특수 % · 변수 표시. ① 전후 같음 — M01 로직 전부(입력 = 자동값 + 기본값)의 날 셈 · 끝수 셈 지문 = 고치기 전 뜬 지문 (`fixtures/m01_791_1_logic_fingerprint.json` · 마스터 데이터가 바뀌면 건너뜀 → 이 파일을 바로 돌려 다시 뜸). ② 새 의미 단위 시험 · 검사. ③ 실무 손 옮김 견본 넷 — 원문 글(STmate 숨은 G 열 · EST 보이는 글)을 사람이 로직 꼴로 옮김 · 단가 = 그 엑셀 목록표 값 · 노 · 재 · 경 · 계 = 엑셀(`helper_m02_hopyo_golden.FILES` · 649-G2). """ import hashlib import importlib import json import sys from decimal import Decimal from pathlib import Path import pytest from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠 mcut = importlib.import_module("master_cut") mf = importlib.import_module("master_formula") ms = importlib.import_module("master_formula_sum") ROOT = Path(__file__).resolve().parents[2] FINGER = Path(__file__).resolve().parent / "fixtures" / "m01_791_1_logic_fingerprint.json" COSTS = ("노무비", "재료비", "경비", "계") D = Decimal # ── ① 전후 같음 ──────────────────────────────────────────────────────── def _master_hash() -> str: h = hashlib.sha256() for path in sorted((ROOT / "resources" / "master_data").glob("*.json")): if not path.name.startswith("_"): raw = path.read_bytes().replace(b"\r\n", b"\n") # 줄바꿈(CRLF/LF) 무관 h.update(path.name.encode() + raw) return h.hexdigest()[:16] def _given(whole, row) -> dict: import master_auto as ma given = ma.values(whole, row) given.update({k: v for k, v in ma.defaults(whole, row).items() if v is not None}) return given def _print(whole, row) -> str: key = str(row["키"]) try: raw = mf.run(whole, key, _given(whole, row)) text = json.dumps(raw, default=str, sort_keys=True, ensure_ascii=False) cut = mcut.engine(whole, key, raw) text += json.dumps(cut, default=str, sort_keys=True, ensure_ascii=False) except Exception as e: # 멈춤도 지문 — 멈춤 까닭이 바뀌어도 잡음 text = f"ERR {type(e).__name__} {e}" return hashlib.sha256(text.encode()).hexdigest()[:16] def fingerprints() -> dict: files, whole = m01.loaded() rows = [r for d in files.values() if d.get("그룹") == "로직" for r in d.get("줄", [])] return {str(r["키"]): _print(whole, r) for r in rows} def test_M01_로직_전부_전후_같음(): saved = json.loads(FINGER.read_text(encoding="utf-8")) if saved["마스터"] != _master_hash(): pytest.skip("마스터 데이터 바뀜 — 지문 다시 뜸: python resources/tester/test_791_1_…py") now = fingerprints() assert len(now) == len(saved["결과"]) >= 1515 diff = sorted(k for k in now if now[k] != saved["결과"].get(k)) assert not diff, f"셈 결과 바뀐 로직 {len(diff)} 개: {diff[:10]}" # ── 시험 마스터 ─────────────────────────────────────────────────────── def _master(logics: list[dict], labor: dict, mats: dict | None = None) -> mf.Master: """시험 전용 색인 — 인력 LB99 · 자재품목 MT99(물가자료) · 로직 GX99.""" files = { "인력.json": { "그룹": "인력", "줄": [ {"키": k, "이름": n, "단위": "원/일", "값": D(v)} for k, (n, v) in labor.items() ], }, "재료_자재품목.json": { "그룹": "재료", "줄": [ {"키": k, "이름": n, "단위": u, "물가자료": D(v)} for k, (n, u, v) in (mats or {}).items() ], }, "로직_시험.json": {"그룹": "로직", "줄": logics}, } return mf.Master(files) def _calc(master, key: str, given: dict | None = None) -> dict: return mcut.engine(master, key, mf.run(master, key, given or {})) def _money(got: dict) -> dict: return {k: got[k] for k in COSTS} def _line(got: dict, name: str) -> dict: return next(x for x in got["줄"] if x["이름"] == name) def _step(got: dict, name: str) -> dict: return next(x for x in got["단계"] if x["이름"] == name) def _man(key, qty, name, **more): return { "종류": "인력", "요소": key, "이름": name, "단위": "인", "수량": qty, "비목": "노무비", **more, } def _mat(key, qty, name, **more): return { "종류": "재료", "요소": key, "이름": name, "단위": "식", "수량": qty, "비목": "재료비", **more, } def _machine(qty, name, bundle): """기계 시간당 노 · 재 · 경 = 엑셀 중기목록표 값 그대로(M02 `값묶음` 길).""" return { "종류": "로직", "요소": "로직(GX990009)", "이름": name, "단위": "hr", "수량": qty, "값묶음": dict(zip(("노무비", "재료비", "경비"), map(D, bundle))), } _HOUR = { "키": "GX990009", "이름": "시간당 기계경비(시험)", "결과단위": "원/시간", "입력": [], "호표": [], } def _logic(key: str, **body) -> dict: return {"키": key, "이름": key, "결과단위": "원/식", "입력": [], **body} # ── ② 새 의미 단위 시험 ──────────────────────────────────────────────── LAB = {"LB990001": ("보통인부", 100000), "LB990002": ("특별인부", 200000)} MAT = {"MT990001": ("자재 갑", "식", 1000), "MT990002": ("자재 을", "식", 3000)} def _base(**body) -> mf.Master: row = _logic( "GX990001", 호표=[ _mat("MT990001", "1.23", "갑", 번지="A01"), _mat("MT990002", "0.5", "을", 번지="A01"), _man("LB990001", "0.0123", "보통", 번지="A02"), _man("LB990002", "0.01", "특별", 번지="A02"), ], **body, ) return _master([row], LAB, MAT) def test_옛_로직은_새_칸_없음(): master = _base() for item in master.logic("GX990001")["호표"]: del item["번지"] got = _calc(master, "GX990001") assert "단계" not in got and not ms.uses(master.logic("GX990001")) assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 0, "계": 5960} def test_번지_율_밑(): master = _base(덧줄=[{"이름": "잡재료", "식": "번지.A01.재료비 * 10 / 100", "비목": "재료비"}]) got = _calc(master, "GX990001") assert _line(got, "잡재료")["금액"] == D("273.0") assert got["재료비"] == 3003 def test_단계_누계_제외_합밖(): master = _base( 덧줄=[{"이름": "공구손료", "식": "단계.'인부 소계'.노무비 * 3 / 100", "비목": "경비"}], 단계=[ {"이름": "자재 소계", "단": 1, "줄": ["갑", "을"]}, {"이름": "인부 소계", "단": 1, "줄": ["보통", "특별"]}, {"이름": "경비 소계", "단": 1, "줄": ["공구손료"], "제외": True}, {"이름": "계", "단": 2}, ], ) got = _calc(master, "GX990001") assert _step(got, "인부 소계")["계"] == D("3230.0") assert _line(got, "공구손료")["금액"] == D("96.9") assert _step(got, "경비 소계") == { "이름": "경비 소계", "단": 1, "노무비": 0, "재료비": 0, "경비": D("96.9"), "계": D("96.9"), "제외": True, } assert _step(got, "계")["계"] == D("5960.0") # 제외 단계는 위로 안 올라감 assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 0, "계": 5960} # 합밖 = 번지엔 쌓되 합에서 뺌 row = master.logic("GX990001") row["호표"][1]["합밖"] = True got = _calc(master, "GX990001") assert got["재료비"] == 1230 and _step(got, "자재 소계")["재료비"] == D("2730.0") def test_줄_자리_셋(): for way, want in (("반올림", 124), ("올림", 124), ("버림", 123)): # 123.5 master = _base( 덧줄=[{"이름": "덧", "식": "123.5", "비목": "경비", "자리": {"방법": way, "자리": 0}}] ) assert _line(_calc(master, "GX990001"), "덧")["금액"] == want master = _base( 덧줄=[ {"이름": "덧", "식": "123.456", "비목": "경비", "자리": {"방법": "반올림", "자리": 2}} ] ) assert _line(_calc(master, "GX990001"), "덧")["금액"] == D("123.46") def test_블록_끝전_둘(): body = {"덧줄": [{"이름": "경", "식": "0.6", "비목": "경비"}]} # 노 3230.0 · 재 2730.0 · 경 0.6 assert _calc(_base(**body), "GX990001")["계"] == 5960 got = _calc(_base(끝전="큰나머지", **body), "GX990001") assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 0, "계": 5960} # 원 미만이 모여 1 원이 되면 큰 나머지 비목에 · 같으면 경 › 재 › 노 body = { "덧줄": [ {"이름": "노", "식": "0.6", "비목": "노무비"}, {"이름": "경", "식": "0.6", "비목": "경비"}, ] } assert _calc(_base(**body), "GX990001")["계"] == 5960 got = _calc(_base(끝전="큰나머지", **body), "GX990001") assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 1, "계": 5961} def test_결과금액_조정(): got = _calc(_base(조정={"옮김": "경비"}), "GX990001") assert _money(got) == {"노무비": 0, "재료비": 0, "경비": 5960, "계": 5960} got = _calc(_base(조정={"배율": {"노무비": D("0.5")}}), "GX990001") assert _money(got) == {"노무비": 1615, "재료비": 2730, "경비": 0, "계": 4345} assert mf.run(_base(조정={"옮김": "재료비"}), "GX990001", {})["재료비"] == D("5960.0") def test_특수_퍼센트_누계와_앞_소계(): master = _base( 덧줄=[ {"이름": "L%", "식": "노무비 * 2 / 100", "비목": "경비"}, # 처음부터 노무비 누계 {"이름": "T%", "식": "단계.'소계'.계 * 0.8", "비목": "경비"}, # 바로 앞 소계 × 수량 ], 단계=[{"이름": "소계", "단": 1, "줄": ["갑", "을"]}], ) got = _calc(master, "GX990001") assert _line(got, "L%")["금액"] == D("64.6") assert _line(got, "T%")["금액"] == D("2184.0") def test_변수_표시_반올림(): row = _logic( "GX990002", 중간=[{"이름": "f", "식": "1 / 1.3", "표시": 2}, {"이름": "g", "식": "1 / 1.3"}], 결과={"식": "f * 100"}, ) got = mf.run(_master([row], LAB), "GX990002", {}) assert got["결과"] == D("77.00") and got["중간"]["g"] != D("0.77") def test_검사(): def problems(**body) -> list[str]: master = _base(**body) return mf.check_logic(master, master.logic("GX990001"))[0] assert problems(덧줄=[{"이름": "덧", "식": "번지.A01.재료비 * 1", "비목": "경비"}]) == [] assert any( "없는 번지" in x for x in problems(덧줄=[{"이름": "덧", "식": "번지.B09.계", "비목": "경비"}]) ) order = problems( 덧줄=[{"이름": "덧", "식": "단계.'소'.계", "비목": "경비"}], 단계=[{"이름": "소", "단": 1, "줄": ["갑", "덧"]}], ) assert any("단계 순서" in x for x in order) assert any( "자리" in x for x in problems( 덧줄=[{"이름": "덧", "식": "1", "비목": "경비", "자리": {"방법": "버림", "자리": 4}}] ) ) assert any("끝전" in x for x in problems(끝전="합버림")) assert any("단 「6」" in x for x in problems(단계=[{"이름": "소", "단": 6, "줄": ["갑"]}])) assert any("없는 줄" in x for x in problems(단계=[{"이름": "소", "단": 1, "줄": ["없음"]}])) assert any("조정 옮김" in x for x in problems(조정={"옮김": "계"})) assert any( "이 단계" in x for x in problems( 단계=[{"이름": "가", "단": 1, "줄": ["갑"]}, {"이름": "나", "단": 1, "줄": ["갑"]}] ) ) # ── ③ 실무 손 옮김 견본 넷 ───────────────────────────────────────────── def 봉화(): """봉화 단가산출근거 2909~2943 행 — `&M00081& * 1.03 * 0.327 :=A01` · `&PA01M:M& * 11'%'` · `&PA02L:S& * 1'%'` · `(=)` · `(==)` · 총계 45,652 · 34,119 · 11,192 · 341.""" row = _logic( "GX990101", 호표=[ _mat("MT990101", "1.03 * 0.327", "합판(내수)", 번지="A01"), _mat("MT990102", "0.038 * 0.327", "각재", 번지="A01"), _man("LB990101", "5 / 50", "형틀목공", 번지="A02"), _man("LB990102", "2 / 50", "보통인부", 번지="A02"), ], 덧줄=[ {"이름": "소모자재(박리재 등)", "식": "번지.A01.재료비 * 11 / 100", "비목": "재료비"}, { "이름": "공구손료 및 경장비 기계경비", "식": "번지.A02.노무비 * 1 / 100", "비목": "경비", }, ], 단계=[ {"이름": "자재비 소계", "단": 1, "줄": ["합판(내수)", "각재", "소모자재(박리재 등)"]}, { "이름": "설치 소계", "단": 1, "줄": ["형틀목공", "보통인부", "공구손료 및 경장비 기계경비"], }, {"이름": "계", "단": 2}, ], ) master = _master( [row], {"LB990101": ("형틀목공", 274978), "LB990102": ("보통인부", 165545)}, {"MT990101": ("합판(내수)", "㎡", 11468), "MT990102": ("각재", "㎥", 500661)}, ) return master, row, {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652} def test_봉화_D00102_합판거푸집_STmate_번지_율(): master, row, want = 봉화() assert mf.check_logic(master, row)[0] == [] got = _calc(master, "GX990101") assert _money(got) == want assert [x["금액"] for x in got["줄"]] == [ D(x) for x in ("3862.5", "6221.2", "27497.8", "6621.8", "1109.2", "341.1") ] assert [x["계"] for x in got["단계"]] == [D("11192.9"), D("34460.7"), D("45653.6")] def 소광(): """울진소광 32 호표(D01489) — `'공구손료 :'&S01045& * 2'%'`(S01045 단위 L% = 처음부터 노무비 누계) · `L = Q/10 =` 표시 2 · 계 줄 420,734 · 182,223 · 230,359 · 8,152 (계약단가 87.745 % 는 B09 몫).""" row = _logic( "GX990102", 중간=[ {"이름": "N", "식": "40"}, {"이름": "Q", "식": "200"}, {"이름": "L", "식": "Q / 10", "표시": 2}, {"이름": "Q1", "식": "360"}, {"이름": "A", "식": "5.2"}, ], 호표=[ _mat("MT990103", "1", "낙석방지책"), _man("LB990103", "1 / N", "용접공"), _man("LB990104", "3 / N", "특별인부(지주)"), _man("LB990105", "2 / N", "보통인부(지주)"), _machine("1 / N * 8", "트럭탑재형 크레인 10톤", (49479, 16797, 22539)), _man("LB990104", "4 / Q * L", "특별인부(와이어)"), _man("LB990105", "2 / Q * L", "보통인부(와이어)"), _man("LB990104", "4 / Q1 * A", "특별인부(철망)"), _man("LB990105", "2 / Q1 * A", "보통인부(철망)"), ], 덧줄=[{"이름": "공구손료", "식": "노무비 * 2 / 100", "비목": "경비"}], 단계=[ {"이름": "재료비 계", "단": 2, "줄": ["낙석방지책"]}, {"이름": "인건비 소계", "단": 1, "줄": ["용접공", "특별인부(지주)", "보통인부(지주)"]}, {"이름": "크레인 소계", "단": 1, "줄": ["트럭탑재형 크레인 10톤"]}, {"이름": "지주 계", "단": 2}, {"이름": "와이어 계", "단": 2, "줄": ["특별인부(와이어)", "보통인부(와이어)"]}, {"이름": "철망 계", "단": 2, "줄": ["특별인부(철망)", "보통인부(철망)"]}, {"이름": "공구손료 계", "단": 2, "줄": ["공구손료"]}, {"이름": "합계", "단": 3}, ], ) master = _master( [row, _HOUR], { "LB990103": ("용접공", 278326), "LB990104": ("특별인부", 221506), "LB990105": ("보통인부", 169804), }, {"MT990103": ("낙석방지책", "경간", 227000)}, ) return master, row, {"노무비": 182223, "재료비": 230359, "경비": 8152, "계": 420734} def test_울진소광_D01489_낙석방지책_STmate_L퍼센트(): master, row, want = 소광() assert mf.check_logic(master, row)[0] == [] got = _calc(master, "GX990102") assert _money(got) == want assert _line(got, "공구손료")["금액"] == D("3644.4") assert _step(got, "지주 계")["계"] == D("49824.2") assert _step(got, "합계")["계"] == D("420735.3") def 영월(): """동부청영월 단가산출근거 54~103 행(D00003) — `f = 1/1.3 =` · `E=…=` · `Q = 3600*q*K*f*E/Cm =`(표시 2) · `&X00205L/M/S& / Q * 0.9` · 계 원 버림 + 큰 나머지 5,471 · 4,764 · 310 · 397.""" row = _logic( "GX990103", 중간=[ {"이름": "q", "식": "0.7"}, {"이름": "f", "식": "1 / 1.3", "표시": 2}, {"이름": "K", "식": "0.9"}, {"이름": "Cm", "식": "20"}, {"이름": "E", "식": "(0.7 + 0.6) / 2 - 0.05", "표시": 2}, {"이름": "Q", "식": "3600 * q * K * f * E / Cm", "표시": 2}, ], 호표=[ _man("LB990106", "(0.2 + 0.26) / 2 * 0.1", "인부"), _machine("1 / Q * 0.9", "유압식 백호우 0.7㎥", (55700, 18015, 23128)), ], 단계=[ {"이름": "인력 소계", "단": 1, "줄": ["인부"]}, {"이름": "기계 소계", "단": 1, "줄": ["유압식 백호우 0.7㎥"]}, {"이름": "계", "단": 2}, ], 끝전="큰나머지", ) master = _master([row, _HOUR], {"LB990106": ("보통인부", 165545)}) return master, row, {"노무비": 4764, "재료비": 310, "경비": 397, "계": 5471} def test_동부청영월_D00003_터파기_STmate_큰나머지_끝전(): master, row, want = 영월() assert mf.check_logic(master, row)[0] == [] got = _calc(master, "GX990103") assert got["중간"]["Q"] == D("52.39") assert _money(got) == want assert _step(got, "계")["계"] == D("5471.0") row["끝전"] = "비목버림" # 비목 원 버림이면 1 원 모자람 assert _calc(master, "GX990103")["계"] == 5470 def 임도(): """북부청임도 단가산출 18 번(문양거푸집) — `5,786.1 × 2% = 116`(그 줄만 반올림 0) · `인부소계 : 24,467.3` · 노무비 24,467 · 재료비 5,902 · 계 30,369.""" row = _logic( "GX990104", 호표=[ _mat("MT990104", "1 * (1 / 0.72)", "문양거푸집(ABS코팅폼)"), _man("LB990107", "0.07", "형틀목공"), _man("LB990108", "0.03", "보통인부"), ], 덧줄=[ { "이름": "잡재료비", "식": "재료비 * 2 / 100", "비목": "재료비", "자리": {"방법": "반올림", "자리": 0}, } ], 단계=[{"이름": "인부소계", "단": 1, "줄": ["형틀목공", "보통인부"]}], ) master = _master( [row], {"LB990107": ("형틀목공", 275790), "LB990108": ("보통인부", 172068)}, {"MT990104": ("문양거푸집(ABS코팅폼)", "㎡", 4166)}, ) return master, row, {"노무비": 24467, "재료비": 5902, "경비": 0, "계": 30369} def test_북부청임도_18_문양거푸집_EST_줄_자리(): master, row, want = 임도() assert mf.check_logic(master, row)[0] == [] got = _calc(master, "GX990104") assert _line(got, "잡재료비")["금액"] == 116 assert _step(got, "인부소계")["계"] == D("24467.3") assert _money(got) == want del row["덧줄"][0]["자리"] # 기본 0.1 버림이면 엑셀과 1 원 다름 assert _calc(master, "GX990104")["계"] == 30368 if __name__ == "__main__": # 지문 다시 뜸 — 마스터 데이터가 바뀐 뒤 · 엔진 고치기 전 커밋에서만 data = {"마스터": _master_hash(), "결과": fingerprints()} FINGER.write_text( json.dumps(data, ensure_ascii=False, indent=1, sort_keys=True) + "\n", encoding="utf-8" ) print(len(data["결과"]), "개 지문", FINGER, file=sys.stderr)