"""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"] == ["구 분", "금 액", "구 성 비", "비 고"] def _boq_items(name): """BOQ.DBF 품목 줄(SORTCODE 차례) — 엑셀판 Prn_Nae_B_EXL·Prn_Nae_EST_EXL 이 직접 읽는 표.""" from test_stmate_report_sheets import DOCS, PAIRS, server load = server.api_load(server.LoadReq(source_path=str(DOCS / PAIRS[name][0]))) rows = sorted(load["boq_rows"], key=lambda r: float(r.get("SORTCODE") or 0)) skip = ("T", "E", "C", ":") return [r for r in rows if str(r.get("BDCODE1") or "").strip()[:1] not in ("",) + skip] @pytest.mark.parametrize("name", ["영덕변경", "봉화2024"]) def test_split_0605_takes_slot1_and_slot2_columns(name): """0605 분할내역서(Prn_Nae_B_EXL 0x528e00) — 수량 전체 = QTY1 · 금회 = QTY2 · 단가 = 1칸 · 금액 전체·금회 = 1·2칸 금액.""" f = _forms(name, {}) 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"] == "금 회" items = [r for r in rows if r.get("_k") == "item"] raw = _boq_items(name) assert len(items) == len(raw) > 10 assert [r["E"] for r in items] == [float(x["QTY2"] or 0) for x in raw] assert all(r["J"] == r["_code"] for r in items) fin = [r for r in rows if r.get("_k") == "final"][0] if any( float(x["QTY2"] or 0) for x in raw ): # 2칸이 있는 자료(영덕 변경) — 금회 끝 = 2차수 설계내역서 총합계 design = book(name)["설계내역서"]["rows"] dfin = [r for r in design if r.get("_k") == "final"][0] assert fin["I"] in [v for v in dfin.values() if isinstance(v, (int, float))] assert fin["I"] != fin["H"] else: assert fin["I"] == 0 and all(r["E"] == 0 and r["I"] == 0 for r in items) def test_fund_0651_takes_slot1_and_slot2_group_amounts(): """0651 구.시도(Prn_Nae_Ex1_CT_EXL 0x59e818 · PRT.DBF T_AMT1·T_AMT2) — 공종 줄 시기금·구기금 = BOQ 저장 1·2칸 금액 · 합계 = 둘 더하기(영덕 변경 · 2칸 있음).""" from test_stmate_report_sheets import DOCS, PAIRS, server f = _forms("영덕변경", {}) rows = [r for r in f["총괄내역서구시도"]["rows"] if str(r.get("_k", "")).startswith("T")] load = server.api_load(server.LoadReq(source_path=str(DOCS / PAIRS["영덕변경"][0]))) raw = sorted(load["boq_rows"], key=lambda r: float(r.get("SORTCODE") or 0)) raw = [r for r in raw if str(r.get("BDCODE1") or "").strip()[:1] == "T"] assert len(rows) == len(raw) > 5 assert [(r["D"], r["E"]) for r in rows] == [ (float(x["T_AMT1"]), float(x["T_AMT2"])) for x in raw ] assert all(r["F"] == r["D"] + r["E"] for r in rows) head = f["총괄내역서구시도"]["rows"][2] assert ( all(head.get(c) for c in "DEF") and "".join(head["D"].split()) == "시기금" ) # SET_REP FLD_N @pytest.mark.parametrize("name", NAMES) def test_cost_statement_simple_0695_rows(name): """0695 간이공사비 산출명세서(ST_P_XLS Prn_Boq_Not 0x55f168) — B 회사분(0)·도급분·계 × C~J 8칸 · 도급분 소계 = 재+노+경+일반+이윤 · 부가세 = 원가계산서 · 합계 = 소계 + 부가세 · 계 = 회사분 + 도급분.""" f = _forms(name, {}) rows = [r for r in f["간이명세서"]["rows"] if r.get("_k") == "cost"] assert [r["B"] for r in rows] == ["회사분", "도급분", "계"] com, dog, tot = rows assert all(com[c] == 0 for c in "CDEFGHIJ") assert dog["C"] + dog["D"] + dog["E"] + dog["F"] + dog["G"] == dog["H"] assert dog["H"] + dog["I"] == dog["J"] and all(tot[c] == dog[c] for c in "CDEFGHIJ") won = book(name)["공사원가계산서"]["rows"] vat = [r for r in won if "".join(str(r.get("A") or "").split()) == "부가가치세"] assert ( vat and vat[0]["D"] == dog["I"] ) # 합계 = 수식(소계+부가세) — 원가 끝전·추가금액은 안 들어감 head = f["간이명세서"]["rows"][2] assert head["A"] == "구 분" and head["K"] == "작성자" and tot["K"] == "(인)" @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(Prn_Nae_EST_EXL 0x5611dc) — 줄마다 두 행 · 견적단가 = 1칸 단가 · 기준단가 = 2칸 · 아래 행 = 견적 ÷ 기준.""" 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"] == "기준단가" body = est[4:] assert len(body) % 2 == 0 and all(r.get("_k") == "ratio" for r in body[1::2]) items = [r for r in body[0::2] if r.get("_k") == "item"] design = [r for r in book(name)["설계내역서"]["rows"] if r.get("_k") == "item"] assert len(items) == len(design) == len(_boq_items(name)) assert all([e[c] for c in "EGIK"] == [d[c] for c in "FHJL"] for e, d in zip(items, design))