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

190 lines
8.2 KiB
Python

# -*- coding: utf-8 -*-
"""저장 덧붙임 — COSTN 새 코드 행 · BDQTY 구성 고침(암호 그대로) · QCODE 수량코드 · 제잡비 설정(PRJLST.SMT).
근거: STC_왕복검증.md §3(알려진 칸만 갱신) · STC_난독화_상태.md(BDQTY 자리별 변환) · 29번 ·
02번(QCODE 15필드) · 25번 §1(저장키) · kanjub_cfg.smt_updates(설정 → 저장키)
규칙:
- 화면이 보낸 값이 불러온 값과 뜻으로 같으면 그 파일은 원래 바이트 그대로
- COSTN 새 코드: 같은 머리글자 마지막 행 뒤에 끼움 · 숫자 칸 0 · 메모 칸 빈 포인터
- BDQTY: 바뀐 호표(BDCODE 묶음)만 다시 늘어놓음 · 원래 행은 원래 바이트에서 바뀐 칸만 암호로 씀
· 새 행 틀 = 같은 구성 코드 행(이름·규격 암호 칸을 그대로 옮김) > 같은 호표 첫 행 > 첫 행
· 복호가 깨진 값(U+FFFD 포함)은 쓰지 않음 — 틀 행의 원래 바이트 유지
- QCODE: 행이 바뀌었을 때만 qty_qcode.build_qcode 로 두 파일을 다시 씀
- 제잡비: 차수별 설정 → smt_updates · 원래 값과 같은 키는 버림 · 없던 키는 빈 값·0 이 아닐 때만
"""
from __future__ import annotations
import re
from bdqty import decode_bdqty
from dbf_crypt import BdqtyCodec, covers
from dbf_memo import _same_ini_value, ini_items
from dbf_rw import DbfTable
from kanjub_cfg import smt_updates
from qty_qcode import build_qcode, read_qcode
CODE_RE = re.compile(r"^[A-Z][0-9A-Z]{5}$")
def _num(v) -> float | None:
try:
return float(str(v).strip().replace(",", ""))
except (TypeError, ValueError):
return None
def same_val(a, b) -> bool:
"""뜻 비교 — 둘 다 숫자로 읽히면 숫자로 · 아니면 앞뒤 공백·줄바꿈 꼴 무시."""
na, nb = _num(a), _num(b)
if na is not None and nb is not None:
return abs(na - nb) < 1e-9
norm = lambda x: str("" if x is None else x).replace("\r\n", "\n").strip() # noqa: E731
return norm(a) == norm(b)
def same_row(old: dict, new: dict) -> bool:
"""old 의 칸 중 new 에 있는 칸이 모두 같은가(new 에 없는 칸은 안 바뀐 것으로 봄)."""
return all(same_val(v, new[k]) for k, v in old.items() if k in new and not k.startswith("_"))
# ── COSTN 새 코드 행 ──
def insert_costn_rows(ct: DbfTable, ups: list[dict], log) -> list[str]:
"""요청 중 COSTN 에 없는 코드를 새 행으로 끼움 — 끼운 코드 목록."""
have = {ct.get(i, "CODE") for i in range(len(ct))}
added = []
for u in ups:
code = str(u.get("CODE", "")).strip().upper()
if code in have or not CODE_RE.match(code):
continue
last = [i for i in range(len(ct)) if ct.get(i, "CODE")[:1] == code[:1]]
pos = (last[-1] + 1) if last else len(ct)
rec = bytearray(b" " * ct.rlen)
for f in ct.fields.values():
if f.type in ("N", "F"):
rec[f.off : f.off + f.len] = ct.encode_value(f.name, 0)
ct.recs.insert(pos, rec)
ct.dirty = True
vals = {k: v for k, v in u.items() if not k.startswith("_")}
vals["CODE"] = code
for k, v in vals.items():
if k in ct.fields and ct.fields[k].type != "M":
ct.set_raw(pos, k, ct.encode_value(k, v))
log.add("COSTN.DBF", pos, "_insert", "", code)
have.add(code)
added.append(code)
return added
# ── BDQTY 구성 ──
def _groups(rows: list[dict]) -> dict[str, list[int]]:
out: dict[str, list[int]] = {}
for i, r in enumerate(rows):
out.setdefault(str(r.get("BDCODE", "")).strip(), []).append(i)
return out
def _lsame(t: DbfTable, k: str, a, b) -> bool:
"""칸 꼴을 아는 비교 — 논리 칸은 빈칸 = F."""
f = t.fields.get(k)
if f is not None and f.type == "L":
n = lambda x: str(x or "").strip().upper()[:1] or "F" # noqa: E731
return n(a) == n(b)
return same_val(a, b)
def save_bdqty(b: bytes, new_rows: list[dict], log, strict: bool = True) -> bytes | None:
"""화면 구성행(decode_bdqty 꼴) → 새 BDQTY 바이트 · 바뀐 것이 없으면 None.
strict=False(엑셀 역등록) — 암호 표 밖 칸은 오류 대신 건너뜀."""
old = decode_bdqty(b)
t = DbfTable(b, codec=BdqtyCodec())
def same(o: dict, n: dict) -> bool:
return all(_lsame(t, k, v, n[k]) for k, v in o.items() if k in n and not k.startswith("_"))
if len(old) == len(new_rows) and all(same(o, n) for o, n in zip(old, new_rows)):
return None
g_old, g_new = _groups(old), _groups(new_rows)
by_comp: dict[str, int] = {}
for i, r in enumerate(old):
by_comp.setdefault(str(r.get("CODE", "")).strip(), i)
recs, plan = [], [] # plan = (새 자리, 비교 기준 옛 행, 새 행)
for code, idx in g_new.items():
olds = g_old.get(code, [])
news = [new_rows[i] for i in idx]
if len(olds) == len(news) and all(same(old[o], n) for o, n in zip(olds, news)):
recs += [bytearray(t.recs[o]) for o in olds]
continue
for k, n in enumerate(news):
if k < len(olds):
base = olds[k]
else:
comp = str(n.get("CODE", "")).strip()
base = by_comp.get(comp, olds[0] if olds else 0)
log.add("BDQTY.DBF", len(recs), "_insert", old[base].get("CODE", ""), comp)
plan.append((len(recs), base, n))
recs.append(bytearray(t.recs[base]))
for code in g_old:
if code not in g_new:
log.add("BDQTY.DBF", g_old[code][0], "_delete", code, "")
t.recs, t.dirty = recs, True
for i, base, n in plan:
ref = old[base]
for k, v in n.items():
if k.startswith("_") or k not in t.fields or _lsame(t, k, ref.get(k, ""), v):
continue
if "�" in str(v): # 복호가 깨진 칸(표 밖 글자)에서 온 값 — 옮겨 온 원래 바이트 유지
continue
f = t.fields[k]
if f.type in ("N", "F") and _num(v) is None and str(v).strip():
continue # 숫자로 읽히지 않는 복호값(자리 표가 덜 맞는 칸)도 같음
if not covers(f.off, f.len):
if strict:
raise ValueError(f"BDQTY {k} 칸은 암호 표 밖이라 쓸 수 없음")
continue
t.set_raw(i, k, t.encode_value(k, v))
log.add("BDQTY.DBF", i, k, str(ref.get(k, "")), str(v))
return t.to_bytes()
# ── QCODE ──
def save_qcode(files: dict[str, bytes], rows: list[dict], log) -> dict[str, bytes]:
"""수량코드 행이 바뀌었을 때만 {QCODE.DBF, QCODE.SMT}."""
cur = read_qcode(files)["rows"]
if len(cur) == len(rows) and all(same_row(o, n) for o, n in zip(cur, rows)):
return {}
bad = [
r.get("CODE") for r in rows if not re.fullmatch(r"Q\d{5}", str(r.get("CODE", "")).strip())
]
if bad:
raise ValueError(f"수량코드 형식 오류: {bad[:5]}")
clean = [{k: v for k, v in r.items() if not k.startswith("_")} for r in rows]
dbf, smt = build_qcode(clean, files)
log.add("QCODE.DBF", 0, "_rows", str(len(cur)), str(len(rows)))
return {"QCODE.DBF": dbf, "QCODE.SMT": smt}
# ── 제잡비 설정 → PRJLST.SMT 키 ──
def kanjub_smt(text: str, stages: dict, explicit: dict) -> dict:
"""차수별 설정(kanjubStages)과 화면이 직접 준 키를 합친 갱신 사전 — 원래 값과 같은 키는 뺌."""
orig = {k: v for _s, k, v in ini_items(text)}
derived: dict = {}
for slot, cfg in sorted((stages or {}).items(), key=lambda x: int(x[0])):
if not isinstance(cfg, dict):
continue
upd = smt_updates(cfg, int(slot))
if int(slot) != 1:
upd = {k: v for k, v in upd.items() if not k.startswith("ADD_")}
derived.update(upd)
keep = {
k: v
for k, v in derived.items()
if (k in orig and not _same_ini_value(orig[k], v))
or (k not in orig and str(v).strip() not in ("", "0"))
}
out = dict(explicit or {})
for k, v in keep.items():
if k not in out or (k in orig and _same_ini_value(orig[k], out[k])):
out[k] = v
return out