119 lines
4.8 KiB
Python
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""
|