Files
Aislo/tools/stmate-test/conv_xls_res.py
T

214 lines
7.9 KiB
Python

# -*- coding: utf-8 -*-
"""엑셀 역등록 자원 쪽 (CONV_XLS_2_ST · XLS_2_BDREC) — 목록표 → COSTN 행 · 일위대가표/중기사용료 → BDQTY 구성 ·
단가산출근거 → COSTN.SMT 산출식.
근거: XLSX_INITIAL_구조.md · 24_모듈구조_전수.md §3(XLS_2_BDREC — 엑셀 수량산출 양식 ↔ BDQTY) · 23번(COSTN.SMT) ·
실무 xlsx 숨은 열 실측
목록표 INFO: COD · BQC(BQCODE) · EQC(EQCODE) · JDC(JDCODE) · WQC(WQCODE) · EDT(EDT_DATE) · ADJ(S_ADJ) · RXX
구성표 머리 줄 INFO: BDCOD(호표 코드) · POSS/POSE(구성 줄 범위)
구성 줄 INFO: COD · EXI(EXIST1) · DVD(DEVD1) · BMK(BIMOK) · IPR(PRPT) · KWN · BLA · EQC · PRI · PRO · UNI · RCU
산출근거 숨은 열: 머리 줄 BDCOD·POSS·POSE · 그 아래 줄마다 0x07+원래 식 한 줄 → CRLF 로 이으면 COSTN.SMT 본문
"""
from __future__ import annotations
import re
from conv_xls_boq import _numv, _txt, find_sheet, parse_info
# 목록표 이름 → (머리글자, 단가 열 배치)
LISTS = {
"재료비목록표": "M",
"노무비목록표": "L",
"경비목록표": "S",
"일식목록표": "W",
"중기목록표": "X",
"일위대가목록표": "B",
"산출근거목록표": "D",
}
LIST_INFO = {
"BQC": "BQCODE",
"EQC": "EQCODE",
"JDC": "JDCODE",
"WQC": "WQCODE",
"EDT": "EDT_DATE",
"ADJ": "S_ADJ",
"RXX": "RXX",
}
COMP_INFO = {
"EXI": "EXIST1",
"DVD": "DEVD1",
"BMK": "BIMOK",
"IPR": "PRPT",
"KWN": "KWNGP",
"BLA": "BL_AMT",
"EQC": "EQCODE",
"PRI": "PRIN",
"PRO": "PROUT",
"RCU": "RCU",
}
def _sheet(wb, info: dict, kind: str):
spec = find_sheet(info, kind)
if not spec or spec["sheet"] not in wb.sheetnames:
return None, None
return wb[spec["sheet"]], spec["cols"]
def _info_col(ws, top: int = 3) -> int | None:
"""숨은 INFO 열 — 머리 아래 첫 몇 줄에서 `COD| 또는 `BDCOD| 가 든 열."""
for r in ws.iter_rows(min_row=top, max_row=min(ws.max_row, top + 30), values_only=True):
for j, v in enumerate(r, start=1):
if isinstance(v, str) and ("`COD|" in v or "`BDCOD|" in v):
return j
return None
def stored_unit(code: str, unit: str, size: str) -> str:
"""보이는 단위 → 저장 단위(report_core.unitText 의 역) — 실무 7건 COSTN 전수 꼴:
X 코드 '시간' = 'X' · '%' = 규격 글로 비목(노무·인건 → L% · 재료·연료 → M% · 경비 → S% · 그 밖 T%)."""
u = str(unit or "").strip()
if code[:1] == "X" and u in ("시간", "X"):
return "X"
if u != "%":
return u
s = str(size or "")
if re.search(r"노무|인건|,노", s):
return "L%"
if re.search(r"재료|연료", s):
return "M%"
return "S%" if "경비" in s else "T%"
def costn_from_lists(wb, info: dict) -> list[dict]:
"""목록표 시트들 → COSTN 행(CODE NAME SIZE UNIT ?AMT1 + INFO 칸) · 목록 차례대로."""
out, seen = [], set()
for kind, pre in LISTS.items():
ws, cols = _sheet(wb, info, kind)
if ws is None:
continue
ic = _info_col(ws) or cols.get("INFO")
for r in ws.iter_rows(
min_row=cols.get("SPOS", 4), max_row=cols.get("EPOS", ws.max_row), values_only=True
):
meta = parse_info(r[ic - 1] if ic and ic - 1 < len(r) else None)
code = meta.get("COD", "").strip()
if not code or code in seen:
continue
row = {
"CODE": code,
"NAME": _txt(r[cols["NAME"] - 1]) if cols.get("NAME") else "",
"SIZE": _txt(r[cols["SIZE"] - 1]) if cols.get("SIZE") else "",
"UNIT": _txt(r[cols["UNIT"] - 1]).strip() if cols.get("UNIT") else "",
}
row["UNIT"] = stored_unit(code, row["UNIT"], row["SIZE"])
t = _numv(r[cols["TAMT"] - 1]) if cols.get("TAMT") else 0.0
part = {
c: (_numv(r[cols[f"{c}AMT"] - 1]) if cols.get(f"{c}AMT") else None) for c in "MLS"
}
if all(v is None for v in part.values()): # 단가 한 칸 목록(재·노·경)
part = {c: (t if c == pre else 0.0) for c in "MLS"}
row.update({f"{c}AMT1": part[c] or 0.0 for c in "MLS"}, TAMT1=t)
for k, f in LIST_INFO.items():
if k in meta:
row[f] = meta[k]
out.append(row)
seen.add(code)
return out
def _blocks(ws, ic: int):
"""구성표·산출근거의 호표 묶음 — (코드, 시작 줄, 끝 줄) · POSS/POSE 는 엑셀 줄 번호."""
for line, r in enumerate(ws.iter_rows(values_only=True), start=1):
v = r[ic - 1] if ic - 1 < len(r) else None
if isinstance(v, str) and "`BDCOD|" in v:
m = parse_info(v)
yield (
m["BDCOD"].strip(),
int(_numv(m.get("POSS")) or line + 1),
int(_numv(m.get("POSE")) or line + 1),
)
def comp_from_sheets(wb, info: dict) -> list[dict]:
"""일위대가표 · 중기사용료 → BDQTY 구성행(decode_bdqty 칸 이름) · 호표 차례대로."""
out = []
for kind in ("일위대가표", "중기사용료"):
ws, cols = _sheet(wb, info, kind)
if ws is None:
continue
ic = _info_col(ws)
rows = list(ws.iter_rows(values_only=True))
for bd, a, b in _blocks(ws, ic):
for line in range(a, b + 1):
r = rows[line - 1]
meta = parse_info(r[ic - 1] if ic - 1 < len(r) else None)
if "COD" not in meta:
continue
q = r[cols["QTY"] - 1] if cols.get("QTY") else None
row = {"BDCODE": bd, "CODE": meta["COD"].strip(), "QTY1": _numv(q)}
for k, f in COMP_INFO.items():
if k in meta:
row[f] = meta[k]
out.append(row)
return out
REF_RE = re.compile(
r"(PR|RR)[A-Z]?_\(.*\)_1|([A-Z][0-9A-Z]{0,5})_1"
) # 'RRC_(25)_1' · 'PRS_(2)_1' = 비율 줄
def comp_from_san(wb, info: dict) -> list[dict]:
"""단가산출근거 → D 코드 BDQTY 구성행 — 코드 열 「코드_1」 줄(수량 = 숨은 QTY 열) · 소계/계 줄 = T1·T2…
(묶음 뿌리 T7 = 총계 줄은 저장 안 함 — 봉화 D00014 등 복호 대조)."""
ws, _cols = _sheet(wb, info, "단가산출근거")
if ws is None:
return []
ic = _info_col(ws)
rows = list(ws.iter_rows(values_only=True))
qc = next(
(j for r in rows[:6] for j, v in enumerate(r, start=1) if str(v or "").strip() == "QTY"),
None,
)
out = []
for bd, a, b in _blocks(ws, ic):
for line in range(a, b + 1):
r = rows[line - 1]
ref = next(
(
m.group(1) or m.group(2)
for v in r
if isinstance(v, str)
for m in [REF_RE.fullmatch(v)]
if m
),
None,
)
if not ref or ref == "T7" or ref == bd:
continue
q = _numv(r[qc - 1]) if qc and not ref.startswith("T") else 0.0
out.append({"BDCODE": bd, "CODE": ref, "QTY1": round(q, 11)})
return out
def memos_from_sheet(wb, info: dict) -> dict[str, str]:
"""단가산출근거 → {D코드: COSTN.SMT 본문} — 숨은 열 줄을 CRLF 로 이음."""
ws, _cols = _sheet(wb, info, "단가산출근거")
if ws is None:
return {}
ic = _info_col(ws)
rows = list(ws.iter_rows(values_only=True))
out = {}
for bd, a, b in _blocks(ws, ic):
lines = []
for line in range(a, b + 1):
v = rows[line - 1][ic - 1] if ic - 1 < len(rows[line - 1]) else None
if isinstance(v, str):
v = re.sub(r"_x0007_", "\x07", v)
if v.startswith("\x07"):
lines.append(v[1:])
out[bd] = "\r\n".join(lines)
return out