fix: gate cClassTrib applicability by CRT and date
This commit is contained in:
@@ -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")}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user