101 lines
4.2 KiB
Python
101 lines
4.2 KiB
Python
"""SGV UI oracle: snapshot/diff of ONE C000025 row (READ-ONLY; never writes to Firebird).
|
|
|
|
python scripts/sgv_product_snapshot.py before <CODIGO>
|
|
-> reads the whole row and saves docs/runs/snapshots/<CODIGO>_before.json
|
|
(operator changes exactly ONE field in the SGV product screen and saves)
|
|
python scripts/sgv_product_snapshot.py diff <CODIGO> [--ui-label "CFOP saída"]
|
|
-> re-reads, prints FIELD/BEFORE/AFTER for every changed column, and (with --ui-label) records the
|
|
result in docs/runs/sgv_ui_diff_ledger.json. A label is CONFIRMED only when exactly ONE
|
|
non-technical column changed.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "agent" / "src"))
|
|
SNAP_DIR = ROOT / "docs" / "runs" / "snapshots"
|
|
LEDGER = ROOT / "docs" / "runs" / "sgv_ui_diff_ledger.json"
|
|
# Columns the ERP rewrites by itself on every save; ignored ONLY because they are clearly automatic.
|
|
TECHNICAL = {"ULTIMA_ALTERACAO", "DATA_ALTERACAO", "DATA_ULTIMA_ALTERACAO", "USUARIO_ALTERACAO"}
|
|
|
|
|
|
def norm(v):
|
|
if isinstance(v, (bytes, bytearray)):
|
|
import hashlib
|
|
return "blob:" + hashlib.sha256(bytes(v)).hexdigest()[:16]
|
|
if isinstance(v, (datetime, date)):
|
|
return v.isoformat()
|
|
if isinstance(v, Decimal):
|
|
return str(v)
|
|
return v.strip() if isinstance(v, str) else v
|
|
|
|
|
|
def read_row(env, code):
|
|
from gti_fiscal_agent.firebird_reader import _connect # read-only isolation
|
|
conn = _connect(env)
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT * FROM C000025 WHERE CODIGO = ?", (code,))
|
|
row = cur.fetchone()
|
|
if row is None:
|
|
raise SystemExit(f"product {code} not found")
|
|
names = [c[0].strip() for c in cur.description]
|
|
blob = {}
|
|
return {n: norm(v) for n, v in zip(names, row)}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def compute_diff(before, after):
|
|
out = []
|
|
for f in sorted(set(before) | set(after)):
|
|
if before.get(f) != after.get(f):
|
|
out.append({"FIELD": f, "BEFORE": before.get(f), "AFTER": after.get(f), "technical": f in TECHNICAL})
|
|
return out
|
|
|
|
|
|
def load_env():
|
|
for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
|
|
if "=" in line and not line.startswith("#"):
|
|
k, _, v = line.partition("="); os.environ.setdefault(k.strip(), v.strip())
|
|
|
|
|
|
def record(label, code, changes):
|
|
real = [c for c in changes if not c["technical"]]
|
|
ledger = json.loads(LEDGER.read_text(encoding="utf-8")) if LEDGER.exists() else []
|
|
entry = {"ui_label": label, "product": code, "recorded": datetime.now().isoformat(timespec="seconds"), "changed_columns": [c["FIELD"] for c in real],
|
|
"before": {c["FIELD"]: c["BEFORE"] for c in real}, "after": {c["FIELD"]: c["AFTER"] for c in real},
|
|
"confirmed": len(real) == 1, "note": "" if len(real) == 1 else ("no column changed" if not real else "more than one column changed; repeat changing a single field")}
|
|
ledger.append(entry)
|
|
LEDGER.write_text(json.dumps(ledger, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
return entry
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("action", choices=["before", "diff"]); ap.add_argument("codigo"); ap.add_argument("--ui-label")
|
|
a = ap.parse_args(argv)
|
|
load_env()
|
|
SNAP_DIR.mkdir(parents=True, exist_ok=True)
|
|
path = SNAP_DIR / f"{a.codigo}_before.json"
|
|
if a.action == "before":
|
|
path.write_text(json.dumps(read_row(os.environ, a.codigo), ensure_ascii=False, indent=1, default=str), encoding="utf-8")
|
|
print(f"snapshot saved: {path}"); return
|
|
if not path.exists():
|
|
raise SystemExit("run 'before' first")
|
|
changes = compute_diff(json.loads(path.read_text(encoding="utf-8")), read_row(os.environ, a.codigo))
|
|
print(f"{'FIELD':<28}{'BEFORE':<28}AFTER")
|
|
for c in changes:
|
|
print(f"{c['FIELD']:<28}{str(c['BEFORE']):<28}{c['AFTER']}{' (technical, ignored)' if c['technical'] else ''}")
|
|
if a.ui_label:
|
|
print(json.dumps(record(a.ui_label, a.codigo, changes), ensure_ascii=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|