Files
Aislo/tools/stmate-test/conv_xls_out.py
T
eomsangdonandClaude Opus 5.5 b9847c7db8 refactor(stmate-test): 원본에 없는 「자재 기준자료 엑셀 내보내기·불러오기」 걷음
- 원본 wM_JajeXLS = 재료비 수량집계표 엑셀 내보내기 한 방향뿐(REA 37 · TWM_JAJEXLS 0x5658bc) — 추측으로 만든 자재 기준자료 왕복 메뉴·API·시험 삭제
- 남김: 「재료비 수량집계표를 엑셀파일로 만들기」(conv_jaje · scr_jaje)
- 낡은 시험 test_jaje_xlsx_roundtrip_and_edit · conv_api 의 jaje 왕복 삭제 — 원본에 없는 기능

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 17:43:12 +09:00

184 lines
6.6 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
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()
# ── 코드 변경표 ──
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_code_xlsx",
"import_code_xlsx",
"level_label",
]