# -*- coding: utf-8 -*- """엑셀 내보내기 짝 — 기준 BOQ(MK_BSCBOQ_EXL ↔ MK_EXL_BSCBOQ) · 자재 기준자료(wM_JajeXLS) · 코드 변경표(ST_U_ChgCode). 근거: 24_모듈구조_전수.md §3(기준 BOQ 양방향 · CONV_XLS_2_ST) · XLSX_INITIAL_구조.md(재등록 메타) · 10번(ST_U_ChgCode 코드 일괄·선택 변경 · Excel 왕복) · 34번(wM_JajeXLS 자재 기준자료 XLS) - 기준 BOQ: STMATE 와 같은 꼴 — 「설계내역서」 시트 + 숨은 INFO 열(COD·END·PSKP…) + veryHidden 「〓 INITIAL 〓」 → conv_xls_boq 로 되읽으면 같은 내역 행(닫는 행·페이지 넘김·SORTCODE 포함) - 자재 기준자료: COSTN M 코드 · 단가 출처 5칸(AMT_11~51 · 이름 = Start8x.Ini JMOK1~5) · 적용 단가(AMT_61) · 되읽으면 바뀐 칸만 costn_updates - 코드 변경표: 지금 코드 · 명칭 · 규격 · 단위 · 새 코드(빈칸 = 그대로) → {옛: 새} 화면·방향·선택지 원본 미확인(34번) — 칸 배치는 STMATE 목록표 꼴을 따름. """ from __future__ import annotations import io import re from conv_xls_boq import COLS, INITIAL, LEVEL_NAME, _numv, load_book from smt_parser import parse_dbf_bytes, parse_ini_bytes from stc_save import STRUCT_RE, pick_boq_table BOQ_HEAD = [ "공 종", "명 칭", "규 격", "수 량", "단위", "합 계", None, "노 무 비", None, "재 료 비", None, "경 비", None, "비 고", ] BOQ_COLS = { "ITNAME": 1, "NAME": 2, "SIZE": 3, "QTY": 4, "UNIT": 5, "TAMT": 6, "TAMOUNT": 7, "LAMT": 8, "LAMOUNT": 9, } BOQ_COLS.update({"MAMT": 10, "MAMOUNT": 11, "SAMT": 12, "SAMOUNT": 13, "INFO": 15}) INFO_OUT = (("EQCODE", "EQC"), ("PRIN", "PRI"), ("PROUT", "PRO"), ("RCU", "RCU"), ("KWNGP", "KWN")) def _info(pairs: list[tuple[str, object]]) -> str: return "_x0007_" + "".join(f"`{k}|{v}_x0005_" for k, v in pairs) + "`" def _wb(): import openpyxl return openpyxl.Workbook() def _initial(wb, pname: str, sheets: list[tuple[str, str, dict]]): ws = wb.create_sheet("〓 INITIAL 〓") ws.sheet_state = "veryHidden" ws.append([None, "※※ 현재 Sheet를 수정 또는 삭제하지 마십시요 ※※"]) ws.append([None, "[Sheet정보]", False, 24]) ws.append([None, "목차", "Sheet", "FPos", *COLS]) for title, sheet, cols in sheets: ws.append([None, title, sheet, None, *[cols.get(c) for c in COLS]]) ws.append([None, "[공사정보]"]) ws.append([None, "PNAME", pname]) ws.append([None, "[.끝.]"]) def export_boq_xlsx(files: dict, table: str = "") -> bytes: """BOQ 표 → STMATE 꼴 설계내역서 xlsx(INITIAL 메타 포함).""" name = pick_boq_table(files, table) prj = parse_dbf_bytes(files["PRJLST.DBF"])[1] if "PRJLST.DBF" in files else [{}] pname = (prj[0] if prj else {}).get("PNAME", "").strip() wb = _wb() ws = wb.active ws.title = "설계내역서" ws.append(["설 계 내 역 서"]) ws.append([f"공사명 : {pname}"]) ws.append(BOQ_HEAD) ws.append([None, None, None, None, None] + ["단 가", "금 액"] * 4) spos = 5 rows = parse_dbf_bytes(files[name])[1] # 글 칸 앞 빈칸 보존(들여쓰기) sort_at = {r["SORTCODE"]: k for k, r in enumerate(rows)} lines: list[list] = [] line_of: dict[int, int] = {} # BOQ 행 → 엑셀 줄 for k, r in enumerate(rows): bd = r["BDCODE1"].strip() line_of[k] = spos + len(lines) if bd.startswith(("E", "C")) and STRUCT_RE.match(bd): continue # 닫는 행은 머리 줄에 금액이 실림 if bd == ":": if lines: lines[-1][14] = lines[-1][14][:-1] + "`PSKP|1_x0005_`" continue amt = [_numv(r.get(f"{c}AMT1")) for c in "TLMS"] tot = [_numv(r.get(f"{c}_AMT1")) for c in "TLMS"] cells = [ r["ITNUM"], r["NAME1"], r["SIZE1"] or None, _numv(r["QTY1"]) or None, r["UNIT1"] or None, ] for a, b in zip(amt, tot): cells += [a, b] cells.append(r.get("BQCODE") or None) if bd.startswith("T") and STRUCT_RE.match(bd): end = sort_at.get(r["ENDCODE"]) close = rows[end]["BDCODE1"].strip() if end is not None else "E" + bd[1:] meta = [("COD", close), ("QTY1", 1), ("EXI", 0), ("ITT", 0), ("END", f"@{end}")] elif not bd: meta = [("COD", ""), ("QTY1", 1), ("EXI", 1), ("ITT", 0)] else: meta = [ ("COD", bd), ("QTY1", 1), ("EXI", r.get("EXIST1") or 0), ("IPR", r.get("PRPT") or 0), ] meta += [ (m, r.get(f)) for f, m in INFO_OUT if str(r.get(f) or "").strip() not in ("", "0") ] if r["UNIT1"].strip(): meta.append(("UNI", r["UNIT1"].strip())) meta.append(("BLA", r.get("BL_AMT") or "F")) lines.append(cells + [_info(meta)]) for ln in lines: # END = 닫는 행 다음 첫 엑셀 줄 m = re.search(r"`END\|@(\d+)", ln[14]) if m: k = int(m.group(1)) ln[14] = ln[14].replace(m.group(0), f"`END|{line_of.get(k + 1, spos + len(lines))}") ws.append(ln) ws.column_dimensions["O"].hidden = True cols = dict(BOQ_COLS, SPOS=spos, EPOS=spos + len(lines)) _initial(wb, pname, [("11. 내역서", "설계내역서", cols)]) buf = io.BytesIO() wb.save(buf) return buf.getvalue() # ── 자재 기준자료 ── JAJE_HEAD = ["코드", "명 칭", "규 격", "단위"] def export_jaje_xlsx(files: dict) -> bytes: """COSTN M 코드 → 자재 기준자료 xlsx(출처 5칸 · 적용 단가).""" ini = parse_ini_bytes(files.get("START8X.INI") or files.get("STINI.MEM") or b"") src = [ini.get(f"JMOK{i}", f"출처{i}") for i in range(1, 6)] wb = _wb() ws = wb.active ws.title = "자재기준자료" ws.append(JAJE_HEAD + src + ["적용 단가", "BQCODE", "JDCODE"]) for r in parse_dbf_bytes(files["COSTN.DBF"])[1]: if not r["CODE"].strip().startswith("M"): continue ws.append( [r["CODE"].strip(), r["NAME"], r["SIZE"], r["UNIT"].strip()] + [_numv(r.get(f"AMT_{i}1")) for i in range(1, 7)] + [r.get("BQCODE", "").strip(), r.get("JDCODE", "").strip()] ) buf = io.BytesIO() wb.save(buf) return buf.getvalue() def import_jaje_xlsx(files: dict, src) -> list[dict]: """자재 기준자료 xlsx → 바뀐 칸만 담은 costn_updates(없는 코드는 새 행).""" ws = load_book(src).worksheets[0] cur = {r["CODE"].strip(): r for r in parse_dbf_bytes(files["COSTN.DBF"])[1]} head = [str(v or "").strip() for v in next(ws.iter_rows(max_row=1, values_only=True))] fields = ( ["CODE", "NAME", "SIZE", "UNIT"] + [f"AMT_{i}1" for i in range(1, 7)] + ["BQCODE", "JDCODE"] ) out = [] for r in ws.iter_rows(min_row=2, values_only=True): if not r or not r[0]: continue vals = dict(zip(fields, r[: len(fields)])) code = str(vals["CODE"]).strip() old = cur.get(code) upd = {"CODE": code} for k, v in vals.items(): if k == "CODE" or k not in fields[: len(head)]: continue o = old.get(k) if old else None if k.startswith("AMT_"): if old is None or abs(_numv(o) - _numv(v)) > 1e-9: upd[k] = _numv(v) elif old is None or str(o or "").strip() != str(v or "").strip(): upd[k] = "" if v is None else str(v) if "AMT_61" in upd: # 적용 단가 = 1차 재료비 단가 upd["MAMT1"] = upd["TAMT1"] = upd["AMT_61"] if len(upd) > 1: out.append(upd) return out # ── 코드 변경표 ── def export_code_xlsx(files: dict) -> bytes: wb = _wb() ws = wb.active ws.title = "코드변경" ws.append(["지금 코드", "명 칭", "규 격", "단위", "새 코드"]) for r in parse_dbf_bytes(files["COSTN.DBF"])[1]: ws.append([r["CODE"].strip(), r["NAME"], r["SIZE"], r["UNIT"].strip(), None]) buf = io.BytesIO() wb.save(buf) return buf.getvalue() def import_code_xlsx(src) -> dict[str, str]: """코드 변경표 → {옛 코드: 새 코드}(새 코드 칸이 찬 줄만).""" ws = load_book(src).worksheets[0] out = {} for r in ws.iter_rows(min_row=2, values_only=True): if r and r[0] and len(r) > 4 and r[4] and str(r[4]).strip() != str(r[0]).strip(): out[str(r[0]).strip()] = str(r[4]).strip().upper() return out def level_label(code: str) -> str: """닫는 행 단계 이름(소계·계·합계…) — 화면 표시용.""" return LEVEL_NAME.get(int(code[1:]) if code[1:].isdigit() else 0, "") __all__ = [ "INITIAL", "export_boq_xlsx", "export_jaje_xlsx", "import_jaje_xlsx", "export_code_xlsx", "import_code_xlsx", "level_label", ]