# -*- 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: <작업 폴더>\\수량산출서\\.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 있는 행마다 (.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""