214 lines
7.9 KiB
Python
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
|