228 lines
9.5 KiB
Python
228 lines
9.5 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 직접 = 그 항목 공식 묶음
|
||
# 조합 기계 「굴삭기 0.6㎥+부착용집게」 = 기계마다 항목 하나 · 제목 = 그 기계(812-9 · 요구 738-3)
|
||
assert titles[3] == "굴착기(무한궤도) 0.6㎥" and got[3][2] == ["=Q_3", "굴삭기(무한궤도)"]
|
||
assert got[4] == ("부착용 집게 0.6∼0.8", False, ["부착용 집게"])
|
||
assert got[5] == ("전석쌓기 인부", 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, [])
|
||
# 「가. 굴삭기(무한궤도)(0.7+브레이카)」 = 조합 기계 둘 = 가나다 항목 둘(812-9 · 요구 738-3)
|
||
assert got[1][:2] == ("굴착기(무한궤도) 0.7㎥", True)
|
||
assert got[2] == ("대형 브레이커 0.7㎥", True, ["대형 브레이커"])
|
||
assert got[3] == ("치즐(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) >= 1513 # 813-2 지움 6
|
||
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)
|