Files
Aislo/tools/stmate-test/conv_xls_out.py
T

243 lines
9.0 KiB
Python

# -*- 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",
]