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

94 lines
3.7 KiB
Python

"""STMATE 수량코드(QCODE.DBF · QCODE.SMT) 읽기·쓰기 — 수량산출서 기본 수량변수 원장.
근거: 지식DB STmate/old/20_분석 02(QCODE 15필드 CODE·NAME·BQCODE·QTY0~9·IFORM·XCODE) ·
28 §2(기본수량변수 ↔ 확장수량변수) · REA 30 quantity_variable_resolution(QTY0~9 직접 · XCODE 이름=값) ·
SIxMemo_구조(메모 칸 10바이트 <HII: 태그 · 길이 · 블록번호>).
"""
from __future__ import annotations
import struct
from smt_parser import build_dbf_bytes, parse_dbf_bytes
MEMO_FIELDS = ("IFORM", "XCODE")
QTY_FIELDS = tuple(f"QTY{i}" for i in range(10))
# 표본 STC 의 QCODE.DBF 필드 정의(빈 표 · 새 파일을 만들 때의 틀)
DEFAULT_FIELDS = [
{"name": "CODE", "type": "C", "len": 6, "dec": 0},
{"name": "NAME", "type": "C", "len": 60, "dec": 0},
{"name": "BQCODE", "type": "C", "len": 16, "dec": 0},
*({"name": f, "type": "N", "len": 12, "dec": 4} for f in QTY_FIELDS),
{"name": "IFORM", "type": "M", "len": 10, "dec": 0},
{"name": "XCODE", "type": "M", "len": 10, "dec": 0},
]
def _memo_text(hex_ptr: str, smt: bytes) -> str:
raw = bytes.fromhex(hex_ptr or "")
if (
len(raw) < 10
or not smt
or smt[8:15] != b"SIxMemo"
or not any(b not in (0, 0x20) for b in raw)
):
return ""
_tag, length, block = struct.unpack("<HII", raw[:10])
size = struct.unpack("<I", smt[4:8])[0] or 64
start = block * size
if length <= 0 or start >= len(smt):
return ""
return smt[start : start + length].rstrip(b"\x00").decode("cp949", "replace")
def read_qcode(files: dict[str, bytes]) -> dict:
"""STC 파일 묶음 → {fields, rows} · 메모 칸(IFORM · XCODE)은 본문 글자로."""
dbf = files.get("QCODE.DBF", b"")
if len(dbf) < 32:
return {"fields": DEFAULT_FIELDS, "rows": [], "present": False}
fields, rows, _meta = parse_dbf_bytes(dbf)
smt = files.get("QCODE.SMT", b"")
out = []
for r in rows:
row = {f["name"]: r.get(f["name"], "") for f in fields}
for m in MEMO_FIELDS:
if m in row:
row[m] = _memo_text(r.get(f"{m}_HEX", ""), smt)
out.append(row)
return {
"fields": [{k: f[k] for k in ("name", "type", "len", "dec")} for f in fields],
"rows": out,
"present": True,
}
def build_qcode(rows: list[dict], files: dict[str, bytes] | None = None) -> tuple[bytes, bytes]:
"""행 목록 → (QCODE.DBF, QCODE.SMT) 바이트 · 원본 필드 정의가 있으면 그대로 씀."""
files = files or {}
fields, meta = DEFAULT_FIELDS, None
if len(files.get("QCODE.DBF", b"")) >= 32:
fields, _rows, meta = parse_dbf_bytes(files["QCODE.DBF"])
smt = bytearray(512)
smt[8:15] = b"SIxMemo"
recs = []
for r in rows:
rec = {}
for f in fields:
name, val = f["name"], r.get(f["name"], "")
if f.get("type") == "M" or name in MEMO_FIELDS:
text = str(val or "").replace("\r\n", "\n").replace("\n", "\r\n")
data = text.encode("cp949", "replace")
if not data:
rec[f"{name}_HEX"] = (b" " * int(f["len"])).hex()
continue
block = len(smt) // 64
smt.extend(data)
smt.extend(b"\x00" * ((-len(smt)) % 64))
rec[f"{name}_HEX"] = struct.pack("<HII", 0x0008, len(data), block).hex()
else:
rec[name] = "" if val is None else val
recs.append(rec)
# 머리 — 다음 빈 블록 번호 · 블록 크기 64
struct.pack_into("<II", smt, 0, len(smt) // 64, 64)
return build_dbf_bytes(fields, recs, meta), bytes(smt)