fix: gate cClassTrib applicability by CRT and date

This commit is contained in:
gti
2026-09-25 17:47:04 -03:00
parent 95605c2ff5
commit 4a12640031
25 changed files with 498 additions and 72 deletions
+9 -4
View File
@@ -175,15 +175,20 @@ def rtc_status_view(summary, mode="DISABLED"):
except ValueError:
observed = None
source = context.get("rtc_source") or "PARAMETROS.USAR_REFORMA_TRIBUTARIA"
products = _rtc_products_view(context.get("rtc_resolution"), bool(context["rtc_enabled"]))
products = _rtc_products_view(context.get("rtc_resolution"), bool(context["rtc_enabled"]), context.get("cclasstrib_applicability"))
return {"products": products, "apply": _rtc_apply_view(summary.get("rtc_auto_apply"), mode), "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")), "observed_at": observed, "conflict": bool(context.get("rtc_param_conflict")),
"title": f"Lido no banco do cliente ({source})" + (f" em {observed}" if observed else "") + (" - linhas de PARAMETROS divergentes: RTC tratada como desativada" if context.get("rtc_param_conflict") else "")}
def _rtc_products_view(resolution, enabled):
def _rtc_products_view(resolution, enabled, cclasstrib_applicability=None):
"""Compact 'Produtos RTC' summary. None when RTC is off (RTC never shows up as a problem) or the Agent did not report a resolution."""
if not enabled or not isinstance(resolution, dict) or not resolution.get("total"):
if not enabled:
return None
if isinstance(cclasstrib_applicability, dict) and cclasstrib_applicability.get("state") == "NOT_APPLICABLE":
label = "cClassTrib: Aguardando confirmação do regime da empresa" if cclasstrib_applicability.get("basis") == "COMPANY_CRT_UNRESOLVED" else "cClassTrib: Não utilizado neste cliente"
return {"applicable": False, "label": label}
if not isinstance(resolution, dict) or not resolution.get("total"):
return None
total = int(resolution["total"])
valid = int(resolution.get("valid_existing", 0))
@@ -195,7 +200,7 @@ def _rtc_products_view(resolution, enabled):
other_reason_max = max((int(count or 0) for reason, count in reasons.items() if not str(reason).endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT")), default=0)
blocked = total - valid - ready
pending = total - valid
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
return {"applicable": True, "total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
"simple_orientation_count": simple_count, "simple_orientation_dominant": simple_count > 0 and simple_count > other_reason_max,
"headline": f"Reforma Tributária ativa — cClassTrib pendente em {pending} produto{'s' if pending != 1 else ''}" if pending else "Reforma Tributária ativa — todos os produtos com cClassTrib válido",
"catalog": (resolution.get("catalog_compatibility") or {}).get("compatibility")}
+35 -8
View File
@@ -168,12 +168,38 @@ def _dedupe(items):
# ------------------------------------------------------------------------------------------------- RTC
CCLASSTRIB_EFFECTIVE_FROM = {
"3": date(2026, 8, 3),
"1": date(2027, 1, 1),
"2": date(2027, 1, 1),
"4": date(2027, 1, 1),
}
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 cclasstrib_applicability(crt, rtc_enabled: bool, today: date | None = None) -> dict:
"""Company-level cClassTrib requiredness from live CRT and its effective date.
The ERP RTC switch scopes the feature; it does not make cClassTrib mandatory
for every company/product. Unknown regimes fail closed to NOT_APPLICABLE.
"""
today = today or date.today()
regime = _text(crt)
if not rtc_enabled:
return {"state": "NOT_APPLICABLE", "basis": "RTC_DISABLED_IN_ERP", "effective_from": None, "source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA"}
effective_from = CCLASSTRIB_EFFECTIVE_FROM.get(regime)
if effective_from is None:
return {"state": "NOT_APPLICABLE", "basis": "COMPANY_CRT_UNRESOLVED", "effective_from": None, "source": "C000004.CRT"}
if today < effective_from:
basis = "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE" if regime in SIMPLES_FAMILY else "CCLASSTRIB_NOT_YET_EFFECTIVE"
return {"state": "NOT_APPLICABLE", "basis": basis, "effective_from": effective_from.isoformat(), "source": "C000004.CRT + RTC effective-date rules"}
return {"state": "REQUIRED", "basis": "CCLASSTRIB_EFFECTIVE_FOR_CRT", "effective_from": effective_from.isoformat(), "source": "C000004.CRT + RTC effective-date rules"}
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", ()))
@@ -202,11 +228,13 @@ def validate_cclasstrib(value, catalog: dict | None, today: date | None = None)
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"}
def evaluate_rtc(product: dict, rtc: dict | None, today: date | None = None, crt=None) -> dict:
"""Validate cClassTrib only when RTC is on and the company's regime/date gate applies."""
app = rtc.get("cclasstrib_applicability") if isinstance(rtc, dict) else None
if not isinstance(app, dict):
app = cclasstrib_applicability(crt, bool(rtc and rtc.get("enabled")), today)
if app["state"] != "REQUIRED":
return {"applicability": app, "blockers": []}
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)
@@ -222,7 +250,7 @@ def evaluate_product(product: dict, company: dict | None, rtc: dict | None, toda
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)
rtc_result = evaluate_rtc(product, rtc, today, crt)
blockers = [{"type": kind, "category": blocker_category(kind), "reason": reason} for kind, reason in pis["blockers"] + rtc_result["blockers"]]
warnings = [{"type": kind, "reason": reason} for kind, reason in pis["warnings"]]
mandatory = []
@@ -236,8 +264,7 @@ def evaluate_product(product: dict, company: dict | None, rtc: dict | None, toda
def legal_effective_context(crt) -> dict:
"""INFORMATIONAL ONLY (never a blocker, never part of completeness). The operational rule stays ERP_RTC_ENABLED; this records what the official acts say about the
obligation to issue DF-e with IBS/CBS for the company regime, so a timing difference can be explained without inventing a rule.
"""Summarize the broad IBS/CBS document timeline; cClassTrib requiredness has its own CRT/date gate above.
Source: Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026) art. 1: NF-e (55) and NFC-e (65) from 2026-08-03; par. 1: Simples optantes from 2027-01-01."""
simples = _text(crt) in SIMPLES_FAMILY
return {"crt": _text(crt) or None, "informational_only": True,
+1 -1
View File
@@ -4,5 +4,5 @@ kept identical to this) and what an AgentRelease row's `version` column holds
once published — never a second, independent version number. Fiscal semantics
are versioned separately (ENGINE_VERSION/DECISION_VERSION in classification.py,
DECISION_ENGINE_VERSION etc.) and are NOT changed by a release bump."""
GTI_FISCAL_VERSION = "1.2.4"
GTI_FISCAL_VERSION = "1.2.5"
API_COMPAT_MAJOR_MINOR = "1.0" # an Agent 1.0.x/1.1.x talks to any Hub 1.0.x