feat: unified fiscal coverage - PIS/COFINS applicability + conditional RTC in one definition, one queue, one panel

- central/app/unified_fiscal_coverage.py: pure rules (PIS/COFINS NOT_APPLICABLE/REQUIRED/OPTIONAL/NEEDS_REVIEW, RTC only when the ERP switch is on)
- agent: read-only loader (unified_fiscal.py) replaces the legacy/expanded module; findings feed the single operational state, coverage and blocker queue
- hub: BLOCKED_FISCAL_DATA blockers (PIS/COFINS/RTC) share the queue; stale ones close on the next full pass; dashboard breakdown gains CEST/PIS/COFINS/RTC
- panel: legacy/expanded coverage block removed; discreet 'Reforma Tributaria: Ativa/Desativada'
- docs: unified_fiscal_completeness, pis_cofins_applicability, rtc_pis_cofins_model + official source manifests; read-only E2E script

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 16:39:53 -03:00
co-authored by Claude Sonnet 5
parent d897cceebb
commit b15dbea213
32 changed files with 3979 additions and 517 deletions
+4 -1
View File
@@ -11,7 +11,7 @@ from .classification import ENGINE_VERSION, Classifier
from .config import settings
from .datasets.service import DatasetService
from .jobs import SYNC_KINDS, claim_next_job, complete_job, create_job, fail_job, schedule_automatic_sync
from .blocked_products import list_blockers, resolve_blocker, resolve_product_blockers, upsert_blocker
from .blocked_products import list_blockers, resolve_blocker, resolve_product_blockers, resolve_stale_fiscal_blockers, upsert_blocker
from .context_keys import validate_entry
from .models import AgentSnapshot, BlockedProduct, FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Installation, Job
from .events import record_event
@@ -422,10 +422,13 @@ def router(session_factory, bootstrap_token):
if job is None or job.agent_id != agent.id: raise HTTPException(404, "job not found")
result_summary = payload.get("result_summary", {})
installation = session.scalar(select(Installation).where(Installation.agent_id == agent.id))
pass_started = datetime.now(timezone.utc)
for blocker in result_summary.get("blocked_products", []):
upsert_blocker(session, agent.tenant_id, blocker, installation_id=installation.id if installation else None)
for product_code in result_summary.get("resolved_products", []):
resolve_product_blockers(session, agent.tenant_id, product_code, installation_id=installation.id if installation else None)
if installation is not None and (result_summary.get("fiscal_context") or {}).get("evaluated_all"):
resolve_stale_fiscal_blockers(session, agent.tenant_id, installation.id, pass_started)
if not complete_job(session, job, result_summary): raise HTTPException(409, "job is not RUNNING")
if installation is not None:
_apply_job_outcome(session, agent, installation, job, result_summary, failed=False)
+18 -1
View File
@@ -18,7 +18,7 @@ def upsert_blocker(session, tenant_id: int, payload: dict, *, installation_id: i
row = session.scalar(select(BlockedProduct).where(BlockedProduct.blocker_fingerprint == fingerprint))
now = datetime.now(timezone.utc)
if row is None:
row = BlockedProduct(tenant_id=tenant_id, installation_id=installation_id, company=payload.get("company", ""), product_code=str(payload.get("product_code", "")), gtin=payload.get("gtin"), description=payload.get("description"), blocker_type=payload.get("blocker_type", "BLOCKED_MISSING_EVIDENCE"), field=payload.get("field", ""), reason_code=payload.get("reason_code", ""), evidence_summary=payload.get("evidence_summary") or {}, input_fingerprint=payload.get("input_fingerprint", ""), blocker_fingerprint=fingerprint)
row = BlockedProduct(tenant_id=tenant_id, installation_id=installation_id, company=payload.get("company", ""), product_code=str(payload.get("product_code", "")), gtin=payload.get("gtin"), description=payload.get("description"), blocker_type=payload.get("blocker_type", "BLOCKED_MISSING_EVIDENCE"), field=payload.get("field", ""), reason_code=payload.get("reason_code", ""), evidence_summary=payload.get("evidence_summary") or {}, input_fingerprint=payload.get("input_fingerprint", ""), blocker_fingerprint=fingerprint, first_seen_at=now, last_seen_at=now)
session.add(row)
else:
row.last_seen_at = now
@@ -73,3 +73,20 @@ def resolve_product_blockers(session, tenant_id: int, product_code: str, source:
if rows:
session.commit()
return len(rows)
FISCAL_BLOCKER_FIELDS = ("PIS", "COFINS", "RTC")
def resolve_stale_fiscal_blockers(session, tenant_id: int, installation_id: int, since: datetime) -> int:
"""After a FULL evaluation of the unified fiscal fields, every open PIS/COFINS/RTC blocker of the installation that this pass did not
re-report (last_seen_at < since) no longer holds: the ERP data changed, or RTC was switched off. Other blocker fields are untouched."""
rows = session.scalars(select(BlockedProduct).where(BlockedProduct.tenant_id == tenant_id, BlockedProduct.installation_id == installation_id,
BlockedProduct.resolved_at.is_(None), BlockedProduct.field.in_(FISCAL_BLOCKER_FIELDS), BlockedProduct.last_seen_at < since)).all()
now = datetime.now(timezone.utc)
for row in rows:
row.resolved_at = now
row.resolution_source = "AUTO_REEVALUATION"
if rows:
session.commit()
return len(rows)
+1 -1
View File
@@ -16,7 +16,7 @@ from .panel_presentation import health_reason_label, blocker_reason_label, block
HEALTH_STATES = ("ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN")
BLOCKER_FIELDS = ("CFOP", "ST", "ICMS", "CSOSN", "NCM", "OTHER")
BLOCKER_FIELDS = ("NCM", "CEST", "CFOP", "ST", "ICMS", "CSOSN", "PIS", "COFINS", "RTC", "OTHER")
ATTENTION_CANDIDATE_LIMIT = 10
INACTIVE_ENROLLMENT_STATES = ("REVOKED", "FAILED", "EXPIRED")
+12 -3
View File
@@ -6,7 +6,7 @@ import json
STATES = {
"COMPLETE", "CORRECTED", "BLOCKED_MISSING_EVIDENCE", "BLOCKED_CONFLICT",
"BLOCKED_COMPANY_CONTEXT", "BLOCKED_SCHEMA", "FAILED_TECHNICAL",
"BLOCKED_COMPANY_CONTEXT", "BLOCKED_SCHEMA", "BLOCKED_FISCAL_DATA", "FAILED_TECHNICAL", "NEEDS_REVIEW",
}
@@ -17,7 +17,8 @@ def blocker_fingerprint(tenant: str, company: str, product_code: str, field: str
return hashlib.sha256(json.dumps(raw, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
def classify_product_state(product: dict, decision: dict, *, write_status: str | None = None) -> dict:
def classify_product_state(product: dict, decision: dict, *, write_status: str | None = None, fiscal: dict | None = None) -> dict:
"""`fiscal` = unified_fiscal_coverage.evaluate_product(...): PIS/COFINS + (only when the ERP has RTC on) cClassTrib. One definition of COMPLETE."""
status = str(decision.get("status") or "").upper()
if write_status == "FAILED":
state = "FAILED_TECHNICAL"
@@ -33,7 +34,13 @@ def classify_product_state(product: dict, decision: dict, *, write_status: str |
state = "CORRECTED"
else:
state = "COMPLETE"
return {"product_code": str(product.get("CODIGO") or ""), "state": state, "decision_status": status}
fiscal = fiscal or {}
if state in ("COMPLETE", "CORRECTED"):
if fiscal.get("blockers"):
state = "BLOCKED_FISCAL_DATA"
elif fiscal.get("needs_review"):
state = "NEEDS_REVIEW"
return {"product_code": str(product.get("CODIGO") or ""), "state": state, "decision_status": status, "fiscal_blockers": list(fiscal.get("blockers") or [])}
def should_retry_blocker(previous_input_fingerprint: str | None, current_input_fingerprint: str) -> bool:
@@ -44,6 +51,8 @@ def summarize_operational_states(states: list[dict]) -> dict:
counts = Counter(item.get("state") for item in states)
result = {state: counts.get(state, 0) for state in sorted(STATES)}
result["TOTAL_PRODUCTS"] = len(states)
result["UNCLASSIFIED"] = result["NEEDS_REVIEW"]
result["COMPLETE_PRODUCTS"] = result["COMPLETE"] + result["CORRECTED"]
result["SAFE_BLOCKED"] = sum(result[state] for state in STATES if state.startswith("BLOCKED_"))
result["AUTONOMOUSLY_PROCESSABLE_PRODUCTS"] = result["TOTAL_PRODUCTS"] - result["FAILED_TECHNICAL"]
result["AGENT_HEALTH"] = "HEALTHY" if result["FAILED_TECHNICAL"] == 0 else "DEGRADED"
+15 -16
View File
@@ -80,7 +80,7 @@ def operational_priority(blocker) -> str:
return "CRITICAL"
if blocker_type in {"BLOCKED_MISSING_EVIDENCE", "MISSING_EVIDENCE"}:
return "HIGH"
if field in {"CFOP", "ST", "ICMS", "CSOSN", "NCM"} or reason in {"INSUFFICIENT_EVIDENCE", "NO_EVIDENCE"}:
if field in {"CFOP", "ST", "ICMS", "CSOSN", "NCM", "CEST", "PIS", "COFINS", "RTC"} or reason in {"INSUFFICIENT_EVIDENCE", "NO_EVIDENCE"}:
return "NORMAL"
return "LOW"
@@ -120,16 +120,17 @@ def job_kind_label(value):
return _human_label(value, {"SYNC_INCREMENTAL": "Sincronização incremental", "INITIAL_SYNC": "Sincronização inicial", "REPROCESS_ALL": "Reprocessar tudo", "REPROCESS_PRODUCTS": "Reprocessar produtos"})
_RTC_LABELS = {
"CCLASSTRIB_MISSING": "cClassTrib ausente", "CCLASSTRIB_INVALID": "cClassTrib inválido", "CCLASSTRIB_NOT_IN_CATALOG": "cClassTrib fora do catálogo local",
"RTC_CST_INVALID": "CST IBS/CBS inválido", "PIS_CST_MISSING": "CST PIS ausente", "PIS_CST_INVALID": "CST PIS inválido", "PIS_RATE_INVALID": "Alíquota PIS inválida",
"COFINS_CST_MISSING": "CST COFINS ausente", "COFINS_CST_INVALID": "CST COFINS inválido", "COFINS_RATE_INVALID": "Alíquota COFINS inválida",
"PIS_COFINS_FLAG_INCONSISTENT": "Flag PIS/COFINS inconsistente",
_FISCAL_LABELS = {
"CCLASSTRIB_MISSING": "cClassTrib ausente", "CCLASSTRIB_INVALID_FORMAT": "cClassTrib com formato inválido", "CCLASSTRIB_NOT_IN_CATALOG": "cClassTrib fora do catálogo local",
"RTC_CST_MISSING": "CST IBS/CBS ausente no catálogo", "RTC_CATALOG_INVALID": "Catálogo RTC local inválido",
"PIS_CST_MISSING": "CST PIS ausente", "PIS_CST_INVALID": "CST PIS inválido", "PIS_RATE_MISSING": "Alíquota PIS ausente", "PIS_RATE_INVALID": "Alíquota PIS inválida", "PIS_CST_INCIDENCE_MISMATCH": "CST PIS incompatível com a incidência",
"COFINS_CST_MISSING": "CST COFINS ausente", "COFINS_CST_INVALID": "CST COFINS inválido", "COFINS_RATE_MISSING": "Alíquota COFINS ausente", "COFINS_RATE_INVALID": "Alíquota COFINS inválida", "COFINS_CST_INCIDENCE_MISMATCH": "CST COFINS incompatível com a incidência",
"PIS_COFINS_FLAG_INCONSISTENT": "Flag PIS/COFINS inconsistente", "BLOCKED_FISCAL_DATA": "Dado fiscal do cadastro",
}
def blocker_type_label(value):
return _human_label(value, {"BLOCKED_MISSING_EVIDENCE": "Evidência fiscal ausente", "MISSING_EVIDENCE": "Evidência fiscal ausente", "TECHNICAL_FAILURE": "Falha técnica", "CONNECTION_ERROR": "Erro de conexão", "OTHER": "Outro", **_RTC_LABELS})
return _human_label(value, {"BLOCKED_MISSING_EVIDENCE": "Evidência fiscal ausente", "MISSING_EVIDENCE": "Evidência fiscal ausente", "TECHNICAL_FAILURE": "Falha técnica", "CONNECTION_ERROR": "Erro de conexão", "OTHER": "Outro", **_FISCAL_LABELS})
_REASON_LABELS = {
@@ -143,7 +144,7 @@ _REASON_LABELS = {
def blocker_reason_label(value):
return _human_label(value, _REASON_LABELS)
return _human_label(value, {**_REASON_LABELS, **_FISCAL_LABELS})
def health_reason_label(value):
@@ -160,12 +161,10 @@ def priority_label(value):
return _human_label(value, {"CRITICAL": "Crítica", "HIGH": "Alta", "NORMAL": "Média", "MEDIUM": "Média", "LOW": "Baixa"})
def expanded_scope_view(summary):
"""Panel-ready view of the Agent's `expanded_scope` job summary (legacy and expanded coverage side by side). None when absent/unavailable."""
if not isinstance(summary, dict) or "expanded_scope" not in summary:
def rtc_status_view(summary):
"""Discreet Reforma Tributária status from the last completed job's `fiscal_context`. None when the Agent did not report it (older Agents)."""
context = (summary or {}).get("fiscal_context") if isinstance(summary, dict) else None
if not isinstance(context, dict) or "rtc_enabled" not in context:
return None
legacy, expanded, total = summary.get("legacy_scope") or {}, summary["expanded_scope"] or {}, summary.get("total_products") or 0
percent = lambda part: round(100 * part / total, 1) if total else None
return {"total": total, "legacy": legacy, "expanded": expanded, "legacy_percent": percent(legacy.get("complete", 0)), "expanded_percent": percent(expanded.get("complete", 0)),
"blockers": [{"type": key, "label": blocker_type_label(key), "count": count} for key, count in (summary.get("blockers_by_type") or {}).items()],
"capabilities": summary.get("capabilities") or {}, "local_catalog_only": bool(summary.get("local_catalog_only"))}
return {"enabled": bool(context["rtc_enabled"]), "label": "Ativa" if context["rtc_enabled"] else "Desativada", "supported": bool(context.get("rtc_supported")),
"catalog_valid": bool(context.get("rtc_catalog_valid"))}
+211
View File
@@ -0,0 +1,211 @@
"""UNIFIED fiscal completeness: ONE definition of "complete" for a product (pure, no I/O).
mandatory(product, company, rtc) = base fiscal fields (decided elsewhere: NCM/CEST/CFOP/ICMS/IPI ...)
+ PIS/COFINS when they APPLY to the product
+ RTC (cClassTrib -> CST IBS/CBS) ONLY when the ERP has the tax reform switched on.
There is no "legacy scope" and no "expanded scope": every finding here becomes a normal blocker in the single queue and
changes the single coverage number. Nothing here writes to the ERP or invents a fiscal value.
Sources (see docs/pis_cofins_applicability.md): SPED EFD-Contribuicoes table 4.3.3/4.3.4 (CST domain), 4.3.10-4.3.15 (special regimes),
LC 123/2006 art. 13/18 (PIS/Cofins inside the Simples DAS), Leis 9.718/98, 10.637/2002, 10.833/2003 (cumulative / non-cumulative),
NT 2025.002-RTC v1.51 (IBS/CBS fields), LC 214/2025 art. 348.
"""
from __future__ import annotations
import re
import unicodedata
from datetime import date
# Official CST PIS / CST COFINS domain (SPED tables 4.3.3 / 4.3.4). Membership only - never used to pick a value.
CST_DOMAIN = frozenset([f"{n:02d}" for n in (*range(1, 10), 49, *range(50, 57), *range(60, 68), *range(70, 76), 98, 99)])
RATE_REQUIRED_CST = frozenset({"01", "02"}) # taxable by percentage: a rate is needed
RATE_NOT_APPLICABLE_CST = frozenset({"03", "04", "05", "06", "07", "08", "09"}) # per-unit amount / monophasic / ST / zero / exempt / no incidence / suspended
INCIDENCE_CST = {"MONOFASICA": frozenset({"02", "03", "04"}), "SUBSTITUICAO": frozenset({"05"})} # 4.3.10/4.3.11 (02,03,04) and 4.3.12 (05)
SIMPLES_FAMILY = frozenset({"1", "2", "4"}) # CRT 1 Simples, 2 Simples excess sublimit, 4 MEI (LC 123/2006): PIS/Cofins are collected inside the DAS
PIS_COFINS_FIELDS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS")
FIELD_BY_BLOCKER_PREFIX = (("PIS", "PIS"), ("COFINS", "COFINS"), ("CCLASSTRIB", "RTC"), ("RTC", "RTC"))
def _text(value) -> str:
return "" if value is None else str(value).strip()
def blocker_category(blocker_type: str) -> str:
"""Dashboard category of a blocker type: PIS | COFINS | RTC | OTHER."""
for prefix, category in FIELD_BY_BLOCKER_PREFIX:
if blocker_type.startswith(prefix):
return category
return "OTHER"
def incidence_kind(value) -> str:
"""NONE | MONOFASICA | SUBSTITUICAO | UNKNOWN (accent/case-insensitive, encoding tolerant). Anything the SGV does not define is UNKNOWN, never NONE."""
raw = _text(value)
text = re.sub(r"[^A-Z]", "", unicodedata.normalize("NFKD", raw).encode("ascii", "ignore").decode().upper())
if not raw or text == "NENHUMA":
return "NONE"
if text.startswith("MONOF"):
return "MONOFASICA"
if text.startswith("SUBST"):
return "SUBSTITUICAO"
return "UNKNOWN"
def validate_cst(value) -> str:
code = _text(value)
if not code:
return "EMPTY"
return "VALID" if code in CST_DOMAIN else "INVALID"
def cst_suggestion(value) -> str | None:
"""A short numeric code that becomes an official CST once zero-padded to two digits ('4' -> '04'). Only a hint for the operator; never applied."""
code = _text(value)
return code.zfill(2) if code.isdigit() and len(code) == 1 and code.zfill(2) in CST_DOMAIN else None
def validate_rate(value) -> str:
text = _text(value)
if not text:
return "EMPTY"
if not re.fullmatch(r"\d{1,3}([.,]\d{1,4})?", text):
return "INVALID"
return "VALID" if float(text.replace(",", ".")) <= 100 else "INVALID"
# ------------------------------------------------------------------------------------------------- PIS / COFINS
def pis_cofins_applicability(product: dict, crt) -> dict:
"""{"state": NOT_APPLICABLE | REQUIRED | OPTIONAL | NEEDS_REVIEW, "basis": ...}.
NOT_APPLICABLE the product carries no product-level PIS/COFINS treatment at all (flag N/blank, incidence NENHUMA/blank, all six fields blank).
The tax is then settled elsewhere: inside the DAS (Simples family) or from the company default (regime normal).
REQUIRED the product itself claims a special treatment (flag S, or incidence MONOFASICA/SUBSTITUICAO): the exit CST (and a rate when the CST needs one) must be valid.
OPTIONAL no special claim, but some CST/rate was typed: it is not mandatory, yet whatever is there must be valid.
NEEDS_REVIEW the incidence text is not one the SGV defines (NENHUMA / MONOFASICA / SUBST.TRIBUTARIA): applicability cannot be decided.
"""
flag = _text(product.get("PISCOFINS")).upper()
kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
has_values = any(_text(product.get(c)) for c in PIS_COFINS_FIELDS)
if kind == "UNKNOWN":
return {"state": "NEEDS_REVIEW", "basis": "INCIDENCE_NOT_DEFINED_BY_SGV"}
if flag == "S" or kind in ("MONOFASICA", "SUBSTITUICAO"):
return {"state": "REQUIRED", "basis": "PRODUCT_DECLARES_SPECIAL_TREATMENT"}
if has_values:
return {"state": "OPTIONAL", "basis": "VALUES_TYPED_WITHOUT_SPECIAL_CLAIM"}
return {"state": "NOT_APPLICABLE", "basis": "SIMPLES_DAS" if _text(crt) in SIMPLES_FAMILY else "COMPANY_DEFAULT"}
def evaluate_pis_cofins(product: dict, crt) -> dict:
"""{"applicability": {...}, "blockers": [(type, reason)]}. Entry and exit are separate; a missing OPTIONAL value never blocks."""
app = pis_cofins_applicability(product, crt)
blockers: list[tuple[str, str]] = []
if app["state"] in ("NOT_APPLICABLE", "NEEDS_REVIEW"):
return {"applicability": app, "blockers": blockers}
kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
flag = _text(product.get("PISCOFINS")).upper()
for tax in ("PIS", "COFINS"):
entry, exit_, rate = product.get(f"COD_ENTRADA_{tax}"), product.get(f"COD_SAIDA_{tax}"), product.get(f"ALIQ_{tax}")
entry_state, exit_state, rate_state = validate_cst(entry), validate_cst(exit_), validate_rate(rate)
if entry_state == "INVALID":
blockers.append((f"{tax}_CST_INVALID", "ENTRY" + (f":UNPADDED_USE_{cst_suggestion(entry)}" if cst_suggestion(entry) else "")))
if exit_state == "INVALID":
blockers.append((f"{tax}_CST_INVALID", "EXIT" + (f":UNPADDED_USE_{cst_suggestion(exit_)}" if cst_suggestion(exit_) else "")))
if app["state"] == "REQUIRED" and exit_state == "EMPTY":
blockers.append((f"{tax}_CST_MISSING", "EXIT"))
if rate_state == "INVALID":
blockers.append((f"{tax}_RATE_INVALID", "RATE"))
if exit_state == "VALID":
code = _text(exit_)
if code in RATE_REQUIRED_CST and rate_state == "EMPTY":
blockers.append((f"{tax}_RATE_MISSING", f"CST_{code}_NEEDS_RATE"))
if kind in INCIDENCE_CST and code not in INCIDENCE_CST[kind]:
blockers.append((f"{tax}_CST_INCIDENCE_MISMATCH", f"{kind}:CST_{code}"))
if kind == "NONE" and app["state"] == "REQUIRED" and code in RATE_NOT_APPLICABLE_CST - {"03"} and flag == "S":
blockers.append((f"{tax}_CST_INCIDENCE_MISMATCH", f"NENHUMA:CST_{code}"))
if flag == "N" and kind != "NONE":
blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_N_WITH_SPECIAL_INCIDENCE"))
if flag == "S" and kind == "NONE" and not any(_text(product.get(c)) for c in PIS_COFINS_FIELDS):
blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_S_WITHOUT_INCIDENCE_OR_CODES"))
return {"applicability": app, "blockers": _dedupe(blockers)}
def _dedupe(items):
seen, out = set(), []
for item in items:
if item not in seen:
seen.add(item)
out.append(item)
return out
# ------------------------------------------------------------------------------------------------- RTC
def rtc_enabled(caps: dict) -> bool:
"""RTC is in scope ONLY when the ERP is switched to the reform (PARAMETROS.USAR_REFORMA_TRIBUTARIA truthy) AND its schema can hold it."""
return bool(caps.get("HAS_PRODUCT_CCLASSTRIB") and caps.get("HAS_RTC_RELATIONSHIP") and caps.get("RTC_ENABLED_BY_PARAM"))
def catalog_integrity(catalog: dict) -> dict:
"""catalog: {"classes": {cClassTrib: cst_or_None}, "cst": {code, ...}, "duplicates": int}."""
classes, cst = catalog.get("classes", {}), set(catalog.get("cst", ()))
bad_format = [c for c in classes if not re.fullmatch(r"\d{6}", str(c))]
bad_cst_format = [c for c in cst if not re.fullmatch(r"\d{3}", str(c))]
orphans = [c for c, s in classes.items() if s is None or str(s) not in cst]
return {"classes": len(classes), "cst": len(cst), "orphan_classes": len(orphans), "bad_class_format": len(bad_format), "bad_cst_format": len(bad_cst_format),
"duplicates": int(catalog.get("duplicates", 0)), "valid": bool(classes and cst and not orphans and not bad_format and not bad_cst_format and not catalog.get("duplicates"))}
def validate_cclasstrib(value, catalog: dict | None, today: date | None = None) -> str:
"""EMPTY | INVALID_FORMAT | NOT_IN_CATALOG | EXPIRED | CST_MISSING | VALID (CST IBS/CBS derived through the catalog, never stored on the product)."""
code = _text(value)
if not code:
return "EMPTY"
if not re.fullmatch(r"\d{6}", code):
return "INVALID_FORMAT"
if not catalog or code not in catalog.get("classes", {}):
return "NOT_IN_CATALOG"
validity = (catalog.get("validity") or {}).get(code)
if validity and today is not None:
start, end = validity
if (start and start > today) or (end and end < today):
return "EXPIRED"
cst = catalog["classes"][code]
return "VALID" if cst is not None and str(cst) in set(catalog.get("cst", ())) else "CST_MISSING"
def evaluate_rtc(product: dict, rtc: dict | None, today: date | None = None) -> dict:
"""rtc: {"enabled": bool, "catalog": {...}|None, "catalog_valid": bool}. Disabled => NOT_APPLICABLE and NO blocker, whatever CCLASSTRIB holds."""
if not rtc or not rtc.get("enabled"):
return {"applicability": {"state": "NOT_APPLICABLE", "basis": "RTC_DISABLED_IN_ERP"}, "blockers": []}
app = {"state": "REQUIRED", "basis": "RTC_ENABLED_IN_ERP"}
if not rtc.get("catalog_valid"):
return {"applicability": app, "blockers": [("RTC_CATALOG_INVALID", "LOCAL_CATALOG_FAILED_INTEGRITY")]}
state = validate_cclasstrib(product.get("CCLASSTRIB"), rtc.get("catalog"), today)
mapping = {"EMPTY": "CCLASSTRIB_MISSING", "INVALID_FORMAT": "CCLASSTRIB_INVALID_FORMAT", "NOT_IN_CATALOG": "CCLASSTRIB_NOT_IN_CATALOG", "EXPIRED": "CCLASSTRIB_NOT_IN_CATALOG", "CST_MISSING": "RTC_CST_MISSING"}
return {"applicability": app, "blockers": [(mapping[state], state)] if state in mapping else []}
# ------------------------------------------------------------------------------------------------- unified
def evaluate_product(product: dict, company: dict | None, rtc: dict | None, today: date | None = None) -> dict:
"""The single fiscal-completeness verdict for the fields this module owns.
Returns {"blockers": [{"type","category","reason"}], "applicability": {"PIS_COFINS": state, "RTC": state}, "mandatory_fields": [...], "needs_review": bool}."""
crt = (company or {}).get("crt")
pis = evaluate_pis_cofins(product, crt)
rtc_result = evaluate_rtc(product, rtc, today)
blockers = [{"type": kind, "category": blocker_category(kind), "reason": reason} for kind, reason in pis["blockers"] + rtc_result["blockers"]]
mandatory = []
if pis["applicability"]["state"] == "REQUIRED":
mandatory += ["COD_SAIDA_PIS", "COD_SAIDA_COFINS"]
if rtc_result["applicability"]["state"] == "REQUIRED":
mandatory.append("CCLASSTRIB")
return {"blockers": blockers, "applicability": {"PIS_COFINS": pis["applicability"]["state"], "RTC": rtc_result["applicability"]["state"]},
"basis": {"PIS_COFINS": pis["applicability"]["basis"], "RTC": rtc_result["applicability"]["basis"]}, "mandatory_fields": mandatory,
"needs_review": pis["applicability"]["state"] == "NEEDS_REVIEW"}
+2 -2
View File
@@ -15,7 +15,7 @@ from .config import settings
from .blocker_resolution import BlockerResolutionUnavailable, _company_key, resolve_blocker_with_context
from .context_keys import ALLOWED_KEYS, ENUMS
from .installation_identity import reference_agent_version
from .panel_presentation import expanded_scope_view, job_kind_label, job_status_label
from .panel_presentation import job_kind_label, job_status_label, rtc_status_view
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import create_session, current_session, require_csrf, verify_password
from .panel_presentation import format_ui_datetime
@@ -296,7 +296,7 @@ def router(session_factory):
if installation.processing_enabled and last_sync is not None:
next_sync = last_sync + timedelta(seconds=installation.sync_interval_seconds)
last_summary = session.scalar(select(Job.result_summary).where(Job.agent_id == agent.id, Job.status == "COMPLETED", Job.kind.in_(("INITIAL_SYNC", "SYNC_INCREMENTAL", "REPROCESS_ALL"))).order_by(Job.id.desc()).limit(1))
return {"expanded": expanded_scope_view(last_summary), "enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
return {"rtc": rtc_status_view(last_summary), "enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
"last_sync": format_ui_datetime(last_sync) if last_sync else None, "next_sync": format_ui_datetime(next_sync) if next_sync else None,
"last_changed": installation.last_sync_changed, "last_job": {"kind": last_job.kind, "status": last_job.status, "id": last_job.id, "kind_label": job_kind_label(last_job.kind), "status_label": job_status_label(last_job.status)} if last_job else None}