243 lines
9.0 KiB
Python
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",
|
|
]
|