88 lines
3.6 KiB
Python
88 lines
3.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""코드 일괄 변경(ST_U_ChgCode Chg_Code_Sel · Exec_Change_Code) — 옛 코드 → 새 코드를 프로젝트 전체에 바꿈.
|
|
|
|
근거: 10_System_186개_파일분석장부.md(ST_U_ChgCode — 코드 일괄·선택 변경 · Excel 왕복 · CH_CODE 임시표) ·
|
|
23번(COSTN.SMT 식 참조 &코드[LMS]&) · STC_난독화_상태.md(BDQTY 자리별 변환) · 02번(BOQ BDCODE1~4)
|
|
바꾸는 자리: COSTN.CODE · BOQ1n.BDCODE1~5 · BDQTY.BDCODE·CODE(암호 그대로 다시 숨김) · COSTN.SMT 식의 &코드…& ·
|
|
QCODE.BQCODE · BOQ1n.CODEA
|
|
규칙: 새 코드는 6자 영숫자 · 이미 있는 코드로는 못 바꿈 · 같은 길이라 메모는 제자리 덮어씀
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from dbf_crypt import BdqtyCodec
|
|
from dbf_memo import SixMemo
|
|
from dbf_rw import DbfTable
|
|
from stc_save import SaveLog
|
|
|
|
CODE_RE = re.compile(r"^[A-Z][0-9A-Z]{5}$")
|
|
|
|
|
|
def _check(files: dict, mapping: dict[str, str]) -> dict[str, str]:
|
|
ct = DbfTable(files["COSTN.DBF"])
|
|
have = {ct.get(i, "CODE") for i in range(len(ct))}
|
|
m = {
|
|
str(a).strip().upper(): str(b).strip().upper() for a, b in mapping.items() if str(b).strip()
|
|
}
|
|
m = {a: b for a, b in m.items() if a != b}
|
|
for a, b in m.items():
|
|
if a not in have:
|
|
raise ValueError(f"COSTN 에 없는 코드: {a}")
|
|
if not CODE_RE.match(b):
|
|
raise ValueError(f"새 코드 형식 오류(영문 1자 + 5자): {b}")
|
|
if b in have and b not in m:
|
|
raise ValueError(f"이미 있는 코드로 바꿀 수 없음: {a} → {b}")
|
|
if len(set(m.values())) != len(m):
|
|
raise ValueError("새 코드가 겹침")
|
|
return m
|
|
|
|
|
|
def _table(
|
|
files: dict, out: dict, name: str, cols: tuple[str, ...], m: dict, log: SaveLog, codec=None
|
|
):
|
|
if name not in files:
|
|
return
|
|
t = DbfTable(files[name], codec=codec)
|
|
for i in range(len(t)):
|
|
for c in cols:
|
|
if c in t.fields:
|
|
v = t.get(i, c)
|
|
if v in m and t.set_raw(i, c, t.encode_value(c, m[v])):
|
|
log.add(name, i, c, v, m[v])
|
|
if t.dirty:
|
|
out[name] = t.to_bytes()
|
|
|
|
|
|
def change_codes(files: dict, mapping: dict[str, str]) -> tuple[dict, list]:
|
|
"""{옛: 새} → 새 files · 바뀐 자리 목록."""
|
|
m = _check(files, mapping)
|
|
out, log = dict(files), SaveLog()
|
|
if not m:
|
|
return out, []
|
|
_table(files, out, "COSTN.DBF", ("CODE",), m, log)
|
|
for name in sorted(k for k in files if re.match(r"^BOQ\d*\.DBF$", k)):
|
|
_table(files, out, name, tuple(f"BDCODE{n}" for n in range(1, 6)) + ("CODEA",), m, log)
|
|
_table(files, out, "BDQTY.DBF", ("BDCODE", "CODE"), m, log, codec=BdqtyCodec())
|
|
_table(files, out, "QCODE.DBF", ("BQCODE",), m, log)
|
|
if "COSTN.SMT" in files and "IFORM" in DbfTable(out["COSTN.DBF"]).fields:
|
|
ct = DbfTable(out["COSTN.DBF"])
|
|
memo = SixMemo(files["COSTN.SMT"])
|
|
pat = re.compile(r"&(" + "|".join(map(re.escape, m)) + r")([LMS]?)&")
|
|
for i in range(len(ct)):
|
|
ptr = ct.raw(i, "IFORM")
|
|
body = memo.read(ptr)
|
|
if not body:
|
|
continue
|
|
text = body.decode("cp949", "replace")
|
|
new = pat.sub(lambda x: f"&{m[x.group(1)]}{x.group(2)}&", text)
|
|
if new != text:
|
|
new_ptr = memo.write(ptr, new.encode("cp949"))
|
|
if ct.set_raw(i, "IFORM", new_ptr):
|
|
out["COSTN.DBF"] = ct.to_bytes()
|
|
log.add("COSTN.SMT", i, ct.get(i, "CODE"), "", f"{len(pat.findall(text))}곳")
|
|
if memo.dirty:
|
|
out["COSTN.SMT"] = memo.to_bytes()
|
|
return out, log.items
|