Files
Aislo/M02_MasterTemplete/M02_San_Xlsx_Ilwi.py
T
eomsangdonandClaude Opus 5.5 109789a746 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>
2026-11-13 20:39:15 +09:00

187 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""STmate 출력 엑셀 「일위대가표」 → 호표 · 구성 줄(PLAN 828-2 · 요구 813 · 814 ㉮ · 읽기만 · 서버 단독).
읽는 곳 하나 — 일위대가 불러오기(`M02_Hopyo_Stmate.read`) · 단가산출 불러오기
(`M02_San_Xlsx_Stmate.read` 「일위」) · 재료 단서(`M02_San_MatFind.from_book`).
호표 = A 「제N호표」 줄(숨은 칸 `BDCOD|B…`) → 다음 줄 이름 · 규격 · 단위 → 구성 줄 → 「합계」 · 「계」.
구성 줄 = 숨은 칸 기준(표시대로 · 글 짐작 안 함 · 원파일 구성표 BDQTY 와 같은 칸) —
N 열 메타 `COD|M00131 ␅ EXI DVD BMK IPR KWN BLA EQC PRI PRO UNI TPR` · O 열 차수 꼬리 `M00131_1`.
율 줄 = 밑줄(IPR 1) 밑 비목(PRI) × 율 → 대상 비목(PRO) · 비목 수 1 M · 2 L · 3 S · 4 T(전체) ·
율 코드 EQC · 닫기(C) 뒤 % 줄(켜쌓기적용 「전체금액의 %」) = `전체`.
별도계상 = 비고 「별산…」(STmate 표기) · 규격 「별도계상」 · 관급 = KWN ≠ 0 · 나누기 = DVD T.
숨은 칸 없는 엑셀(값만 복사) = 코드는 M~Z 열 코드 꼴 첫 것(옛 길) · 시트 `숨은칸` 거짓 ·
단가산출 길 「일위」 = 빈 목록(물러남).
줄 꼴 = 828-1 `M02_San_Ilwi` 일위 구성 줄 꼴 {차례, 코드, 갈래(M L S X B D W · 율 · 닫기 · 구분 · 글),
이름, 규격, 단위, 수량, 단가 {T L M S}, 금액 {T L M S}, 율? {밑, 대상, 곱}, 밑줄?, 관급?, 별도계상?}
+ 엑셀 칸 {행, 차수, 비고, 나누기?, 전체?, 율.코드, 메타}.
"""
from __future__ import annotations
import re
from typing import Any
SHEET = "일위대가표"
CODE = re.compile(r"^(?:COD\|)?([A-Z]{1,2}\d{5}|PR[A-Z]?\d+|[TC]\d+)(?:_\d+)?(?: →)?$")
CODE_COLS = range(12, 26) # M ~ Z 열(옛 길 · 판마다 열 수 다름)
META_COL, CODE_COL, NOTE_COL = 13, 14, 12 # N 메타 · O 코드(차수 꼬리) · M 비고
_HOPYO = re.compile(r"제\s*(\d+)\s*호표")
_HEAD = re.compile(r"BDCOD\|(B\d{5})")
_TAIL = re.compile(r"^[A-Z]{1,2}\d*_(\d+)$")
_X7, _X5 = "_x0007_", "_x0005_"
KIND = "MLSXBDW" # 자원 코드 머리(L 노임 · M 자재 · S 경비 · X 기계 · D 산근 · B 일위 · W 일식)
PART = {"1": "M", "2": "L", "3": "S", "4": "T"} # PRI · PRO 비목 수
_MONEY = {"T": 4, "L": 6, "M": 8, "S": 10} # 단가 열 · 금액 = +1
def _cell(row: list[Any], col: int) -> Any:
return row[col] if col < len(row) else None
def _s(v: Any) -> str:
if isinstance(v, float) and v == int(v):
v = int(v)
return str(v if v is not None else "").strip()
def _num(v: Any) -> float | None:
if isinstance(v, bool) or v in (None, ""):
return None
try:
return float(str(v).replace(",", "").strip())
except ValueError:
return None
def meta(text: Any) -> dict[str, str]:
"""숨은 메타 칸 「`COD|L00003 ␅ EXI|0 …」 → dict(`M02_San_Book.meta` 와 같은 풀이)."""
s = str(text or "").replace(_X7, "").replace(_X5, "\x05")
out = {}
for part in s.split("\x05"):
part = part.strip("`")
if "|" in part:
k, v = part.split("|", 1)
out[k] = v
return out
def scan_code(row: list[Any]) -> str | None:
"""옛 길 — M~Z 열에서 코드 꼴 첫 것(숨은 칸 없는 엑셀 · `M02_Hopyo_Stmate` 와 같음)."""
for col in CODE_COLS:
hit = CODE.match(_s(_cell(row, col)))
if hit:
return hit.group(1)
return None
def kind_of(code: str) -> str:
"""갈래(`M02_San_Ilwi` 꼴) — 자원 머리 글자 M L S X B D W · 율 · 닫기 · 구분 · 글(빈 코드)."""
if code.startswith(("PR", "RR")):
return "율"
if code[:1] in "CT" and code[1:2].isdigit():
return "닫기" if code[0] == "C" else "구분"
return code[:1] if code[:1] and code[0] in KIND else "글"
def _money(row: list[Any], offset: int) -> dict[str, float | None]:
return {k: _num(_cell(row, c + offset)) for k, c in _MONEY.items()}
def _line(row: list[Any], at: int, closed: bool) -> dict[str, Any] | None:
"""구성 줄 하나 — 숨은 메타 있으면 그것 · 없으면 옛 코드 찾기(코드 없으면 None)."""
raw = _cell(row, META_COL)
got = meta(raw) if isinstance(raw, str) and "COD|" in raw else {}
tail = _TAIL.match(_s(_cell(row, CODE_COL)))
if "COD" in got:
code = got["COD"].strip()
else:
code = scan_code(row)
if code is None:
return None
one: dict[str, Any] = {
"행": at,
"코드": code,
"갈래": kind_of(code),
"차수": int(tail.group(1)) if tail else None,
"이름": _s(_cell(row, 0)),
"규격": _s(_cell(row, 1)),
"단위": _s(_cell(row, 3)),
"수량": _num(_cell(row, 2)),
"단가": _money(row, 0),
"금액": _money(row, 1),
"비고": _s(_cell(row, NOTE_COL)),
}
flags = {
"나누기": got.get("DVD") == "T",
"밑줄": got.get("IPR") == "1",
"별도계상": one["비고"].startswith("별산") or "별도계상" in one["규격"],
"관급": got.get("KWN", "0").strip() not in ("0", ""),
}
if code.startswith(("PR", "RR")) and got:
one["율"] = {"밑": PART.get(got.get("PRI", "")), "대상": PART.get(got.get("PRO", ""))}
one["율"] |= {"곱": code.startswith("RR"), "코드": got.get("EQC") or ""}
pct = one["단위"] == "%" or got.get("UNI") == "%"
flags["전체"] = closed and pct and one["갈래"] in ("율", "W")
one |= {k: True for k, v in flags.items() if v}
if got:
one["메타"] = {k: v for k, v in got.items() if k != "COD"}
return one
def read(rows: Any) -> dict[str, Any]:
"""시트 줄 → {숨은칸, 호표 [{번, 코드, 이름, 규격, 단위, 행 [처음,끝], 머리행, 저장 {T L M S}, 줄}]}.
끝 줄(합계 · 계)에서 다음 「제N호표」 까지는 안 받음 · 이름 빈 줄은 건너뜀(옛 길과 같음)."""
hopyos: list[dict[str, Any]] = []
want_head = done = closed = False
hidden = False
for at, row in enumerate(rows or [], 1):
row = list(row)
first = _s(_cell(row, 0))
flat = re.sub(r"\s+", "", first)
if hit := _HOPYO.fullmatch(flat):
code = next((m.group(1) for v in row[1:26] if (m := _HEAD.search(_s(v)))), None)
hopyos.append({"번": int(hit.group(1)), "코드": code, "행": [at, at], "줄": []})
want_head, done, closed = True, False, False
continue
if not hopyos or done:
continue
hopyo = hopyos[-1]
if want_head:
hopyo |= {"이름": first, "규격": _s(_cell(row, 1)), "단위": _s(_cell(row, 3))}
hopyo["머리행"] = at
want_head = False
continue
if flat.startswith("합계") or flat == "계":
hopyo["저장"] = _money(row, 1)
hopyo["끝행"] = at
done = True
continue
if not first:
continue
hopyo["행"][1] = at
one = _line(row, at, closed)
if one is None: # 숨은 칸 없는 엑셀의 코드 없는 글 줄(옛 길 근거글)
if re.search(r"[0-9A-Za-z가-힣]", first):
one = {"행": at, "코드": "", "갈래": "글", "이름": first, "규격": _s(_cell(row, 1))}
hopyo["줄"].append({"차례": len(hopyo["줄"]), **one})
continue
hidden = hidden or "메타" in one
closed = closed or one["갈래"] == "닫기"
hopyo["줄"].append({"차례": len(hopyo["줄"]), **one})
for hopyo in hopyos:
for key in ("이름", "규격", "단위"):
hopyo.setdefault(key, "")
return {"숨은칸": hidden, "호표": hopyos}
def blocks(sheet: dict[str, Any]) -> list[dict[str, Any]]:
"""`read` 답 → 단가산출 불러오기 「일위」 블록(원파일 `M02_San_Stc.read` 「일위」 와 같은 머리 칸 + 줄) ·
숨은 칸 없는 시트 = 빈 목록(구성 줄을 글로 짐작하지 않음)."""
if not sheet.get("숨은칸"):
return []
return [
{k: h.get(k) for k in ("코드", "번", "이름", "규격", "단위", "저장")}
| {"엑셀행": h["행"][0], "줄": h["줄"], "줄합": None, "표지": []}
for h in sheet["호표"]
]