186 lines
8.9 KiB
Python
186 lines
8.9 KiB
Python
"""Assisted single-session mapping of the SGV "Cadastro de Produto" fiscal screen (READ-ONLY tool).
|
|
|
|
python scripts/map_product_ui_fields.py --product <CODIGO>
|
|
|
|
For each UI field the tool shows CURRENT and SUGGESTED_TEMP, waits for ENTER while the operator changes ONLY that
|
|
field in the SGV and saves, re-reads the whole C000025 row, diffs it against the previous baseline and records
|
|
which column(s) changed. The tool never writes to Firebird and never touches the GUI. State is persisted after every
|
|
step (docs/SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1.json) so the session can be resumed.
|
|
|
|
Binding classes: DIRECT_BINDING (one non-technical column changed) | DERIVED/COUPLED_FIELD (several changed;
|
|
ALL recorded, none chosen) | AUTOMATIC_METADATA (only timestamp-like technical columns) | AMBIGUOUS (nothing changed
|
|
or several changed without the expected one).
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "agent" / "src")); sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
SESSION = ROOT / "docs" / "SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1.json"
|
|
# Columns the ERP rewrites by itself on every save. Only these are ignored, and they are still listed in the record.
|
|
TECHNICAL = {"ULTIMA_ALTERACAO", "DATA_ALTERACAO", "DATA_ULTIMA_ALTERACAO", "USUARIO_ALTERACAO"}
|
|
|
|
# ui_label, hypothesised column (never trusted), candidate temporary values (first one different from CURRENT wins)
|
|
SEQUENCE = [
|
|
("NCM", "CLASSIFICACAO_FISCAL", ["22030000", "19022000"]),
|
|
("CFOP entrada", "CFOP_ENTRADA", ["1102", "1949"]),
|
|
("CFOP saída", "CFOP", ["5405", "5102", "5949"]),
|
|
("CST ICMS entrada", "CST_ENTRADA", ["040", "000", "060"]),
|
|
("CST ICMS saída", "CST", ["041", "000", "060"]),
|
|
("Alíquota ICMS", "ALIQUOTA", ["12", "18", "17"]),
|
|
("CEST", "CEST", ["0601100", "1708400"]),
|
|
("CSOSN", "CSOSN", ["103", "500", "102"]),
|
|
("PIS entrada", "COD_ENTRADA_PIS", ["73", "98", "50"]),
|
|
("PIS saída", "COD_SAIDA_PIS", ["04", "01", "49"]),
|
|
("Alíquota PIS", "ALIQ_PIS", ["0,65", "1,65"]),
|
|
("COFINS entrada", "COD_ENTRADA_COFINS", ["73", "98", "50"]),
|
|
("COFINS saída", "COD_SAIDA_COFINS", ["04", "01", "49"]),
|
|
("Alíquota COFINS", "ALIQ_COFINS", ["3", "7,6"]),
|
|
("IPI", "IPI", ["5", "10", "15"]),
|
|
("CST IPI", "CST_IPI", ["50", "00", "99"]),
|
|
("EX TIPI", "EX_TIPI", ["1", "2"]),
|
|
("Origem", "ORIGEM", ["1", "2", "0"]),
|
|
("IAT", "IAT", ["T", "A"]),
|
|
("Incidência PIS/COFINS", "INCIDENCIA_PISCOFINS", ["MONOFÁSICA", "SUBST.TRIBUTÁRIA", "NENHUMA"]),
|
|
("IPPT", "IPPT", ["P", "T"]),
|
|
]
|
|
|
|
|
|
def _same(a, b):
|
|
if a is None or b is None:
|
|
return a is None and b is None
|
|
try:
|
|
return float(str(a).replace(",", ".")) == float(str(b).replace(",", "."))
|
|
except ValueError:
|
|
return str(a).strip().upper() == str(b).strip().upper()
|
|
|
|
|
|
def suggest_temp(current, candidates):
|
|
"""First candidate that differs from the current value (never the same value, never blank)."""
|
|
for c in candidates:
|
|
if not _same(current, c):
|
|
return c
|
|
return None
|
|
|
|
|
|
def classify(changes, expected_column):
|
|
"""changes: [{'FIELD','BEFORE','AFTER','technical'}]. Never picks a column when several changed."""
|
|
real = [c["FIELD"] for c in changes if not c["technical"]]
|
|
tech = [c["FIELD"] for c in changes if c["technical"]]
|
|
if not real:
|
|
return "AUTOMATIC_METADATA" if tech else "AMBIGUOUS", "no fiscal column changed" if not tech else "only technical timestamp columns changed"
|
|
if len(real) == 1:
|
|
return "DIRECT_BINDING", "exactly one column changed" + ("" if real[0] == expected_column else f" (differs from hypothesis {expected_column})")
|
|
if expected_column in real:
|
|
return "DERIVED/COUPLED_FIELD", f"{len(real)} columns changed together; none selected"
|
|
return "AMBIGUOUS", f"{len(real)} columns changed and none is the hypothesis {expected_column}"
|
|
|
|
|
|
def compute_diff(before, after):
|
|
return [{"FIELD": f, "BEFORE": before.get(f), "AFTER": after.get(f), "technical": f in TECHNICAL}
|
|
for f in sorted(set(before) | set(after)) if before.get(f) != after.get(f)]
|
|
|
|
|
|
def new_session(product):
|
|
return {"artifact": "SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1", "database": "TESTESOFICIAIS.FDB", "product": product, "started": datetime.now().isoformat(timespec="seconds"),
|
|
"initial_snapshot": None, "baseline": None, "results": [], "firebird_writes": 0}
|
|
|
|
|
|
def save(session):
|
|
SESSION.write_text(json.dumps(session, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
|
|
|
|
|
|
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 run_step(session, ui_label, column, candidates, read_row, ask):
|
|
baseline = session["baseline"]
|
|
current = baseline.get(column) if column else None
|
|
temp = suggest_temp(current, candidates) if candidates else None
|
|
print(f"\nUI_FIELD={ui_label}\nCURRENT={current}\nSUGGESTED_TEMP={temp}\nHYPOTHESIS_COLUMN={column} (not trusted)")
|
|
answer = ask("Change ONLY this field in the SGV and save, then press ENTER [s=skip (not on screen), q=quit]: ").strip().lower()
|
|
if answer == "q":
|
|
return "quit"
|
|
if answer == "s":
|
|
session["results"].append({"ui_label": ui_label, "hypothesis_column": column, "status": "NOT_ON_SCREEN", "at": datetime.now().isoformat(timespec="seconds")})
|
|
save(session)
|
|
return "skipped"
|
|
after = read_row()
|
|
changes = compute_diff(baseline, after)
|
|
binding, why = classify(changes, column)
|
|
rec = {"ui_label": ui_label, "hypothesis_column": column, "current": current, "suggested_temp": temp, "status": "TESTED", "binding_type": binding, "reason": why,
|
|
"db_fields_changed": [c["FIELD"] for c in changes if not c["technical"]], "technical_fields_changed": [c["FIELD"] for c in changes if c["technical"]],
|
|
"changes": changes, "at": datetime.now().isoformat(timespec="seconds")}
|
|
session["results"].append(rec)
|
|
session["baseline"] = after # new state is the baseline for the next field
|
|
save(session)
|
|
print(f"UI_FIELD={ui_label}\nDB_FIELDS_CHANGED={rec['db_fields_changed']}\nBEFORE={ {c['FIELD']: c['BEFORE'] for c in changes} }\nAFTER={ {c['FIELD']: c['AFTER'] for c in changes} }\nBINDING={binding} ({why})")
|
|
return "tested"
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--product", required=True)
|
|
ap.add_argument("--restart", action="store_true", help="discard a previous session file for this product")
|
|
a = ap.parse_args(argv)
|
|
load_env()
|
|
from sgv_product_snapshot import read_row # read-only single-row reader (read-only isolation)
|
|
from gti_fiscal_agent.firebird_reader import parse_company_context, _connect
|
|
path = os.environ.get("FIREBIRD_DATABASE_PATH", "")
|
|
if Path(path).name.upper() != "TESTESOFICIAIS.FDB":
|
|
raise SystemExit("this experiment is only for the disposable TESTESOFICIAIS.FDB (FIREBIRD_DATABASE_PATH)")
|
|
reader = lambda: read_row(os.environ, a.product) # noqa: E731
|
|
|
|
if SESSION.exists() and not a.restart and json.loads(SESSION.read_text(encoding="utf-8")).get("product") == a.product:
|
|
session = json.loads(SESSION.read_text(encoding="utf-8"))
|
|
print(f"Resuming session ({len(session['results'])} results already recorded).")
|
|
session["baseline"] = reader() # honour any manual change since the last step
|
|
else:
|
|
session = new_session(a.product)
|
|
session["initial_snapshot"] = session["baseline"] = reader()
|
|
save(session)
|
|
conn = _connect(os.environ)
|
|
try:
|
|
cur = conn.cursor(); cur.execute("SELECT UF, CRT FROM C000004")
|
|
session["company_context_read_from_db"] = parse_company_context(cur.fetchall())
|
|
finally:
|
|
conn.close()
|
|
done = {r["ui_label"] for r in session["results"]}
|
|
for ui_label, column, candidates in SEQUENCE:
|
|
if ui_label in done:
|
|
continue
|
|
if run_step(session, ui_label, column, candidates, reader, input) == "quit":
|
|
break
|
|
else:
|
|
while True:
|
|
extra = input("\nAny OTHER fiscal field visible on the screen? Type its label (or ENTER to finish): ").strip()
|
|
if not extra:
|
|
break
|
|
if run_step(session, extra, None, [], reader, input) == "quit":
|
|
break
|
|
session["finished"] = datetime.now().isoformat(timespec="seconds")
|
|
save(session)
|
|
changed = {}
|
|
for r in session["results"]:
|
|
for c in r.get("changes", []):
|
|
if not c["technical"]:
|
|
changed.setdefault(c["FIELD"], session["initial_snapshot"].get(c["FIELD"]))
|
|
print("\nSession saved:", SESSION)
|
|
if changed:
|
|
print("RESTORE_ORIGINAL_VALUES (set these back in the SGV):")
|
|
for f, v in changed.items():
|
|
print(f" {f} = {v}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|