auto: 2026-11-13 18:42 (EOMSANGDON-HOME)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user