Files
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

87 lines
5.2 KiB
Python

"""Human-facing status of every tracked product: nothing may silently 'do nothing'. Computed from the resolver result and refined when the Agent reports the write outcome."""
from __future__ import annotations
from sqlalchemy import select, update
LABELS = {
"ENRIQUECIDO": "Enriquecido: campos seguros preenchidos.",
"PRONTO_PARA_PREENCHER": "Evidência suficiente: aguardando a escrita segura pelo Agent.",
"SIMULADO": "Simulação (DRY_RUN): há candidato, nada foi gravado.",
"AGUARDANDO_EVIDENCIA": "Aguardando evidência: nenhuma fonte confiável ainda.",
"EAN_AUSENTE": "Sem EAN e sem produto único verificado na base: informe o código de barras ou peça a decisão de um administrador.",
"EAN_INVALIDO": "Código de barras inválido: nenhuma fonte externa foi consultada.",
"NOME_AMBIGUO": "Nome genérico ou ambíguo demais para identificar o produto com segurança.",
"SEM_MATCH_DE_PRODUTO": "Nenhum produto conhecido corresponde a este nome/EAN.",
"NCM_NAO_VERIFICADO": "Produto identificado, mas o NCM ainda não foi verificado (falta segunda fonte independente ou decisão de administrador).",
"CONFLITO_DE_NCM": "As fontes divergem sobre o NCM: nada é preenchido até um administrador decidir.",
"PROVIDER_SEM_QUOTA": "Limite diário das fontes externas atingido; será repetido após o reset.",
"PROVIDER_NAO_CONFIGURADO": "Nenhuma fonte externa com NCM está configurada.",
"AGUARDANDO_RETRY": "Fontes indisponíveis agora; nova tentativa automática agendada.",
"HUMAN_OVERRIDE": "Alterado por uma pessoa: nunca mais preenchido automaticamente.",
"TECHNICAL_PLACEHOLDER": "NCM provisório (00000000) reconhecido como 'ainda não classificado'.",
"WRITE_FAILED": "A escrita segura foi recusada ou falhou (veja o motivo).",
"VERIFICACAO_POS_WRITE_FALHOU": "A escrita foi feita mas a releitura não confirmou o valor.",
"NADA_A_PREENCHER": "Todos os campos elegíveis já estão preenchidos.",
"FUNCAO_DESATIVADA": "Enriquecimento desativado para esta instalação.",
}
def classify(result: dict, *, has_gtin: bool, specific_name: bool) -> tuple[str, str]:
"""(ui_status, reason_code) from a resolve() result."""
fields = result.get("fields") or {}
ncm = fields.get("CLASSIFICACAO_FISCAL") or {}
state = result.get("state")
if state == "FEATURE_DISABLED":
return "FUNCAO_DESATIVADA", state
if state == "INVALID_GTIN":
return "EAN_INVALIDO", state
if state == "PENDING_PROVIDER_QUOTA":
return "PROVIDER_SEM_QUOTA", state
if state in ("PENDING_PROVIDERS_OFFLINE", "PENDING_TIME_BUDGET"):
return "AGUARDANDO_RETRY", state
if state == "CONFLICT" or ncm.get("status") == "CONFLICT":
return "CONFLITO_DE_NCM", "CONFLICT"
if ncm.get("status") == "HUMAN_OVERRIDDEN":
return "HUMAN_OVERRIDE", "HUMAN_OVERRIDDEN"
if any(f.get("status") == "AUTO_FILL_SAFE" for f in fields.values()):
return "PRONTO_PARA_PREENCHER", state or "RESOLVED"
if ncm.get("status") == "ALREADY_SET" and not any(f.get("status") in ("AUTO_FILL_SAFE", "SUGGESTION_ONLY") for f in fields.values()):
return "NADA_A_PREENCHER", "ALL_FIELDS_SET"
if not has_gtin:
if state == "AMBIGUOUS" or not specific_name:
return "NOME_AMBIGUO", "GENERIC_OR_AMBIGUOUS_NAME"
return "EAN_AUSENTE", "NAME_ONLY_NO_VERIFIED_MATCH"
if ncm.get("status") == "SUGGESTION_ONLY":
return "NCM_NAO_VERIFICADO", ncm.get("rule") or "NCM_NOT_VERIFIED"
if state == "NOT_FOUND":
return "SEM_MATCH_DE_PRODUTO", "GTIN_UNKNOWN"
return "AGUARDANDO_EVIDENCIA", state or "INSUFFICIENT_EVIDENCE"
def after_write(outcomes: list[dict], mode: str) -> tuple[str | None, str | None]:
"""Refines the status with what the Agent reported (APPLIED / DRY_RUN / ABORTED / FAILED / HUMAN_OVERRIDDEN)."""
kinds = {o.get("outcome") for o in outcomes}
reasons = [o.get("reason") for o in outcomes if o.get("reason")]
if "HUMAN_OVERRIDDEN" in kinds and "APPLIED" not in kinds:
return "HUMAN_OVERRIDE", "HUMAN_OVERRIDDEN"
if "APPLIED" in kinds:
return "ENRIQUECIDO", None
if "FAILED" in kinds:
return ("VERIFICACAO_POS_WRITE_FALHOU" if "POST_WRITE_VERIFICATION_FAILED" in reasons else "WRITE_FAILED"), (reasons[0] if reasons else "FAILED")
if "ABORTED" in kinds:
return "WRITE_FAILED", (reasons[0] if reasons else "ABORTED")
if "DRY_RUN" in kinds:
return "SIMULADO", "DRY_RUN_NO_WRITE"
return None, None
def mark_for_reevaluation(session, installation_id: int, *, codes: list[str] | None = None, gtin: str | None = None) -> int:
"""Flags tracked, NOT-yet-enriched products for a new evaluation (KB/cache first; providers only when really needed). Idempotent; never touches enriched / human-overridden ones."""
from ..models import ProductEnrichment
q = update(ProductEnrichment).where(ProductEnrichment.installation_id == installation_id, ProductEnrichment.ui_status.notin_(["ENRIQUECIDO", "HUMAN_OVERRIDE"]) | ProductEnrichment.ui_status.is_(None))
if codes:
q = q.where(ProductEnrichment.product_code.in_(codes))
if gtin:
q = q.where(ProductEnrichment.gtin == gtin)
return session.execute(q.values(reevaluate=True)).rowcount or 0