143 lines
6.1 KiB
Python
143 lines
6.1 KiB
Python
# -*- 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()},
|
|
}
|