# -*- 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