Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user