chore: establish project baseline
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user