"""STMATE 재현 앱 보고서 양식 변형(G5 · 견본 없는 서식) — REA 35 인쇄 분기·인쇄 양식(DFM)에서 나온 규칙 대조. 원본 출력 견본(엑셀)이 실무 폴더에 없는 서식만 — 정답 = 규칙(서식 → Prn_* 함수 · DFM 머리 칸 차례 · Prn_Won_A 구성비 = 금액 ÷ 총공사비 × 100) 과 같은 STC 로 만든 원본 대조 시트(공사원가계산서 0608 · 설계내역서 0603). """ import json import subprocess import sys import warnings from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools/stmate-test")) sys.path.insert(0, str(Path(__file__).parent)) from test_stmate_report_coll import _forms # noqa: E402 from test_stmate_report_sheets import book # noqa: E402 warnings.filterwarnings("ignore") NAMES = ("봉화2024", "영월2024", "대흥2024") def _node(js): return json.loads(subprocess.check_output(["node", "-e", js], cwd=ROOT, encoding="utf-8")) def test_print_dispatch_table_follows_st_prt_switch(): """ST_Prt 0x44fc34 분기(20000 + 서식 번호) — 0609 = Prn_Won_A · 0608 = Prn_Wonga · 0672 = 0601 과 같은 Prn_Jab · 0606 엑셀판 = 종양식 Prn_Nae_P_EXL · 0695 엑셀판 없음 · 0675~0677 = Prn_Nae_Col{M,L,X}.""" p = _node( "const F=require('./tools/stmate-test/report_forms.js');" "process.stdout.write(JSON.stringify({p:F.FORM_PROC,s:F.FORM_SHEET}))" ) proc, sheet = p["p"], p["s"] assert proc["0609"][0] == "Prn_Won_A" and proc["0608"][0] == "Prn_Wonga" assert proc["0672"][0] == "Prn_Jab" and proc["0606"][1] == "Prn_Nae_P_EXL" assert proc["0695"][0] == proc["0695"][1] == "Prn_Boq_Not" assert [proc[k][0] for k in ("0675", "0676", "0677")] == [ "Prn_Nae_ColM", "Prn_Nae_ColL", "Prn_Nae_ColX", ] assert set(sheet) <= set(proc) @pytest.mark.parametrize("name", NAMES) def test_total_summary_0609_is_cost_statement_rows_with_share(name): """0609 총괄집계표 = 공사원가계산서와 같은 줄 · 금액 · 구성비(T_PER 0.001% 표시) · 비고 — 원가계산서는 원본 엑셀 일치 시트.""" f = _forms(name, {}) won = book(name)["공사원가계산서"]["rows"] want = [r for r in won[4:] if r.get("D") is not None] got = [r for r in f["총괄집계표"]["rows"] if r.get("_k") == "cost"] assert len(got) == len(want) > 10 grand = f["_grand"] for w, g in zip(want, got): label = [x for x in (w.get("A"), w.get("B"), w.get("C")) if x and str(x).strip()][-1] assert g["A"] == str(label).strip() and g["B"] == w["D"] and g["D"] == (w.get("F") or "") share = round(w["D"] / grand * 100, 3) if w["D"] else "" assert g["C"] == share or abs(g["C"] - share) < 1e-9 assert got[-1]["C"] == 100 head = f["총괄집계표"]["rows"][2] assert [head[c] for c in "ABCD"] == ["구 분", "금 액", "구 성 비", "비 고"] @pytest.mark.parametrize("name", NAMES) def test_split_0605_columns_and_full_rate(name): """0605 분할내역서(TPM_NAE_B) — 수량(전체·금회) · 단위 · 단가 · 금액(전체·금회) · 비고 · 100% 이면 금회 = 전체.""" f = _forms(name, {"splitRate": 100}) rows = f["분할내역서"]["rows"] assert rows[2]["D"] == "수 량" and rows[2]["H"] == "금 액" and rows[2]["J"] == "비고" assert rows[3]["D"] == rows[3]["H"] == "전 체" and rows[3]["E"] == rows[3]["I"] == "금 회" for r in rows[4:]: if r.get("_k") == "item": assert r["D"] == r["E"] and r["H"] == r["I"] and r["J"] == r["_code"] elif str(r.get("_k", "")).startswith("T") or r.get("_k") == "final": assert r["H"] == r["I"], r half = _forms(name, {"splitRate": 50})["분할내역서"]["rows"] items = [r for r in half if r.get("_k") == "item" and r["F"] != "%"] assert all(r["I"] == int(r["G"] * r["E"] + 1e-9) for r in items) @pytest.mark.parametrize("name", NAMES) def test_cost_statement_simple_0695_rows(name): """0695 간이공사비 산출명세서(TPM_BOQ_NOT) — 회사분·도급분·계 × 8칸 · 도급분 = 원가계산서 소계·관리비·이윤·총원가·부가세·도급공사비.""" f = _forms(name, {}) rows = [r for r in f["간이명세서"]["rows"] if r.get("_k") == "cost"] assert [r["A"] for r in rows] == ["회사분", "도급분", "계"] dog = rows[1] assert dog["B"] + dog["C"] + dog["D"] + dog["E"] + dog["F"] == dog["G"] assert all(rows[2][c] == rows[0][c] + rows[1][c] for c in "BCDEFGHI") assert rows[2]["I"] == f["_grand"] @pytest.mark.parametrize("name", NAMES) def test_group_collection_and_plan_forms(name): """0675~0677 집계표(TPM_NAE_COL_ML · TPM_NAE_COL) · 0671·0678·0679 투입계획(Plan_K 공종별 · Plan_A 코드 합).""" f = _forms(name, {}) mat = f["재료비집계표"]["rows"] assert [mat[2][c] for c in "ABCEFGHI"][:3] == ["공 종", "작 업 명", "구 분"] eq = f["중장비집계표"]["rows"] assert eq[2]["G"] == "재 료 비" and eq[2]["M"] == "총 액" and eq[3]["N"] == "금 액" for r in eq: if r.get("_k") == "item": assert r["M"] == r["G"] + r["I"] + r["K"] and r["O"] == r["_code"] k = [r for r in f["인력투입계획"]["rows"] if r.get("_k") == "item"] a = [ r for r in _forms(name, {"planAll": True})["인력투입계획"]["rows"] if r.get("_k") == "item" ] by = {} for r in k: by[r["F"]] = by.get(r["F"], 0) + r["D"] assert a and {r["E"]: r["C"] for r in a}.keys() == by.keys() assert all(abs(r["C"] - round(by[r["E"]], 2)) < 0.011 * max(1, len(k)) for r in a) @pytest.mark.parametrize("name", NAMES) def test_hand_form_0303_head_rows_and_lines(name): """0303 수공양식(TPM_ILWI_S) — 호표마다 항목번호·공종·단위 세 줄 · 구성 줄에 총액 칸 없음(K = 비고 코드).""" f = _forms(name, {}) rows = f["일위대가표수공양식"]["rows"] assert rows[2]["K"] == "비고" and "총" not in "".join(str(v) for v in rows[2].values()) kinds = [r.get("_k") for r in rows[4:]] for i, k in enumerate(kinds): if k == "hopyo": assert kinds[i + 1] == kinds[i + 2] == "parent" lines = [r for r in rows if r.get("_k") == "line"] assert lines and all(str(r["K"]).strip() for r in lines) @pytest.mark.parametrize("name", NAMES) def test_gov_and_estimate_forms(name): """0607 사·관급내역서(TPM_BQM) — 「사 급 분」 띠 · 관급 구역 줄 · 계 = 원가 관급 원자재대 / 0615(TPM_NAE_EST) 견적 W 줄만.""" f = _forms(name, {}) gov = f["관급내역서"]["rows"] assert any(r.get("_k") == "band" for r in gov) fin = [r for r in gov if r.get("_k") == "final"][0] items = [r for r in gov if r.get("_k") == "item"] tot = sum(v for r in items for c, v in r.items() if c in "GIKM" and isinstance(v, (int, float))) assert sum(v for c, v in fin.items() if c in "GIKM") == tot est = f["견적단가대비표"]["rows"] assert est[3]["E"] == "견적단가" and est[3]["F"] == "기준단가" assert all(r["_code"][0] == "W" for r in est if r.get("_k") == "item")