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

119 lines
4.8 KiB
Python

# -*- coding: utf-8 -*-
"""STMATE 수량산출서 파일(IFORM) — 원본 형식 그대로 받기·내보내기 · 엑셀(BIFF) 셀·이름 읽기.
근거: 지식DB STmate/REA 분석 35(보고서양식_수량산출서) — STmateQty8.CPM TwQtyList
· 불러오기 GrdQEditButtonClick 0x41bcc8: 확장자 .XLS·.VTS 만 · 16MB(0x1000000) 초과 거절 ·
Formula One Read(DISPID 0xCA) 로 열어 보고 · 원래 파일 바이트를 QCODE.IFORM 메모에 그대로(Replace 0x2000)
· 저장 SaveSheet 0x4211c4: Write(DISPID 0xF0 · 읽은 형식 그대로) → 크기 검사 → IFORM
· 모두 내보내기 sBtnSaveToFileClick 0x420ee0: <작업 폴더>\\수량산출서\\<CODE>.VTS (IFORM 있는 행만 · 내용 그대로)
· 엑셀은 5.0/95(BIFF5) 만 지원한다는 알림(0x1ba80 · 0x1bc55)
VTS(Formula One 고유 형식)는 내용 해독 근거 없음 — 바이트 보존만.
"""
from __future__ import annotations
import base64
import re
MAX_FILE = 0x1000000 # 16MB
OLE_MAGIC = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
EXT_MSG = '수량산출서 파일은 확장가가 ".VTS" 또는 ".XLS"인 파일만 가능 합니다.'
SIZE_MSG = "파일이 너무 커서 저장 할수 없습니다. 저장 가능한 파일의 최대크기는 16MB 입니다."
XLS_MSG = "엑셀파일은 엑셀버젼 5.0,95 버젼만 지원 됩니다."
def kind_of(data: bytes) -> str:
"""IFORM 바이트 종류 — xls(OLE 복합 문서 · BIFF) · vts(그 밖 · Formula One) · 빈 것."""
if not data:
return ""
return "xls" if data[:8] == OLE_MAGIC else "vts"
def check_upload(filename: str, data: bytes) -> str | None:
"""불러오기 검사 — 원본 알림 글 · 통과면 None."""
ext = re.search(r"\.[^.\\/]*$", filename or "")
if not ext or ext.group(0).upper() not in (".XLS", ".VTS"):
return EXT_MSG
if len(data) > MAX_FILE:
return SIZE_MSG
return None
def _col(i: int) -> str:
s = ""
i += 1
while i:
i, r = divmod(i - 1, 26)
s = chr(65 + r) + s
return s
def parse_xls(data: bytes) -> dict:
"""엑셀(BIFF2~8) → {biff, compatible, sheets:[{name, rows, cols, cells{A1:값}}], names:[{name, sheet, addr}]}.
compatible = BIFF5 이하(원본 Formula One 이 읽는 엑셀 5.0/95) · 이름은 한 칸을 가리키는 것만 addr.
"""
import xlrd
bk = xlrd.open_workbook(file_contents=data, formatting_info=False)
sheets = []
for sh in bk.sheets():
cells = {}
for r in range(sh.nrows):
for c in range(sh.ncols):
cell = sh.cell(r, c)
if cell.ctype in (xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK):
continue
v = cell.value
if cell.ctype == xlrd.XL_CELL_NUMBER and float(v).is_integer():
v = int(v)
cells[f"{_col(c)}{r + 1}"] = v if not isinstance(v, str) else v.rstrip()
sheets.append({"name": sh.name, "rows": sh.nrows, "cols": sh.ncols, "cells": cells})
names = []
for nm in bk.name_obj_list:
item = {"name": nm.name, "sheet": None, "addr": None}
res = getattr(nm, "result", None)
refs = getattr(res, "value", None) if res is not None else None
# 한 영역 참조만(여러 영역·식 이름은 칸 없이) · Ref3D coords = 시트lo,hi,행lo,hi,열lo,hi
if isinstance(refs, list) and len(refs) == 1 and hasattr(refs[0], "coords"):
s0, _s1, r0, r1, c0, c1 = refs[0].coords
item["sheet"] = s0
if r1 - r0 == 1 and c1 - c0 == 1:
item["addr"] = f"{_col(c0)}{r0 + 1}"
names.append(item)
return {
"biff": bk.biff_version,
"compatible": bk.biff_version <= 70,
"sheets": sheets,
"names": names,
}
def describe(data: bytes) -> dict:
"""IFORM 바이트 설명 — 종류·크기 · 엑셀이면 시트 해독(실패면 오류 글)."""
out = {"kind": kind_of(data), "size": len(data)}
if out["kind"] == "xls":
try:
out["workbook"] = parse_xls(data)
if not out["workbook"]["compatible"]:
out["warning"] = XLS_MSG
except Exception as e: # noqa: BLE001
out["error"] = (
f"수량산출서로 저장된 파일이 손상되었거나 파일형식이 잘못 되었습니다. ({e})"
)
return out
def export_names(rows: list[dict]) -> list[tuple[str, bytes]]:
"""모두 내보내기 — IFORM 있는 행마다 (<CODE>.VTS, 바이트) · 확장자는 내용과 무관하게 .VTS."""
out = []
for r in rows:
data = b64(r.get("IFORM_B64"))
if data:
out.append((f"{str(r.get('CODE', '')).strip()}.VTS", data))
return out
def b64(text: str | None) -> bytes:
return base64.b64decode(text) if text else b""