Files
GTI_GERENCIADOR_FISCAL/fiscal/scripts/generate_dry_run_plan.py
T
gti d21a9d1140 Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
2026-09-29 14:43:23 -03:00

118 lines
5.6 KiB
Python

"""Generate the dry-run ERP write plan artifact for the 20-product sample.
Calls the running fiscal-hub /v1/classify/batch (read-only classification,
no Firebird access, no dataset creation) and turns each decision into
field-level write proposals via central.app.dry_run_planner. Never issues
an ERP write.
"""
import argparse
import json
import sys
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "central"))
from app.dry_run_planner import build_field_proposals, mva_evidence, split_by_eligibility # noqa: E402
def classify_batch(hub_url, token, products):
payload = {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"]} for p in products]}
request = urllib.request.Request(
f"{hub_url}/v1/classify/batch",
data=json.dumps(payload, ensure_ascii=True).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
method="POST",
)
with urllib.request.urlopen(request) as response:
return json.load(response)["results"]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--sample", default=str(ROOT / "docs" / "SAMPLE_PRODUCTS_V1.json"))
parser.add_argument("--hub-url", default="http://localhost:8000")
parser.add_argument("--token", required=True)
parser.add_argument("--output", default=str(ROOT / "docs" / "DRY_RUN_PLAN_V1.json"))
parser.add_argument("--artifact-name", default="DRY_RUN_PLAN_V1")
parser.add_argument("--firebird-current", default=None, help="JSON file with fresh CODIGO/IPI/CEST read from Firebird (read-only). Omit to keep current values unknown.")
args = parser.parse_args()
sample = json.loads(Path(args.sample).read_text(encoding="utf-8"))
products = sample["sample_without_ncm"]
decisions = classify_batch(args.hub_url, args.token, products)
fresh_by_code = {}
if args.firebird_current:
for row in json.loads(Path(args.firebird_current).read_text(encoding="utf-8")):
fresh_by_code[row["CODIGO"]] = row
field_proposals, mva_records = [], []
for product, decision in zip(products, decisions):
code = product["CODIGO"]
fresh = fresh_by_code.get(code)
if fresh is not None:
current = {"ipi": fresh.get("IPI"), "cest": fresh.get("CEST"), "ex_tipi": fresh.get("EX_TIPI")}
else:
current = {}
if "CEST" in product:
current["cest"] = product["CEST"]
field_proposals.extend(build_field_proposals(code, decision, current))
mva_records.append(mva_evidence(code, decision))
split = split_by_eligibility(field_proposals)
ipi_rows = [p for p in field_proposals if p["field"] == "IPI"]
ex_rows = [p for p in field_proposals if p["field"] == "EX_TIPI"]
cest_rows = [p for p in field_proposals if p["field"] == "CEST"]
ipi_metrics = {
"ipi_numeric_proposals": sum(1 for p in ipi_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")),
"ipi_non_numeric_status": sum(1 for p in ipi_rows if p["status"] == "NON_NUMERIC_IPI_STATUS"),
"ipi_source_blank": sum(1 for p in ipi_rows if p["status"] == "SOURCE_BLANK"),
"ipi_same_value": sum(1 for p in ipi_rows if p["status"] == "SAME_VALUE"),
"ipi_change_required": sum(1 for p in ipi_rows if p["status"] == "CHANGE_REQUIRED"),
"ipi_write_eligible": sum(1 for p in ipi_rows if p["write_eligible"]),
# every emitted EX_TIPI row implies TIPI had at least one EX-specific entry for that NCM.
"ex_tipi_db_match_products": len(ex_rows),
"ex_tipi_concrete_proposals": sum(1 for p in ex_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")),
"ex_tipi_candidates": sum(1 for p in ex_rows if p["status"] == "CANDIDATE"),
"ex_tipi_write_eligible": sum(1 for p in ex_rows if p["write_eligible"]),
}
cest_metrics = {
"cest_db_match_products": sum(1 for p in cest_rows if p["status"] != "NO_CEST_MAPPING_FOUND" and p["reason_code"] != "NO_CEST_MAPPING_FOUND_FOR_NCM"),
"cest_confirmed": sum(1 for p in cest_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")),
"cest_candidates": sum(1 for p in cest_rows if p["status"] == "CANDIDATE"),
"cest_unresolved": sum(1 for p in cest_rows if p["status"] == "UNRESOLVED"),
"cest_write_eligible": sum(1 for p in cest_rows if p["write_eligible"]),
}
artifact = {
"artifact": args.artifact_name,
"firebird_read": "OK" if fresh_by_code else "BLOCKED_EXTERNAL_MISSING_CREDENTIALS",
"firebird_writes": 0,
"products_planned": len(products),
"current_value_source": (
f"live read-only SELECT from C000025 via {args.firebird_current}"
if fresh_by_code else
"docs/SAMPLE_PRODUCTS_V1.json (CEST) — C000025.IPI has no authorized offline source; FB_USER/FB_PASSWORD unavailable in this environment"
),
"field_proposals": field_proposals,
"mva_evidence": mva_records,
"ipi_metrics": ipi_metrics,
"cest_metrics": cest_metrics,
"write_eligible_count": len(split["eligible"]),
"write_blocked_count": len(split["blocked"]),
}
Path(args.output).write_text(json.dumps(artifact, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps({
"products": len(products),
"field_proposals": len(field_proposals),
"write_eligible": len(split["eligible"]),
"write_blocked": len(split["blocked"]),
"output": args.output,
}, indent=2))
if __name__ == "__main__":
main()