git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
115 lines
5.5 KiB
Python
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()
|