190 lines
8.2 KiB
Python
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
|