274 lines
16 KiB
Python
274 lines
16 KiB
Python
"""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 canonical_cst(value) -> str | None:
|
|
"""The official 2-digit CST this text stands for, or None. Exactly two official digits, or ONE digit whose zero-padded form is an official code
|
|
("4" -> "04"). Nothing else is ever normalised ("049", "000", "2x" stay invalid)."""
|
|
code = _text(value)
|
|
if code in CST_DOMAIN:
|
|
return code
|
|
if len(code) == 1 and code.isdigit() and code.zfill(2) in CST_DOMAIN:
|
|
return code.zfill(2)
|
|
return None
|
|
|
|
|
|
def validate_cst(value) -> str:
|
|
"""EMPTY | VALID | VALID_AFTER_NORMALIZATION | INVALID."""
|
|
code = _text(value)
|
|
if not code:
|
|
return "EMPTY"
|
|
if code in CST_DOMAIN:
|
|
return "VALID"
|
|
return "VALID_AFTER_NORMALIZATION" if canonical_cst(code) else "INVALID"
|
|
|
|
|
|
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": ...}.
|
|
|
|
Decided ONLY from signals that are independent of the six CST/rate fields - an empty field is a data state, never proof of "not applicable":
|
|
* the company regime (CRT);
|
|
* the product's own declaration: PISCOFINS flag and INCIDENCIA_PISCOFINS;
|
|
* official rules (LC 123/2006 art. 13 IV/V and art. 18 par. 4-A I; Leis 9.718/98, 10.637/02, 10.833/03; SPED 4.3.x for the CST domain/semantics).
|
|
|
|
REQUIRED the product declares a special treatment (flag S, or incidence MONOFASICA / SUBSTITUICAO): the exit CST must be valid, whatever the fields hold.
|
|
NOT_APPLICABLE Simples family (CRT 1/2/4) with no special treatment declared: PIS/Cofins are collected inside the DAS (LC 123 art. 13), so the product carries no
|
|
obligation. The same verdict holds whether the fields are empty or typed.
|
|
OPTIONAL taxed regime (CRT 3 or other) AND the product explicitly declares "no special treatment" (flag N + incidence NENHUMA): the tax applies but the
|
|
company default covers it, so the product default is not mandatory; anything typed must still be valid.
|
|
NEEDS_REVIEW the declaration cannot be read: incidence text outside the SGV vocabulary, or a taxed regime with a blank flag/incidence.
|
|
"""
|
|
flag = _text(product.get("PISCOFINS")).upper()
|
|
raw_incidence = _text(product.get("INCIDENCIA_PISCOFINS"))
|
|
kind = incidence_kind(raw_incidence)
|
|
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 _text(crt) in SIMPLES_FAMILY:
|
|
return {"state": "NOT_APPLICABLE", "basis": "SIMPLES_DAS_NO_SPECIAL_TREATMENT"}
|
|
if flag == "N" and raw_incidence:
|
|
return {"state": "OPTIONAL", "basis": "TAXED_REGIME_COMPANY_DEFAULT_COVERS"}
|
|
return {"state": "NEEDS_REVIEW", "basis": "NO_DECLARATION_IN_TAXED_REGIME"}
|
|
|
|
|
|
def evaluate_pis_cofins(product: dict, crt) -> dict:
|
|
"""{"applicability": {...}, "blockers": [(type, reason)], "warnings": [(type, reason)]}. Entry and exit are separate; PIS and COFINS independent.
|
|
Blockers exist only where the field applies (REQUIRED / OPTIONAL). Non-blocking warnings report data-quality facts (non-canonical or ignored values)."""
|
|
app = pis_cofins_applicability(product, crt)
|
|
blockers: list[tuple[str, str]] = []
|
|
warnings: list[tuple[str, str]] = []
|
|
kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
|
|
flag = _text(product.get("PISCOFINS")).upper()
|
|
applies = app["state"] in ("REQUIRED", "OPTIONAL")
|
|
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)
|
|
for slot, value, state in (("ENTRY", entry, entry_state), ("EXIT", exit_, exit_state)):
|
|
if state == "VALID_AFTER_NORMALIZATION":
|
|
warnings.append((f"CST_{tax}_NON_CANONICAL_FORMAT", f"{slot}:{_text(value)}->{canonical_cst(value)}"))
|
|
elif state == "INVALID":
|
|
if applies:
|
|
blockers.append((f"{tax}_CST_INVALID", slot))
|
|
else:
|
|
warnings.append((f"CST_{tax}_INVALID_IGNORED", f"{slot}:{_text(value)}"))
|
|
if not applies:
|
|
continue
|
|
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"))
|
|
code = canonical_cst(exit_)
|
|
if code:
|
|
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 applies and flag == "N" and kind != "NONE":
|
|
blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_N_WITH_SPECIAL_INCIDENCE"))
|
|
if applies and 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), "warnings": _dedupe(warnings)}
|
|
|
|
|
|
def _dedupe(items):
|
|
seen, out = set(), []
|
|
for item in items:
|
|
if item not in seen:
|
|
seen.add(item)
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------- 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", ()))
|
|
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, 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)
|
|
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, 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 = []
|
|
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, "warnings": warnings,
|
|
"needs_review": pis["applicability"]["state"] == "NEEDS_REVIEW"}
|
|
|
|
|
|
def legal_effective_context(crt) -> dict:
|
|
"""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,
|
|
"ibs_cbs_in_documents": "MANDATORY_FROM_2027-01-01" if simples else ("MANDATORY_FROM_2026-08-03" if _text(crt) == "3" else "UNKNOWN_REGIME"),
|
|
"cclasstrib_orientation": "PENDING_OFFICIAL_NT" if simples else ("TABLE_AND_LC214_ANNEXES" if _text(crt) == "3" else "UNKNOWN_REGIME"),
|
|
"sources": ["Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026) art. 1 I, II and par. 1", "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT", "LC 214/2025 art. 348"]}
|