feat(828-2): STmate 엑셀 일위대가표 숨은 칸 → 구성 줄 — 읽는 곳 하나 · 율 줄 · 닫기 · 일식 · 별도계상 · 판에 일위 재료

- M02_San_Xlsx_Ilwi(새) = 일위대가표 읽기 한 곳 — N 열 메타(COD · IPR · PRI · PRO · EQC · KWN · DVD) · O 열 차수 꼬리 기준 · 줄 꼴 = 828-1 M02_San_Ilwi 꼴 + 엑셀 칸(행 · 차수 · 비고 · 전체 · 율.코드 · 메타) · 숨은 칸 없음 = 옛 코드 찾기 · 단산 길 일위 빈 목록 + 표지
- 5 건 호표 136 · 줄 706 · STC 구성 줄과 코드 · 갈래 · 수량 · 율 · 밑줄 같음(다른 5 줄 = STC 별도계상 규칙이 비고 별산 표시를 놓침)
- 호표 길(M02_Hopyo_Stmate._hopyos) · 단산 길(M02_San_Xlsx_Stmate.read 일위) · 단서(M02_San_MatFind.from_book) 모두 공용 읽기 · 단서 · 산근 · 단가 전후 같음
- M02_Hopyo_Fixed._ratio = 숨은 칸 율 밑 · 대상 비목 먼저(글 짐작 대신) — 호표 길 전후 바뀜 = 영덕 제11호표 산악할증 0% 한 줄(금액만 → 비율 노무비 · 금액 0 그대로) · 단산 항목 1564 판정 그대로
- 재료 판(census) = 일위 구성 줄 자재도 셈(M02_San_MatFind.ilwi_found · 종 칸 일위 = 호표 코드 · 화면 「일위대가 N 호표」)
- 시험: 새 test_828_2_xlsx_ilwi_meta · test_827_2 단서 시트 보기에 「제N호표」 줄 넣음(실제 시트는 늘 있음 · 공용 읽기가 호표 머리부터 셈)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 20:39:15 +09:00
co-authored by Claude Opus 5.5
parent 53932e964f
commit 109789a746
9 changed files with 466 additions and 88 deletions
@@ -125,6 +125,7 @@ def test_clue_head_and_boq_apart():
def test_clue_book_sheet():
meta = "_x0007_`COD|{}_x0005_`EXI|0_x0005_`"
rows = [
[" 제 17 호표", *[None] * 12, "_x0005_`BDCOD|B01217_x0007_`POSS|3_x0007_`POSE|4_x0007_`"],
["파형강관부설", "D=800mm", None, "m", *[None] * 8, "대가 17", None, "B01217"],
[
"파형강관(800mm)",
@@ -0,0 +1,148 @@
"""828-2 STmate 출력 엑셀 「일위대가표」 숨은 칸 → 호표 · 구성 줄(`M02_San_Xlsx_Ilwi` · 요구 813 · 814 ㉮).
5 건 호표 136 · 줄 706 · 율 줄 밑 · 대상 비목 · 닫기 뒤 전체 % · 일식 · 별도계상 · 숨은 칸 없는 파일 물러남 ·
원파일 STC 구성 줄(828-1)과 같은 꼴 · 읽는 곳 하나(호표 길 · 단산 길 · 단서) · 판 일위 줄 재료.
실무 파일이 없으면 건너뜀.
"""
import sys
from functools import lru_cache
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path[:0] = [str(ROOT), str(ROOT / "resources" / "tester"), str(ROOT / "resources" / "master_data" / "scripts")] # fmt: skip
import helper_m02_hopyo_golden as golden # noqa: E402
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix # noqa: E402
from M02_MasterTemplete import M02_San_Book as book # noqa: E402
from M02_MasterTemplete import M02_San_MatFind as mf # noqa: E402
from M02_MasterTemplete import M02_San_Xlsx as sx # noqa: E402
from M02_MasterTemplete import M02_San_Xlsx_Ilwi as il # noqa: E402
TAGS = ("봉화", "동부청영월", "울진대흥", "영덕계류", "울진소광")
HOPYOS = {"봉화": 45, "동부청영월": 23, "울진대흥": 34, "영덕계류": 9, "울진소광": 25}
LINES = {"봉화": 216, "동부청영월": 138, "울진대흥": 184, "영덕계류": 39, "울진소광": 129}
APART = {"봉화": 7, "동부청영월": 18, "울진대흥": 16, "영덕계류": 0, "울진소광": 9}
@lru_cache(maxsize=8)
def _sheets(tag: str) -> dict:
path = golden.FILES[tag]
if not path.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
return book.sheets(path.name, path.read_bytes())
def _lines(got: dict) -> list[dict]:
return [x for h in got["호표"] for x in h["줄"]]
def _bare(rows: list) -> list:
"""값만 복사한 엑셀 — 숨은 메타(N 열) 지움."""
return [[None if j == il.META_COL else v for j, v in enumerate(r)] for r in rows]
@pytest.mark.parametrize("tag", TAGS)
def test_hopyo_lines_by_meta(tag):
got = il.read(_sheets(tag)[il.SHEET])
lines = _lines(got)
assert got["숨은칸"] and len(got["호표"]) == HOPYOS[tag] and len(lines) == LINES[tag]
assert all("메타" in x for x in lines)
assert all(h["코드"] and h["코드"].startswith("B") and h.get("저장") for h in got["호표"])
assert sum(bool(x.get("별도계상")) for x in lines) == APART[tag]
for x in lines:
if x["갈래"] == "율":
assert (
x["율"]["밑"] in "MLST" and x["율"]["대상"] in "MLST" and x["율"]["코드"] == "A01"
)
if x["코드"] and x["갈래"] not in ("율", "닫기"):
assert x["차수"] == 1
def test_total_count():
got = [il.read(_sheets(t)[il.SHEET]) for t in TAGS]
assert sum(len(g["호표"]) for g in got) == 136 and sum(len(_lines(g)) for g in got) == 706
def test_rate_close_lump():
bong = _lines(il.read(_sheets("봉화")[il.SHEET]))
first = next(x for x in bong if x["갈래"] == "율")
assert (first["이름"], first["율"]["밑"], first["율"]["대상"]) == ("잡재료비", "L", "M")
marked = [x["행"] for x in bong if x.get("밑줄")]
assert 72 in marked and 73 in marked # 포장공 · 보통인부 = 잡재료비 밑수 줄
yd = _lines(il.read(_sheets("영덕계류")[il.SHEET]))
closes = [x for x in yd if x["갈래"] == "닫기"]
whole = [x for x in yd if x.get("전체")]
assert len(closes) == 2 and [(x["코드"], x["수량"]) for x in whole] == [("W00005", 90.0)] * 2
assert sum(x["갈래"] == "W" for x in _lines(il.read(_sheets("울진소광")[il.SHEET]))) == 3
def test_bare_sheet_falls_back():
rows = _bare(_sheets("봉화")[il.SHEET])
got = il.read(rows)
assert not got["숨은칸"] and il.blocks(got) == [] and mf.from_book(rows) == []
out = sx.read_sheets({**_sheets("봉화"), il.SHEET: rows})
assert out["일위"] == [] and any("숨은 칸 없음" in t for t in out["표지"])
# 호표 길 = 옛 코드 찾기(O 열) — 숨은 칸 율 · 호표 코드만 빠지고 줄은 같음
from M02_MasterTemplete import M02_Hopyo_Stmate as st
a = st._hopyos(_sheets("봉화")[il.SHEET])
b = st._hopyos(rows)
strip = [[{k: v for k, v in x.items() if k != "율"} for x in h] for h in a[1]]
heads = [[{k: v for k, v in h.items() if k != "코드"} for h in x[0]] for x in (a, b)]
assert heads[0] == heads[1] and strip == b[1] # 호표 코드(BDCOD)도 숨은 칸이라 빠짐
def test_same_form_as_stc():
import test_810_1_stc as stc
path = stc.FILES["봉화"]
if not path.exists():
pytest.skip("원파일 없음")
xl = sx.read_sheets(_sheets("봉화"))
nat = {b["코드"]: b for b in stc.parsed("봉화")["일위"]}
def key(x):
r = x.get("율") or {}
flags = tuple(bool(x.get(k)) for k in ("밑줄", "관급", "별도계상"))
return (
x["코드"],
x["갈래"],
round(float(x["수량"] or 0), 6),
r.get("밑"),
r.get("대상"),
flags,
)
assert len(xl["일위"]) == 45
for b in xl["일위"]:
assert [key(x) for x in b["줄"]] == [key(x) for x in nat[b["코드"]]["줄"]], b["코드"]
def test_one_reader_three_users():
sh = _sheets("봉화")
clues = mf.from_book(sh[il.SHEET])
assert len(clues) == 41 and clues == mf.from_book(il.read(sh[il.SHEET]))
assert all(c["곳"] == "일위대가표" and c["코드"].startswith("M") for c in clues)
path = golden.FILES["영덕계류"]
hp = hx.read_book(path.name, path.read_bytes())
lines = hp["호표"][6]["줄"] # 제11호표 산악할증 — 숨은 칸 율(노무비 → 노무비 · 0 %)
rate = next(x for x in lines if x["코드"] == "PRA01")
assert rate["율"] == {"밑": "노무비", "대상": "노무비"}
assert hfix._ratio(rate, [x for x in lines if x is not rate]) == {
"기준": "노무비",
"비목": "노무비",
}
def test_census_ilwi_materials():
path = golden.FILES["영덕계류"]
saved = fn.pack(sx.read_sheets(_sheets("영덕계류")), path.name)
got = mf.ilwi_found(saved)
assert len(got) == 4 and all(r["재료찾기"]["판정"] for _, r in got)
assert {b["코드"] for b, _ in got} == {"B01107"}