Files
GTI_GERENCIADOR_FISCAL/fiscal/scripts/discover_inbound_fiscal_evidence.py
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

115 lines
5.5 KiB
Python

"""Discover inbound fiscal evidence from SGV databases using SELECT-only access."""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
from typing import Any, Iterable
ROOT = Path(__file__).resolve().parents[1]
SENSITIVE = re.compile(r"CNPJ|CPF|XML|CHAVE|RAZAO|NOME|ENDERECO|FONE|EMAIL", re.I)
TOKEN_GROUPS = {
"HEADER": {"NOTA", "NF", "DOCUMENTO", "CABECALHO", "EMISSAO", "SERIE"},
"ITEM": {"ITEM", "CODPRODUTO", "CODIGO", "QTD", "QUANTIDADE", "VALOR"},
"SUPPLIER": {"FORNEC", "EMITENTE", "CNPJ", "CPF", "RAZAO"},
"KEY": {"CHAVE", "NFE", "NFCE", "ACCESS", "PROTOCOLO"},
"FISCAL_ITEM": {"NCM", "CFOP", "CST", "CSOSN", "CEST", "ORIG", "ICMS", "PIS", "COFINS", "IPI", "MVA"},
}
def classify_evidence_source(table: str, columns: Iterable[str]) -> set[str]:
text = f"{table} {' '.join(columns)}".upper()
result = {category for category, tokens in TOKEN_GROUPS.items() if any(token in text for token in tokens)}
return result or {"UNKNOWN"}
def discover_from_rows(column_rows: Iterable[tuple[Any, ...]], relationship_rows: Iterable[tuple[Any, ...]]) -> dict[str, Any]:
tables: dict[str, dict[str, Any]] = {}
for table, field, field_type, field_length in column_rows:
table = str(table).strip()
field = str(field).strip()
record = tables.setdefault(table, {"columns": [], "categories": [], "xml_or_blob": False})
if not SENSITIVE.search(field):
record["columns"].append(field)
if field_type == 261 or re.search(r"XML|BLOB|PAYLOAD|DOCUMENT", field, re.I):
record["xml_or_blob"] = True
for table, record in tables.items():
categories = classify_evidence_source(table, record["columns"])
record["categories"] = sorted(categories)
relationships = []
for from_table, from_field, to_table, to_field in relationship_rows:
relationships.append({"from_table": str(from_table).strip(), "from_field": str(from_field).strip(), "to_table": str(to_table).strip(), "to_field": str(to_field).strip()})
return {"tables": tables, "relationships": relationships}
def safe_row_count(cursor: Any, table: str) -> dict[str, Any] | int:
try:
cursor.execute(f'SELECT COUNT(*) FROM "{table}"')
return int(cursor.fetchone()[0])
except Exception as exc: # external tables can be blocked by server policy
return {"status": "ROW_COUNT_UNAVAILABLE", "error_type": type(exc).__name__}
def _load_env() -> None:
path = ROOT / ".env"
if path.exists():
for line in path.read_text(encoding="utf-8").splitlines():
if "=" in line and not line.lstrip().startswith("#"):
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
def discover_database(path: Path) -> dict[str, Any]:
import fdb
conn = fdb.connect(
database=str(path),
user=os.environ["FB_USER"],
password=os.environ["FB_PASSWORD"],
fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"),
isolation_level=fdb.ISOLATION_LEVEL_READ_COMMITED_RO,
)
try:
cur = conn.cursor()
cur.execute("""SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, COALESCE(f.RDB$CHARACTER_LENGTH, f.RDB$FIELD_LENGTH)
FROM RDB$RELATION_FIELDS rf JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE
JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME
WHERE COALESCE(r.RDB$SYSTEM_FLAG, 0) = 0""")
columns = cur.fetchall()
cur.execute("""SELECT TRIM(rc.RDB$RELATION_NAME), TRIM(s.RDB$FIELD_NAME), TRIM(rc2.RDB$RELATION_NAME), TRIM(s2.RDB$FIELD_NAME)
FROM RDB$RELATION_CONSTRAINTS rc
JOIN RDB$INDEX_SEGMENTS s ON s.RDB$INDEX_NAME = rc.RDB$INDEX_NAME
JOIN RDB$REF_CONSTRAINTS ref ON ref.RDB$CONSTRAINT_NAME = rc.RDB$CONSTRAINT_NAME
JOIN RDB$RELATION_CONSTRAINTS rc2 ON rc2.RDB$CONSTRAINT_NAME = ref.RDB$CONST_NAME_UQ
JOIN RDB$INDEX_SEGMENTS s2 ON s2.RDB$INDEX_NAME = rc2.RDB$INDEX_NAME
WHERE rc.RDB$CONSTRAINT_TYPE = 'FOREIGN KEY'""")
relationships = cur.fetchall()
result = discover_from_rows(columns, relationships)
for table, record in result["tables"].items():
record["row_count"] = safe_row_count(cur, table)
result.update({"database": path.name, "mode": "READ_ONLY", "firebird_writes": 0, "xml_documents_available": any(r["xml_or_blob"] for r in result["tables"].values())})
return result
finally:
conn.close()
def main() -> None:
_load_env()
paths = [ROOT / "TESTESOFICIAIS.FDB"] + sorted((ROOT / "Banco").glob("*.FDB"))
output = {"artifact": "ERP_INBOUND_FISCAL_EVIDENCE_MAP_V1", "mode": "READ_ONLY", "databases": [], "firebird_writes": 0}
for path in paths:
try:
output["databases"].append(discover_database(path))
except Exception as exc: # record unavailable evidence without guessing
output["databases"].append({"database": path.name, "mode": "READ_ONLY", "status": "UNAVAILABLE", "error_type": type(exc).__name__, "firebird_writes": 0})
destination = ROOT / "docs" / "ERP_INBOUND_FISCAL_EVIDENCE_MAP_V1.json"
destination.write_text(json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"artifact": output["artifact"], "databases": len(output["databases"]), "firebird_writes": 0}))
if __name__ == "__main__":
main()