"""812-3 지킴 — 가져온 단가산출(원파일) 줄 쌓기 차림 = 경쟁사 산출근거 꼴(요구 713). 차림 = `M02_Doc_NativeLayout`. (a) 견본(거창 덤프 꼴) — 1 측구터파기(토사) 「기 계 … 굴삭기」 항목 + 공식 묶음 q · K · f · E · Cm · Q + 보조인부 항목 · 41 합판거푸집(4회) 근거 글 · 재료비(재료 · 소계 · 율 · 글) · 인건비(인력 · 소계 · 율 · 글) · 15 전석 메쌓기 별산 · 단산 참조 Q 직접 · 기계 · 인부 · 2 가 · 나 소항목 · 봉화 STC = 계 단마다 항목 (b) 전수 — 원파일 EST · STC 산근 블록 모두: 차림 전후 셈(비목 · 계 · 줄 금액) 같음 · TS 왕복(문서 → 줄 → 문서) 같음 · TS `stackOf` = 파이썬 거울 `stack_of` · 수 표 = tmp/s8123/sweep.json(일회 기록) (c) M01 문서 1,518 — 차림 안 바뀜(쌓기 없음 · 왕복 같음 · 거울 같음) 원파일 없으면 그 시험만 skip(다른 PC). """ from __future__ import annotations import json import subprocess import sys from functools import lru_cache from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] HERE = Path(__file__).resolve().parent for p in (ROOT, HERE): if str(p) not in sys.path: sys.path.insert(0, str(p)) import test_810_2_native_doc as t2 # noqa: E402 from test_810_1_estx import FILES as EST # noqa: E402 from test_810_1_stc import FILES as STC # noqa: E402 from M02_MasterTemplete import M02_Doc_Copy as dc # noqa: E402 from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402 from M02_MasterTemplete import M02_Doc_NativeLayout as lay # noqa: E402 COSTS = ("노무비", "재료비", "경비", "계") def _ts(cases: list[dict]) -> list[dict]: proc = subprocess.run( ["node", str(HERE / "helper_812_3_layout.cjs")], input=json.dumps(dc.ml._norm(cases), ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", ) assert proc.returncode == 0, proc.stderr[-3000:] return json.loads(proc.stdout) def _doc(kind: str, tag: str, no: int, laid: bool = True) -> tuple[dict, dict]: sk = t2.sk_of(t2.fn.emit(t2.native(kind, tag), no)) keep = lay.apply if not laid: lay.apply = lambda doc, meta: [x.pop(lay.KEEP, None) for x in doc.get("금액") or []] try: return sk, dim.derive(sk, None, t2.whole()) finally: lay.apply = keep def _items(doc: dict) -> list[dict]: return lay.stack_of(doc) def _show(doc: dict) -> list[tuple]: """(제목, 가나다, [줄 글]) — 금액 = 이름 · 공식 = 「=기호…」 · 글 = 「#글」.""" money = doc["금액"] out = [] for it in _items(doc): lines = [] for x in it["줄"]: if x["꼴"] == "금액": lines.append(money[x["i"]]["이름"]) elif x["꼴"] == "공식": lines.append("=" + " ".join(x["기호"])) else: lines.append("#" + x["글"]) out.append((it["제목"], bool(it.get("가나다")), lines)) return out # ── (a) 견본 ───────────────────────────────────────────────────────────── def test_sample_excavation_machine_and_formula() -> None: _, doc = _doc("EST", "거창", 1) got = _show(doc) assert got == [ ("기 계(100%) : 굴삭기(무한궤도) (0.7㎥)", False, ["=q K f E Cm Q", "굴삭기(무한궤도)"]), ("보조인부(1일당 1인)", False, ["보통인부"]), ], got assert [x["묶음"] for x in doc["금액"]] == [got[0][0], got[1][0]] def test_sample_formwork_text_rate_subtotal() -> None: _, doc = _doc("EST", "거창", 41) got = _show(doc) assert got[0] == ( "", False, ["#< 토목품셈 6-3-1 합판거푸집(2022개정) >", "#※ 사용횟수 4회 : 보통"], ) assert got[1] == ( "재료비", False, [ "합판(내수),대전", "외송각재,대전", "주자재소계", "#소모자재(박리재등) (주자재비의 9%)", "율", ], ) assert got[2][0] == "인건비 (24년 품셈 개정)" and got[2][2][:3] == [ "형틀목공", "보통인부", "인부소계", ] assert got[2][2][3].startswith("#공구손료") and len(got) == 3 sub = next(x for x in doc["금액"] if x["이름"] == "주자재소계") assert sub["합밖"] and "비목" not in sub # 소계 = 합 밖 · 비목 칸 빈칸 그대로(810-2) def test_sample_stone_wall_reference_and_direct_q() -> None: _, doc = _doc("EST", "거창", 15) got = _show(doc) titles = [g[0] for g in got] assert titles[0] == "전석 구입운반 : 별산" and got[0][2] == [] assert ( titles[1].startswith("뒷채움돌 운반 및 채집") and got[1][2][-1] == "뒷채움돌기초잡석채집운반" ) assert got[1][2][0].split()[0] == "=Q" # 단산 참조 Q 직접 = 그 항목 공식 묶음 assert titles[3].startswith("전석쌓기 기계") and got[3][2] == ["=Q_3", "굴삭기(무한궤도)"] assert got[4] == ("전석쌓기 인부", False, ["석공", "보통인부"]) assert "합" not in "".join(titles) # 합계 절 = 화면 자동 합계(두 번 안 보임) def test_sample_stc_steps_are_items() -> None: _, doc = _doc("STC", "봉화", 14) got = [ (t, g, [x for x in ls if not x.startswith("=P_")]) for t, g, ls in _show(doc) ] # 손 단가 = 기본 자리 assert got == [ ("", False, ["#▣ 토공의 비탈규준틀[개소]2-6-1"]), ("재 료 비", False, ["각재(거푸집용,외송)", "판재", "일반못(철못)"]), ("인 건 비", False, ["건축목공", "보통인부"]), ], got def test_sample_ganada_sub_items() -> None: _, doc = _doc("EST", "거창", 2) got = _show(doc) assert got[0] == ("기 계(100%)", False, []) assert got[1][:2] == ("굴삭기(무한궤도) (0.7+브레이카)", True) assert got[2] == ("치즐(0.7㎥용)", True, ["치즐(0.7㎥)"]) # 「다. 소 계」 절 = 뺌 # ── (b) 전수 ───────────────────────────────────────────────────────────── def _money(sk: dict, doc: dict) -> tuple: got = dim.run(doc, sk, t2.whole()) lines = sorted(str(x.get("금액")) for x in got.get("줄") or []) return tuple(str(got.get(c)) for c in COSTS), lines @lru_cache(maxsize=1) def _sweep() -> dict: stat = {"블록": 0, "금액같음": 0, "다름": [], "cases": []} for kind, files in (("EST", EST), ("STC", STC)): for tag, path in files.items(): if not path.exists(): continue for no in t2.fn.numbers(t2.native(kind, tag)): sk, doc = _doc(kind, tag, no) _, plain = _doc(kind, tag, no, laid=False) stat["블록"] += 1 if _money(sk, doc) == _money(sk, plain): stat["금액같음"] += 1 else: stat["다름"].append((kind, tag, no)) stat["cases"].append( {"키": f"{kind}|{tag}|{no}", "문서": doc, "파이썬": _items(doc)} ) return stat def test_sweep_money_same_and_ts_round_trip() -> None: stat = _sweep() if not stat["블록"]: pytest.skip("원파일 없음") assert not stat["다름"] and stat["금액같음"] == stat["블록"], stat["다름"][:5] got = _ts(stat["cases"]) bad = {f: [g["키"] for g in got if not g[f]] for f in ("같음", "거울")} assert bad == {"같음": [], "거울": []}, {k: v[:5] for k, v in bad.items()} by = {} for g in got: kind = g["키"].split("|")[0] row = by.setdefault( kind, dict.fromkeys(("블록", "항목", "제목", "공식", "글", "쌓기", "무제목만"), 0) ) row["블록"] += 1 for f in ("항목", "제목", "공식", "글"): row[f] += g[f] row["쌓기"] += g["쌓기"] row["무제목만"] += g["항목"] > 0 and g["제목"] == 0 out = ROOT / "tmp" / "s8123" out.mkdir(parents=True, exist_ok=True) (out / "sweep.json").write_text(json.dumps(by, ensure_ascii=False, indent=1), "utf-8") assert by.get("EST", {}).get("제목", 0) > by.get("EST", {}).get("블록", 0) # ── (c) M01 ────────────────────────────────────────────────────────────── def test_m01_docs_layout_unchanged() -> None: docs = dc.folder() assert len(docs) >= 1518 cases = [{"키": k, "문서": d, "파이썬": _items(d)} for k, d in docs.items()] got = _ts(cases) bad = {f: [g["키"] for g in got if not g[f]] for f in ("같음", "거울")} assert bad == {"같음": [], "거울": []}, {k: v[:5] for k, v in bad.items()} assert not any(g["쌓기"] for g in got)