- 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>
187 lines
8.2 KiB
Python
187 lines
8.2 KiB
Python
"""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["호표"]
|
||
]
|