# -*- coding: utf-8 -*- """엑셀 → 프로젝트 반영 — 내역 불러오기(wM_From_XLS) · 역등록(CONV_XLS_2_ST · XLS_2_BDREC). 근거: conv_xls_boq.py · conv_xls_res.py(INITIAL 메타 복원 규칙) · STC_왕복검증.md §3(원본을 틀로) · 16번 §4(소계·직접비 = PRJLST ?COSTn) · stc_project.new_project(빈 틀) - 표 쓰기: 틀 DBF 의 필드 그대로 · 새 레코드 = 숫자 칸 0 · 논리 칸 빈칸 · 메모 칸 빈 포인터 · 주어진 칸만 씀 - 내역 불러오기: 고른 BOQ 표를 엑셀 행으로 통째로 바꿈 · BQCODE·EQCODE·단위는 그 프로젝트 COSTN 값 · PRJLST 직접비(?COSTn)는 소계 다시 셈(stc_save.recompute_boq) 결과 - 역등록: 틀 프로젝트(요율·서식·설정) + 목록표 COSTN + 산출근거 COSTN.SMT + 구성표 BDQTY + 내역 BOQ11 · PRJLST 공사명 = [공사정보] PNAME - BDQTY 새 행 = 틀 BDQTY 의 같은 구성 코드 레코드를 옮겨 바뀐 칸만 암호로(stc_save_extra.save_bdqty) """ from __future__ import annotations from conv_xls_boq import ( boq_from_xlsx, boq_rows_from_sheet, fill_from_costn, find_sheet, load_book, read_initial, ) from conv_xls_res import comp_from_san, comp_from_sheets, costn_from_lists, memos_from_sheet from dbf_memo import BLANK_PTR, SixMemo, memo_text_bytes from dbf_rw import DbfTable from smt_parser import parse_dbf_bytes from stc_project import new_project from stc_save import SaveLog, pick_boq_table, prj_row_for, recompute_boq from stc_save_extra import save_bdqty def rows_to_table(template: bytes, rows: list[dict]) -> DbfTable: """틀 DBF 필드로 rows 를 담은 새 표(레코드 전부 새로 씀).""" t = DbfTable(template) zero = bytearray(b" " * t.rlen) for f in t.fields.values(): if f.type in ("N", "F"): zero[f.off : f.off + f.len] = t.encode_value(f.name, 0) t.recs, t.dirty = [], True for i, r in enumerate(rows): t.recs.append(bytearray(zero)) for k, v in r.items(): if k.startswith("_") or k not in t.fields or t.fields[k].type == "M": continue if v is None or (isinstance(v, str) and not v.strip() and t.fields[k].type != "C"): continue t.set_raw(i, k, t.encode_value(k, v)) return t def _direct_to_prj(files: dict, out: dict, bt: DbfTable, name: str, log: SaveLog) -> dict: """소계 행 다시 셈 + PRJLST 그 공사 행 ?COSTn = 차수별 직접비.""" slots = {s for s in "12345" if f"T_AMT{s}" in bt.fields} direct = recompute_boq(bt, name, slots, log) if "PRJLST.DBF" in files: pt = DbfTable(out.get("PRJLST.DBF", files["PRJLST.DBF"])) pi = prj_row_for(pt, name) for s, vals in direct.items(): for c, v in zip(("MCOST", "LCOST", "SCOST", "TCOST"), vals): if f"{c}{s}" in pt.fields: log.set(pt, "PRJLST.DBF", pi, f"{c}{s}", v) if pt.dirty: out["PRJLST.DBF"] = pt.to_bytes() return {s: [float(x) for x in v] for s, v in direct.items()} def import_boq(files: dict, src, table: str = "", kind: str = "내역서") -> tuple[dict, dict]: """엑셀 내역 → 프로젝트 BOQ 표 통째 바꿈 — (새 files, 보고).""" name = pick_boq_table(files, table) if not name: raise ValueError("BOQ 표 없음") got = boq_from_xlsx(src, kind) rows = got["rows"] costn = parse_dbf_bytes(files["COSTN.DBF"])[1] if "COSTN.DBF" in files else [] filled = fill_from_costn(rows, costn) known = {str(r.get("CODE", "")).strip() for r in costn} unknown = sorted( { r["BDCODE1"] for r in rows if r["BDCODE1"][:1] in tuple("MLSXBDW") and r["BDCODE1"] not in known } ) out = dict(files) log = SaveLog() bt = rows_to_table(files[name], rows) direct = _direct_to_prj(files, out, bt, name, log) out[name] = bt.to_bytes() return out, { "table": name, "sheet": got["sheet"], "row_count": len(rows), "costn_filled": filled, "unknown_codes": unknown, "direct": direct, } def _costn_table(template: bytes, rows: list[dict], memos: dict[str, str]) -> tuple[bytes, bytes]: t = rows_to_table(template, rows) memo = SixMemo(None) if "IFORM" in t.fields: for i in range(len(t)): text = memos.get(t.get(i, "CODE")) ptr = memo.write(BLANK_PTR, memo_text_bytes(text)) if text else BLANK_PTR t.set_raw(i, "IFORM", ptr) return t.to_bytes(), memo.to_bytes() def register(template: dict, src, pname: str = "") -> tuple[dict, dict]: """역등록 — 엑셀 한 권으로 새 프로젝트 files 를 만듦(틀 = 다른 프로젝트의 요율·서식·설정).""" wb = load_book(src) info = read_initial(wb) sec = info["sections"].get("공사정보", {}) name = pname or str((sec.get("PNAME") or ["엑셀 역등록 공사"])[0]) out = new_project(template, name) costn = costn_from_lists(wb, info) memos = memos_from_sheet(wb, info) out["COSTN.DBF"], out["COSTN.SMT"] = _costn_table(template["COSTN.DBF"], costn, memos) spec = find_sheet(info, "내역서") if not spec or spec["sheet"] not in wb.sheetnames: raise ValueError("INITIAL 목차에 내역서 시트 없음") rows = boq_rows_from_sheet(wb[spec["sheet"]], spec["cols"]) fill_from_costn(rows, costn) log = SaveLog() bt = rows_to_table(template["BOQ11.DBF"], rows) direct = _direct_to_prj(out, out, bt, "BOQ11.DBF", log) out["BOQ11.DBF"] = bt.to_bytes() comp = comp_from_sheets(wb, info) + comp_from_san(wb, info) if comp and "BDQTY.DBF" in template: base = template["BDQTY.DBF"] # 틀 레코드(같은 구성 코드)의 이름 암호 칸을 옮겨 씀 out["BDQTY.DBF"] = save_bdqty(base, comp, log, strict=False) or base return out, { "pname": name, "costn_count": len(costn), "memo_count": len(memos), "boq_count": len(rows), "bdqty_count": len(comp), "direct": direct, "sheets": {k: v["sheet"] for k, v in info["sheets"].items()}, }