- JDL 줄 = 탭 구분 + \x05/\x07 덧붙임(검사기 0x504d20 · 고정폭 표는 죽은 고리 — 34 §17.9 바로잡음) · 단가 목록 7종·Main·GongJong 머리·Global.Ini 상수 키 · zip 받기 - 구버전 판 가리기(Chk_Data 0x459280) · 7.x/DOS 표별 필드 대응 · B→X 조건 = 단위 X - 엑셀 역등록 PRPT「속성」·PRIN「대상」·PROUT「저장」 = PR/RR 줄만 · 원본 BDQTY 4건 일치 - 자재 = 재료비 수량집계표 엑셀 내보내기(주요건설자재현황 시트) - 그림 코드 = COSTN I 코드 IFORM · SIZE = 그림 클래스 이름 - 지식DB 37번 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
259 lines
10 KiB
Python
259 lines
10 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(ST_P_XLS 0x5f1078 — 지식DB REA 분석/37 §3): 목록표 DET = 구성표 머리 줄 · BDCODE = 목록 COD ·
|
|
모든 줄 IPR→PRPT(「속성」0~99) · DVD→DEVD1 · BMK→BIMOK · BLA→BL_AMT · D 줄 SPR·SBM·TPR ·
|
|
PR/RR 줄만 UNI→UNIT1 · EQC · PRI→PRIN(「대상」 0 빈칸·1 재료비·2 노무비·3 경비·4 합계) · PRO→PROUT(「저장」) ·
|
|
M 줄만 KWN→KWNGP(「관급」 0·1 관급·2 별산) · RCU 는 내역 행 키(구성 줄에 안 씀)
|
|
산출근거 숨은 열: 머리 줄 BDCOD·POSS·POSE · 그 아래 줄마다 0x07+원래 식 한 줄 → CRLF 로 이으면 COSTN.SMT 본문
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from conv_xls_boq import _numv, _txt, _unx, 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",
|
|
}
|
|
|
|
|
|
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 _list_det(wb, info: dict, kind: str) -> list[tuple[str, int]]:
|
|
"""목록표 줄 차례 (COD, DET=구성표 머리 줄 번호) — ST_P_XLS 0x5ef67b."""
|
|
ws, _cols = _sheet(wb, info, kind)
|
|
if ws is None:
|
|
return []
|
|
ic = _info_col(ws)
|
|
out = []
|
|
for r in ws.iter_rows(values_only=True):
|
|
m = parse_info(r[ic - 1] if ic and ic - 1 < len(r) else None)
|
|
if "COD" in m and _numv(m.get("DET")):
|
|
out.append((m["COD"].strip(), int(_numv(m["DET"]))))
|
|
return out
|
|
|
|
|
|
def _int(v) -> int:
|
|
return int(_numv(v)) if str(v or "").strip() else 0
|
|
|
|
|
|
def comp_row(bd: str, r: tuple, cols: dict, meta: dict) -> dict:
|
|
"""구성 줄 하나 → BDQTY 행 — ST_P_XLS 0x5f1078(SetFieldValue 차례 · 키 없으면 0/거짓/빈칸)."""
|
|
cell = lambda k: _txt(r[cols[k] - 1]) if cols.get(k) and cols[k] - 1 < len(r) else "" # noqa: E731
|
|
code = meta.get("COD", "").strip()
|
|
row = {"BDCODE": bd, "CODE": code, "NAME1": cell("NAME"), "SIZE1": cell("SIZE")}
|
|
row["EXIST1"] = _int(meta.get("EXI"))
|
|
row["QTY1"] = 0.0 # AppendBlank 기본값 — 아래 갈래에서만 씀
|
|
ratio = code in ("PR", "RR")
|
|
if not (ratio or code == "QTY" or code[1:2].isdigit()):
|
|
return row
|
|
k = code[:1]
|
|
if k in "CET" and not ratio and code != "QTY":
|
|
row["EXIST1"] = 0
|
|
return row
|
|
if not (ratio or code == "QTY" or k in "ABDLMPQRSWX"):
|
|
return row
|
|
q = r[cols["QTY"] - 1] if cols.get("QTY") and cols["QTY"] - 1 < len(r) else None
|
|
row["QTY1"] = round(_numv(q), 7) # MROUNDUD(x, 7, 1)
|
|
row["UNIT1"] = cell("UNIT")
|
|
row["DEVD1"] = meta.get("DVD", "") == "T"
|
|
row["BIMOK"] = meta.get("BMK", "")
|
|
row["PRPT"] = _int(meta.get("IPR"))
|
|
if k == "D":
|
|
row |= {
|
|
"SPRPT": _int(meta.get("SPR")),
|
|
"SBMOK": meta.get("SBM", ""),
|
|
"TPRPT": _int(meta.get("TPR")),
|
|
}
|
|
if ratio:
|
|
row |= {"UNIT1": meta.get("UNI", ""), "EQCODE": meta.get("EQC", "")}
|
|
row |= {"PRIN": _int(meta.get("PRI")), "PROUT": _int(meta.get("PRO"))}
|
|
if k == "M":
|
|
row["KWNGP"] = _int(meta.get("KWN"))
|
|
row["BL_AMT"] = meta.get("BLA", "") == "T"
|
|
return row
|
|
|
|
|
|
def comp_from_sheets(wb, info: dict) -> list[dict]:
|
|
"""일위대가표 · 중기사용료 → BDQTY 구성행 — 목록표 줄 차례 · BDCODE = 목록 COD ·
|
|
머리 줄(DET) 의 POSS~POSE 줄 중 INFO 가 0x07 로 시작하는 줄마다 한 행(ST_P_XLS 0x5f97c2~0x5f9864)."""
|
|
out = []
|
|
for kind, lst 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, det in _list_det(wb, info, lst):
|
|
hm = parse_info(rows[det - 1][ic - 1]) if 0 < det <= len(rows) else {}
|
|
if "POSS" not in hm:
|
|
continue
|
|
for line in range(int(_numv(hm["POSS"])), int(_numv(hm.get("POSE"))) + 1):
|
|
r = rows[line - 1]
|
|
v = r[ic - 1] if ic - 1 < len(r) else None
|
|
if not (isinstance(v, str) and _unx(v).startswith("")):
|
|
continue
|
|
out.append(comp_row(bd, r, cols, parse_info(v)))
|
|
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
|