- 새 M02_Doc_NativeLayout — 원본 글줄 차림 표지로 금액 줄 묶음(= 항목 제목) + 문서 쌓기(기본과 다를 때만) · EST 금액 차례 = 원본 차례(셈 같을 때만) · 합계 절 뺌 · 줄 쌓기 모델 stackOf · saveOf · hiddenOf 파이썬 거울 - M02_Doc_NativeEst.tags — 글줄 차림 머리(단 > 0 · 소계 구간 첫 글 · 번호 글) · 합계 · 글(분수 그림 위아래 줄 뺌) - M02_San_Emit.stmate — 글 줄 표지 · 원본 차림 표지(계 줄마다 항목) - M02_Doc_Import_Shape.shape — 차림 tag · apply 부름 - 줄 쌓기 hiddenOf — 가져온 값 옵션 칸도 공식 묶음에 보임(원파일 꼬리표 · 같은 값) · 옵션 가로 칸 고침 = 수 그대로(글이면 셈 「—」 흠 고침) - 시험 test_812_3 — 견본 다섯(거창 1 · 2 · 41 · 15 · 봉화 14) · 전수 2,220 블록 금액 같음 · TS 왕복 · 거울 100 % · M01 1,518 차림 그대로 - 낡은 시험 고침 test_810_3 — 옵션 칸 ∩ 공식 묶음 = 빈 → 옵션 칸 ⊆ 공식 묶음(812-3 으로 가져온 값이 공식 묶음에도 보임) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
224 lines
9.0 KiB
Python
224 lines
9.0 KiB
Python
"""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)
|