94 lines
3.7 KiB
Python
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)
|