fix: RTC observed live with provenance, non-circular PIS/COFINS applicability, canonical CST normalization (1.1.8)
- RTC: Agent reads ALL PARAMETROS rows every sync (conflict => off), reports rtc_source/rtc_observed_at/rtc_param_rows; legal_effective_context is informational only - PIS/COFINS: applicability from CRT + PISCOFINS flag + incidence only (never from empty fields); Simples w/o special treatment NOT_APPLICABLE, taxed regime with explicit N OPTIONAL, unreadable declaration NEEDS_REVIEW - CST: one digit with an official zero-padded form is VALID_AFTER_NORMALIZATION (non-blocking warning), nothing is written to the ERP - tests: RTC toggle 0->1->0 across syncs (Agent + Hub stale closing), non-circularity, normalization, panel provenance Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,9 +8,10 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import date
|
||||
from collections import Counter
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, rtc_enabled
|
||||
from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, legal_effective_context, rtc_enabled
|
||||
|
||||
PRODUCT_TABLE = "C000025"
|
||||
RTC_CCLASSTRIB_TABLE = "RTC_CCLASSTRIB"
|
||||
@@ -44,12 +45,17 @@ def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]:
|
||||
cursor.execute("SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS rf WHERE rf.RDB$RELATION_NAME IN ('C000025','RTC_CCLASSTRIB','RTC_CST','PARAMETROS')")
|
||||
for table, column in cursor.fetchall():
|
||||
schema.setdefault(table, set()).add(column)
|
||||
extras: dict = {"param_value": None, "physical_fk": False}
|
||||
extras: dict = {"param_value": None, "param_rows": 0, "param_conflict": False, "physical_fk": False}
|
||||
if "USAR_REFORMA_TRIBUTARIA" in schema.get("PARAMETROS", ()):
|
||||
try:
|
||||
cursor.execute("SELECT FIRST 1 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS")
|
||||
row = cursor.fetchone()
|
||||
extras["param_value"] = None if row is None or row[0] is None else row[0]
|
||||
# ALL rows are read: the source of truth is what THIS installation's database says NOW (never a cached or Hub-side value).
|
||||
cursor.execute("SELECT FIRST 20 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS")
|
||||
values = [None if r[0] is None else str(r[0]).strip().upper() for r in cursor.fetchall()]
|
||||
extras["param_rows"] = len(values)
|
||||
if len(set(values)) > 1:
|
||||
extras["param_conflict"] = True # rows disagree: never guess "on" (RTC stays out of scope, and the conflict is reported)
|
||||
elif values:
|
||||
extras["param_value"] = values[0]
|
||||
except Exception: # noqa: BLE001 - unreadable parameter => RTC stays off (not enabled by the ERP)
|
||||
extras["param_value"] = None
|
||||
try:
|
||||
@@ -92,11 +98,26 @@ def assess_from_connection(conn, company: dict | None, *, today: date | None = N
|
||||
today = today or date.today()
|
||||
products = read_products(cursor, schema)
|
||||
findings = {}
|
||||
warn_by_type: Counter = Counter()
|
||||
warn_products: set = set()
|
||||
samples: list = []
|
||||
applicability: Counter = Counter()
|
||||
for p in products:
|
||||
found = evaluate_product(p, company, rtc, today)
|
||||
found["input_fingerprint"] = hashlib.sha256(json.dumps({k: (None if v is None else str(v).strip()) for k, v in p.items()} | {"rtc": enabled}, sort_keys=True).encode()).hexdigest()
|
||||
findings[str(p.get("CODIGO") or "")] = found
|
||||
code = str(p.get("CODIGO") or "")
|
||||
findings[code] = found
|
||||
applicability[found["applicability"]["PIS_COFINS"]] += 1
|
||||
for w in found.get("warnings", []):
|
||||
warn_by_type[w["type"]] += 1
|
||||
warn_products.add(code)
|
||||
if len(samples) < 10:
|
||||
samples.append({"product_code": code, **w})
|
||||
integrity = catalog_integrity(catalog) if catalog else None
|
||||
return {"context": {"rtc_enabled": enabled, "rtc_supported": caps["HAS_RTC_RELATIONSHIP"], "rtc_catalog_valid": caps["RTC_CATALOG_VALID"], "rtc_param": extras["param_value"],
|
||||
"catalog_integrity": integrity, "crt": (company or {}).get("crt"), "capabilities": caps, "products_evaluated": len(products)},
|
||||
return {"context": {"rtc_enabled": enabled, "erp_rtc_enabled": enabled, "rtc_supported": caps["HAS_RTC_RELATIONSHIP"], "rtc_catalog_valid": caps["RTC_CATALOG_VALID"], "rtc_param": extras["param_value"],
|
||||
"rtc_source": "SIMULATION" if rtc_override is not None else "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_param_rows": extras["param_rows"], "rtc_param_conflict": extras["param_conflict"],
|
||||
"rtc_observed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "legal_effective_context": legal_effective_context((company or {}).get("crt")),
|
||||
"catalog_integrity": integrity, "crt": (company or {}).get("crt"), "capabilities": caps, "products_evaluated": len(products),
|
||||
"pis_cofins_applicability": dict(applicability),
|
||||
"non_canonical_warnings": {"products": len(warn_products), "by_type": dict(sorted(warn_by_type.items())), "samples": samples}},
|
||||
"findings": findings}
|
||||
|
||||
@@ -16,8 +16,8 @@ CLASSES = [("000001", "000", date(2025, 5, 5), None), ("200003", "200", date(202
|
||||
|
||||
class FakeCursor:
|
||||
"""READ ONLY fake: answers only SELECTs and records every statement so tests can prove nothing else ran."""
|
||||
def __init__(self, param=0, schema=SCHEMA_ROWS, products=PRODUCTS, classes=CLASSES, cst=(("000",), ("200",))):
|
||||
self.param, self.schema, self.products, self.classes, self.cst, self.statements, self._rows = param, schema, products, classes, cst, [], []
|
||||
def __init__(self, param=0, params=None, schema=SCHEMA_ROWS, products=PRODUCTS, classes=CLASSES, cst=(("000",), ("200",))):
|
||||
self.param, self.params, self.schema, self.products, self.classes, self.cst, self.statements, self._rows = param, params, schema, products, classes, cst, [], []
|
||||
|
||||
def execute(self, sql, *args):
|
||||
self.statements.append(sql)
|
||||
@@ -26,7 +26,7 @@ class FakeCursor:
|
||||
if "FROM RDB$RELATION_FIELDS" in text:
|
||||
self._rows = list(self.schema)
|
||||
elif "FROM PARAMETROS" in text:
|
||||
self._rows = [(self.param,)]
|
||||
self._rows = [(v,) for v in (self.params if self.params is not None else [self.param])]
|
||||
elif "RDB$RELATION_CONSTRAINTS" in text:
|
||||
self._rows = [(1,)]
|
||||
elif "FROM RTC_CCLASSTRIB" in text:
|
||||
@@ -62,7 +62,7 @@ def test_rtc_off_in_the_erp_is_out_of_scope_and_never_blocks():
|
||||
out = uf.assess_from_connection(conn, {"crt": "1"}, today=date(2026, 9, 24))
|
||||
assert out["context"]["rtc_enabled"] is False and out["context"]["rtc_supported"] is True and out["context"]["rtc_catalog_valid"] is True
|
||||
assert not [b for f in out["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
|
||||
assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == [] and kinds(out["findings"]["3"]) == ["COFINS_CST_INVALID", "PIS_CST_INVALID"]
|
||||
assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == [] and kinds(out["findings"]["3"]) == [] # Simples + explicit "no special treatment": not applicable, typed "2" is only a warning
|
||||
assert all(s.strip().upper().startswith("SELECT") for s in conn.cur.statements) # read-only: SELECT statements only
|
||||
|
||||
|
||||
@@ -70,19 +70,19 @@ def test_rtc_on_in_the_erp_brings_cclasstrib_into_the_same_findings():
|
||||
out = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"}, today=date(2026, 9, 24))
|
||||
assert out["context"]["rtc_enabled"] is True
|
||||
assert kinds(out["findings"]["1"]) == ["CCLASSTRIB_MISSING"] and kinds(out["findings"]["2"]) == []
|
||||
assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING", "COFINS_CST_INVALID", "PIS_CST_INVALID"] # one queue, one verdict per product
|
||||
assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING"] # one queue, one verdict per product
|
||||
|
||||
|
||||
def test_simulation_flag_never_touches_the_erp_parameter():
|
||||
conn = FakeConn(param=0)
|
||||
on = uf.assess_from_connection(conn, {"crt": "1"}, rtc_override=True)
|
||||
assert on["context"]["rtc_enabled"] is True and kinds(on["findings"]["1"]) == ["CCLASSTRIB_MISSING"]
|
||||
assert on["context"]["rtc_param"] == 0 and not any("UPDATE" in s.upper() for s in conn.cur.statements)
|
||||
assert on["context"]["rtc_param"] == "0" and on["context"]["rtc_source"] == "SIMULATION" and not any("UPDATE" in s.upper() for s in conn.cur.statements)
|
||||
|
||||
|
||||
def test_old_schema_without_rtc_columns_reports_unsupported_and_still_evaluates_pis_cofins():
|
||||
old = [("C000025", c) for c in ("CODIGO", "COD_SAIDA_PIS", "PISCOFINS", "INCIDENCIA_PISCOFINS")]
|
||||
rows = [("1", None, "N", "NENHUMA"), ("2", "9", "N", "NENHUMA")]
|
||||
rows = [("1", None, "N", "NENHUMA"), ("2", "049", "N", "NENHUMA")]
|
||||
out = uf.assess_from_connection(FakeConn(schema=old, products=rows, classes=[], cst=()), {"crt": "3"})
|
||||
assert out["context"]["rtc_supported"] is False and out["context"]["rtc_enabled"] is False
|
||||
assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == ["PIS_CST_INVALID"]
|
||||
@@ -108,3 +108,43 @@ def test_input_fingerprint_changes_with_the_fiscal_values_and_the_rtc_switch():
|
||||
changed = [("1", None, None, "99", None, None, None, None, "N", "NENHUMA")] + PRODUCTS[1:]
|
||||
c = uf.assess_from_connection(FakeConn(param=0, products=changed), {"crt": "1"})["findings"]["1"]["input_fingerprint"]
|
||||
assert len({a, b, c}) == 3 and re.fullmatch(r"[0-9a-f]{64}", a)
|
||||
|
||||
|
||||
def test_rtc_state_is_read_from_this_database_now_with_provenance_and_timestamp():
|
||||
out = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"})
|
||||
ctx = out["context"]
|
||||
assert ctx["rtc_enabled"] is True and ctx["erp_rtc_enabled"] is True and ctx["rtc_source"] == "PARAMETROS.USAR_REFORMA_TRIBUTARIA" and ctx["rtc_param"] == "1" and ctx["rtc_param_rows"] == 1
|
||||
assert ctx["rtc_param_conflict"] is False and ctx["rtc_observed_at"].endswith("+00:00")
|
||||
assert ctx["legal_effective_context"]["informational_only"] is True and ctx["legal_effective_context"]["crt"] == "1"
|
||||
|
||||
|
||||
def test_toggle_between_syncs_is_followed_in_both_directions_without_any_cache():
|
||||
conn = FakeConn(param=0)
|
||||
first = uf.assess_from_connection(conn, {"crt": "1"})
|
||||
assert first["context"]["rtc_enabled"] is False and not [b for f in first["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
|
||||
conn.cur.param = 1 # the operator switches the reform on in the ERP between two syncs
|
||||
second = uf.assess_from_connection(conn, {"crt": "1"})
|
||||
assert second["context"]["rtc_enabled"] is True and kinds(second["findings"]["1"]) == ["CCLASSTRIB_MISSING"]
|
||||
assert second["findings"]["1"]["input_fingerprint"] != first["findings"]["1"]["input_fingerprint"] # a new blocker fingerprint: the old (RTC off) state is not reused
|
||||
conn.cur.param = 0 # and off again
|
||||
third = uf.assess_from_connection(conn, {"crt": "1"})
|
||||
assert third["context"]["rtc_enabled"] is False and not [b for f in third["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
|
||||
assert third["findings"]["1"]["input_fingerprint"] == first["findings"]["1"]["input_fingerprint"]
|
||||
|
||||
|
||||
def test_conflicting_or_repeated_parameter_rows_never_guess_on():
|
||||
same = uf.assess_from_connection(FakeConn(params=[1, 1]), {"crt": "1"})["context"]
|
||||
assert same["rtc_enabled"] is True and same["rtc_param_rows"] == 2 and same["rtc_param_conflict"] is False
|
||||
conflict = uf.assess_from_connection(FakeConn(params=[1, 0]), {"crt": "1"})
|
||||
assert conflict["context"]["rtc_enabled"] is False and conflict["context"]["rtc_param_conflict"] is True
|
||||
assert not [b for f in conflict["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
|
||||
assert uf.assess_from_connection(FakeConn(params=[]), {"crt": "1"})["context"]["rtc_enabled"] is False
|
||||
|
||||
|
||||
def test_non_canonical_cst_warnings_are_reported_without_blocking():
|
||||
rows = [("1", None, None, "4", None, None, "4", None, "N", "NENHUMA"), ("2", None, None, None, None, None, None, None, "N", "NENHUMA")]
|
||||
out = uf.assess_from_connection(FakeConn(products=rows), {"crt": "3"})
|
||||
assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == []
|
||||
w = out["context"]["non_canonical_warnings"]
|
||||
assert w["products"] == 1 and w["by_type"] == {"CST_COFINS_NON_CANONICAL_FORMAT": 1, "CST_PIS_NON_CANONICAL_FORMAT": 1} and w["samples"][0]["product_code"] == "1"
|
||||
assert out["context"]["pis_cofins_applicability"] == {"OPTIONAL": 2}
|
||||
|
||||
@@ -185,3 +185,26 @@ def test_partial_reprocess_does_not_claim_a_full_fiscal_evaluation(env_setup, mo
|
||||
hub, state = env_setup
|
||||
result, _ = run_job("REPROCESS_PRODUCTS", [product("1")], state, monkeypatch)
|
||||
assert result["fiscal_context"] is None and result["operational"] is None
|
||||
|
||||
|
||||
def test_rtc_toggle_between_two_syncs_is_followed_by_the_next_job(env_setup, monkeypatch):
|
||||
"""The Agent asks the database EVERY sync: 0 -> 1 makes the next job report RTC blockers, 1 -> 0 makes the following job report none (Hub then closes the old ones)."""
|
||||
from gti_fiscal_agent import unified_fiscal
|
||||
switch = {"on": False}
|
||||
|
||||
def assess(conn, company, **kw):
|
||||
found = _findings(**{"1": [("CCLASSTRIB_MISSING", "RTC")]}) if switch["on"] else {}
|
||||
return {"context": {"rtc_enabled": switch["on"], "rtc_supported": True, "rtc_catalog_valid": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_observed_at": "2026-09-24T20:00:00+00:00"}, "findings": found}
|
||||
monkeypatch.setattr(unified_fiscal, "assess_from_connection", assess)
|
||||
hub, state = env_setup
|
||||
products = [product("1"), product("2")]
|
||||
off1, _ = run_job("INITIAL_SYNC", products, state, monkeypatch)
|
||||
switch["on"] = True
|
||||
on, _ = run_job("SYNC_INCREMENTAL", products, state, monkeypatch) # nothing changed in C000025: the delta cache must not hide the RTC change
|
||||
switch["on"] = False
|
||||
off2, _ = run_job("SYNC_INCREMENTAL", products, state, monkeypatch)
|
||||
assert off1["blocked_products"] == [] and off1["fiscal_context"]["rtc_enabled"] is False
|
||||
assert [b["field"] for b in on["blocked_products"]] == ["RTC"] and on["fiscal_context"]["rtc_enabled"] is True and on["operational"]["BLOCKED_FISCAL_DATA"] == 1
|
||||
assert on["fiscal_context"]["rtc_observed_at"] and on["fiscal_context"]["rtc_source"] == "PARAMETROS.USAR_REFORMA_TRIBUTARIA"
|
||||
assert off2["blocked_products"] == [] and off2["fiscal_context"]["rtc_enabled"] is False and off2["fiscal_context"]["evaluated_all"] is True
|
||||
assert off2["operational"]["COMPLETE_PRODUCTS"] == 2
|
||||
|
||||
@@ -162,9 +162,18 @@ def priority_label(value):
|
||||
|
||||
|
||||
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)."""
|
||||
"""Discreet Reforma Tributaria status from the last completed job's `fiscal_context`, exactly as the Agent OBSERVED it in that installation's own database.
|
||||
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
|
||||
observed = None
|
||||
if context.get("rtc_observed_at"):
|
||||
try:
|
||||
observed = format_ui_datetime(datetime.fromisoformat(str(context["rtc_observed_at"]).replace("Z", "+00:00")))
|
||||
except ValueError:
|
||||
observed = None
|
||||
source = context.get("rtc_source") or "PARAMETROS.USAR_REFORMA_TRIBUTARIA"
|
||||
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"))}
|
||||
"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 "")}
|
||||
|
||||
@@ -54,17 +54,25 @@ def incidence_kind(value) -> str:
|
||||
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"
|
||||
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
|
||||
if code in CST_DOMAIN:
|
||||
return "VALID"
|
||||
return "VALID_AFTER_NORMALIZATION" if canonical_cst(code) else "INVALID"
|
||||
|
||||
|
||||
def validate_rate(value) -> str:
|
||||
@@ -82,56 +90,71 @@ def validate_rate(value) -> str:
|
||||
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.
|
||||
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()
|
||||
kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
|
||||
has_values = any(_text(product.get(c)) for c in PIS_COFINS_FIELDS)
|
||||
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 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"}
|
||||
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)]}. Entry and exit are separate; a missing OPTIONAL value never blocks."""
|
||||
"""{"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]] = []
|
||||
if app["state"] in ("NOT_APPLICABLE", "NEEDS_REVIEW"):
|
||||
return {"applicability": app, "blockers": blockers}
|
||||
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)
|
||||
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 "")))
|
||||
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"))
|
||||
if exit_state == "VALID":
|
||||
code = _text(exit_)
|
||||
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 flag == "N" and kind != "NONE":
|
||||
if applies and 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):
|
||||
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)}
|
||||
return {"applicability": app, "blockers": _dedupe(blockers), "warnings": _dedupe(warnings)}
|
||||
|
||||
|
||||
def _dedupe(items):
|
||||
@@ -201,11 +224,21 @@ def evaluate_product(product: dict, company: dict | None, rtc: dict | None, toda
|
||||
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"]]
|
||||
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,
|
||||
"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:
|
||||
"""INFORMATIONAL ONLY (never a blocker, never part of completeness). The operational rule stays ERP_RTC_ENABLED; this records what the legislation
|
||||
says about IBS/CBS in documents for the company regime so a difference in timing can be explained without inventing a rule."""
|
||||
simples = _text(crt) in SIMPLES_FAMILY
|
||||
return {"crt": _text(crt) or None, "informational_only": True,
|
||||
"ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING" if simples else ("MANDATORY_FROM_2026-01-01" if _text(crt) == "3" else "UNKNOWN_REGIME"),
|
||||
"sources": ["NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", "LC 214/2025 art. 348"]}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<form method="post" action="/agents/{{ detail.agent_id }}/sync-now" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="primary-action" {% if not ops.enabled %}disabled title="Processamento pausado: retome para sincronizar"{% else %}title="Verifica apenas as alterações desde a última sincronização"{% endif %}>Sincronizar agora</button></form>
|
||||
<a class="botao secondary" href="/agents/{{ detail.agent_id }}/reprocess-confirmation" title="Recalcula todos os produtos (pede confirmação)">Reprocessar tudo</a>
|
||||
{% if ops.enabled %}<form method="post" action="/agents/{{ detail.agent_id }}/pause" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="caution" title="Mantém o heartbeat, mas não executa jobs">Pausar processamento</button></form>{% else %}<form method="post" action="/agents/{{ detail.agent_id }}/resume" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="primary-action" title="Volta a executar jobs">Retomar processamento</button></form>{% endif %}
|
||||
</div></div><dl class="facts"><dt>Processamento</dt><dd><span class="badge {{ '' if ops.enabled else 'critical' }}">{{ "Ativo" if ops.enabled else "Pausado" }}</span> · sincronização automática a cada {{ ops.interval_minutes }} min</dd><dt>Última sincronização</dt><dd>{{ ops.last_sync or "ainda não sincronizado" }}{% if ops.last_changed is not none %} · {{ ops.last_changed }} produto(s) alterado(s){% endif %}</dd><dt>Sincronização agendada</dt><dd>{{ ops.next_sync or ("aguardando o primeiro sync" if ops.enabled else "pausado") }}</dd>{% if ops.rtc %}<dt>Reforma Tributária</dt><dd><span class="badge {{ "" if ops.rtc.enabled else "muted" }}">{{ ops.rtc.label }}</span></dd>{% endif %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
|
||||
</div></div><dl class="facts"><dt>Processamento</dt><dd><span class="badge {{ '' if ops.enabled else 'critical' }}">{{ "Ativo" if ops.enabled else "Pausado" }}</span> · sincronização automática a cada {{ ops.interval_minutes }} min</dd><dt>Última sincronização</dt><dd>{{ ops.last_sync or "ainda não sincronizado" }}{% if ops.last_changed is not none %} · {{ ops.last_changed }} produto(s) alterado(s){% endif %}</dd><dt>Sincronização agendada</dt><dd>{{ ops.next_sync or ("aguardando o primeiro sync" if ops.enabled else "pausado") }}</dd>{% if ops.rtc %}<dt>Reforma Tributária</dt><dd><span class="badge {{ "" if ops.rtc.enabled else "muted" }}" title="{{ ops.rtc.title }}">{{ ops.rtc.label }}</span></dd>{% endif %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
|
||||
<nav class="tabs" aria-label="Seções da instalação">{% for name,label in [("overview","Resumo"),("fiscal","Fiscal"),("blockers","Blockers"),("jobs","Jobs"),("events","Eventos")] %}<a class="{% if tab==name %}active{% endif %}" href="?tab={{ name }}">{{ label }}</a>{% endfor %}</nav>
|
||||
<section class="split"><article class="panel"><div class="panel-title"><h2>Contexto da empresa</h2></div><dl class="facts"><dt>Cliente</dt><dd>{{ detail.client or "Não informado" }}</dd><dt>Nome de exibição</dt><dd>{{ detail.display_name or "Não informado" }}</dd><dt>Hostname</dt><dd>{{ detail.hostname or "Não informado" }}</dd><dt>Empresa</dt><dd>{{ detail.company_name or detail.company_code or "Não informado" }}</dd><dt>UF</dt><dd>{{ detail.company_uf or "Não informado" }}</dd><dt>CRT</dt><dd>{{ detail.company_crt or "Não informado" }}</dd><dt>Versão do agent</dt><dd>{{ detail.agent_version_display }}</dd><dt>Último heartbeat</dt><dd title="{{ detail.last_seen_at }}">{{ detail.last_seen_display }} · {{ detail.last_seen_age }}</dd><dt>Firebird</dt><dd>{{ detail.firebird_version or "Não informado" }} · ODS {{ detail.firebird_ods or "Não informado" }}</dd><dt>IDs técnicos</dt><dd>Agent {{ detail.technical_ids.agent_id }} · instalação {{ detail.technical_ids.installation_db_id }}</dd></dl></article>
|
||||
<article class="panel"><div class="panel-title"><h2>Eventos recentes</h2><a href="#eventos">Ver eventos</a></div>{% for event in events[:8] %}<div class="event-row"><span class="event-time">{{ event.occurred_display }}</span><span><b>{{ event.event_type }}</b><small>{{ event.message }}{% if event.job_id %} · <a href="/jobs?job_id={{ event.job_id }}">Job {{ event.job_id }}</a>{% endif %}{% if event.product_code %} · <a href="/blockers?agent_id={{ detail.agent_id }}&product_code={{ event.product_code|urlencode }}">Produto {{ event.product_code }}</a>{% endif %}</small></span><em class="severity {{ event.severity|lower }}">Severidade: {{ event.severity }}</em></div>{% else %}<p class="empty-state">Nenhum evento.</p>{% endfor %}</article></section>
|
||||
|
||||
@@ -575,3 +575,15 @@ def test_pis_cofins_and_rtc_blockers_appear_in_the_same_queue_and_dashboard_brea
|
||||
assert "fila RTC" not in queue.lower()
|
||||
filtered = client.get("/blockers?scope=active&status=OPEN&field=RTC").text
|
||||
assert "cClassTrib ausente" in filtered and "CST PIS inválido" not in filtered
|
||||
|
||||
|
||||
def test_rtc_status_shows_where_and_when_the_agent_observed_it():
|
||||
client, agent_id, _ = _panel_client()
|
||||
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_observed_at": "2026-09-24T20:00:00+00:00"})
|
||||
text = client.get(f"/agents/{agent_id}").text
|
||||
assert 'title="Lido no banco do cliente (PARAMETROS.USAR_REFORMA_TRIBUTARIA) em ' in text and ">Ativa<" in text
|
||||
_complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_param_conflict": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA"})
|
||||
conflict = client.get(f"/agents/{agent_id}").text
|
||||
assert "linhas de PARAMETROS divergentes" in conflict and ">Desativada<" in conflict
|
||||
for legacy in ("Escopo anterior", "Escopo ampliado", "Legacy coverage", "Expanded coverage"):
|
||||
assert legacy not in conflict
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
from datetime import date
|
||||
|
||||
from central.app.operational_state import classify_product_state, summarize_operational_states
|
||||
from central.app.unified_fiscal_coverage import (blocker_category, evaluate_product, incidence_kind, rtc_enabled, validate_cclasstrib)
|
||||
from central.app.unified_fiscal_coverage import (blocker_category, canonical_cst, evaluate_product, incidence_kind, legal_effective_context, pis_cofins_applicability, rtc_enabled,
|
||||
validate_cclasstrib, validate_cst)
|
||||
|
||||
CATALOG = {"classes": {"000001": "000", "200003": "200", "410004": "410", "999999": None, "555555": "555"}, "cst": {"000", "200", "410"}, "duplicates": 0}
|
||||
ON = {"enabled": True, "catalog": CATALOG, "catalog_valid": True}
|
||||
OFF = {"enabled": False, "catalog": CATALOG, "catalog_valid": True}
|
||||
SIMPLES, NORMAL = {"crt": "1"}, {"crt": "3"}
|
||||
EMPTY = {"CODIGO": "1", "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA"}
|
||||
FIELDS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS")
|
||||
|
||||
|
||||
def kinds(result):
|
||||
return {b["type"] for b in result["blockers"]}
|
||||
|
||||
|
||||
def warns(result):
|
||||
return {w["type"] for w in result["warnings"]}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- RTC on / off
|
||||
def test_rtc_off_ignores_cclasstrib_completely():
|
||||
for value in (None, "", "12x", "999999", "000001"):
|
||||
@@ -48,14 +54,85 @@ def test_rtc_enabled_needs_schema_and_erp_switch():
|
||||
assert rtc_enabled(caps) and not rtc_enabled({**caps, "RTC_ENABLED_BY_PARAM": False}) and not rtc_enabled({**caps, "HAS_RTC_RELATIONSHIP": False}) and not rtc_enabled({})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- PIS / COFINS applicability
|
||||
def test_empty_product_is_not_applicable_in_every_regime_and_never_blocks():
|
||||
for company, basis in ((SIMPLES, "SIMPLES_DAS"), ({"crt": "2"}, "SIMPLES_DAS"), ({"crt": "4"}, "SIMPLES_DAS"), (NORMAL, "COMPANY_DEFAULT"), ({"crt": None}, "COMPANY_DEFAULT"), (None, "COMPANY_DEFAULT")):
|
||||
r = evaluate_product(EMPTY, company, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["basis"]["PIS_COFINS"] == basis and r["blockers"] == []
|
||||
assert evaluate_product({"CODIGO": "1"}, SIMPLES, OFF)["blockers"] == [] # columns absent in an old schema: same answer
|
||||
def test_legal_context_is_informational_and_never_creates_a_blocker():
|
||||
simples, normal = legal_effective_context("1"), legal_effective_context("3")
|
||||
assert simples["informational_only"] and "2027" in simples["ibs_cbs_in_documents"] and normal["ibs_cbs_in_documents"] == "MANDATORY_FROM_2026-01-01"
|
||||
assert legal_effective_context(None)["ibs_cbs_in_documents"] == "UNKNOWN_REGIME"
|
||||
# the operational rule is the ERP switch only: a Simples company with RTC on is validated, one with RTC off is not, whatever the legal timing says
|
||||
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON)) == {"CCLASSTRIB_MISSING"} and kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, OFF)) == set()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- PIS / COFINS applicability is NOT circular
|
||||
def test_applicability_never_depends_on_whether_the_six_fields_are_empty():
|
||||
typed = {f: "49" if "COD" in f else "0" for f in FIELDS}
|
||||
for crt in ("1", "2", "4", "3", None):
|
||||
for base in (EMPTY, {"CODIGO": "1", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}):
|
||||
empty_state = pis_cofins_applicability(base, crt)
|
||||
filled_state = pis_cofins_applicability({**base, **typed}, crt)
|
||||
assert empty_state == filled_state, (crt, base) # same verdict with the fields empty or typed: emptiness proves nothing
|
||||
|
||||
|
||||
def test_empty_fields_with_applicable_context_is_a_missing_blocker():
|
||||
for crt in ("1", "3"):
|
||||
r = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"crt": crt}, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "REQUIRED" and kinds(r) == {"PIS_CST_MISSING", "COFINS_CST_MISSING"}
|
||||
|
||||
|
||||
def test_empty_fields_with_provably_not_applicable_context_is_not_applicable():
|
||||
for crt in ("1", "2", "4"):
|
||||
r = evaluate_product(EMPTY, {"crt": crt}, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["basis"]["PIS_COFINS"] == "SIMPLES_DAS_NO_SPECIAL_TREATMENT" and r["blockers"] == [] and not r["needs_review"]
|
||||
assert evaluate_product({"CODIGO": "1"}, SIMPLES, OFF)["blockers"] == [] # columns absent in an old schema, Simples: nothing to require
|
||||
|
||||
|
||||
def test_empty_fields_with_insufficient_context_need_review_and_never_default_to_complete():
|
||||
for company in (NORMAL, {"crt": None}, None):
|
||||
r = evaluate_product({"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}, company, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == []
|
||||
state = classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=r)["state"]
|
||||
assert state == "NEEDS_REVIEW" # counted as UNCLASSIFIED, never inflating coverage
|
||||
|
||||
|
||||
def test_taxed_regime_with_explicit_no_special_treatment_is_optional_and_typed_values_must_be_valid():
|
||||
r = evaluate_product(EMPTY, NORMAL, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "OPTIONAL" and r["blockers"] == []
|
||||
assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2x"}, NORMAL, OFF)) == {"PIS_CST_INVALID"}
|
||||
|
||||
|
||||
def test_unknown_incidence_text_needs_review_in_every_regime():
|
||||
for crt in ("1", "3"):
|
||||
r = evaluate_product({**EMPTY, "INCIDENCIA_PISCOFINS": "OUTRA COISA"}, {"crt": crt}, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == []
|
||||
|
||||
|
||||
def test_simples_typed_garbage_is_ignored_as_a_warning_not_a_blocker():
|
||||
row = {**EMPTY, "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2", "COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "2"}
|
||||
r = evaluate_product(row, SIMPLES, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["blockers"] == []
|
||||
assert warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"} # "2" is one digit of official "02": normalisable
|
||||
bad = evaluate_product({**EMPTY, "COD_ENTRADA_COFINS": "000", "COD_SAIDA_COFINS": "060"}, SIMPLES, OFF)
|
||||
assert bad["blockers"] == [] and warns(bad) == {"CST_COFINS_INVALID_IGNORED"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- CST normalisation
|
||||
def test_canonical_cst_accepts_only_one_digit_with_an_official_zero_padded_form():
|
||||
assert canonical_cst("4") == "04" and canonical_cst("04") == "04" and canonical_cst(" 4 ") == "04" and canonical_cst("9") == "09" and canonical_cst("1") == "01"
|
||||
for text in ("0", "049", "000", "060", "2x", "x", "", None, "10", "12", "00"):
|
||||
assert canonical_cst(text) is None, text
|
||||
assert validate_cst("4") == "VALID_AFTER_NORMALIZATION" and validate_cst("04") == "VALID" and validate_cst("049") == "INVALID" and validate_cst("") == "EMPTY"
|
||||
|
||||
|
||||
def test_unpadded_cst_is_semantically_valid_warns_and_is_used_canonically_for_the_rules():
|
||||
row = {**EMPTY, "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
|
||||
r = evaluate_product(row, NORMAL, OFF)
|
||||
assert r["blockers"] == [] and warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"}
|
||||
mono = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
|
||||
assert kinds(evaluate_product(mono, NORMAL, OFF)) == set() # "4" -> "04" is compatible with MONOFASICA
|
||||
assert kinds(evaluate_product({**mono, "COD_SAIDA_PIS": "1", "ALIQ_PIS": "1,65"}, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # "1" -> "01" is not
|
||||
assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "049"}, NORMAL, OFF)) == {"PIS_CST_INVALID"} # ambiguous: still a blocker
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- PIS / COFINS rules
|
||||
def test_declared_monophasic_product_requires_a_valid_exit_cst():
|
||||
good = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}
|
||||
r = evaluate_product(good, SIMPLES, OFF)
|
||||
@@ -69,13 +146,6 @@ def test_st_incidence_with_taxable_cst_is_a_mismatch_and_pis_cofins_are_independ
|
||||
assert kinds(evaluate_product(row, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # only PIS is wrong; COFINS 05 is right
|
||||
|
||||
|
||||
def test_value_2_is_invalid_format_not_a_test_product_and_not_silently_ignored():
|
||||
row = {**EMPTY, "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2", "COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "2"}
|
||||
r = evaluate_product(row, SIMPLES, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "OPTIONAL" and kinds(r) == {"PIS_CST_INVALID", "COFINS_CST_INVALID"}
|
||||
assert "PIS_RATE_INVALID" not in kinds(r) # rate 2 is a valid number; only the CST codes are malformed
|
||||
|
||||
|
||||
def test_rate_rules_follow_the_cst():
|
||||
base = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "NENHUMA"}
|
||||
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "01", "COD_SAIDA_COFINS": "01"}, NORMAL, OFF)) == {"PIS_RATE_MISSING", "COFINS_RATE_MISSING"}
|
||||
@@ -85,17 +155,15 @@ def test_rate_rules_follow_the_cst():
|
||||
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "ALIQ_PIS": "150", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == {"PIS_RATE_INVALID"}
|
||||
|
||||
|
||||
def test_inconsistent_flags_and_unknown_incidence():
|
||||
def test_inconsistent_flags():
|
||||
assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}, SIMPLES, OFF))
|
||||
assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "S"}, SIMPLES, OFF))
|
||||
unknown = evaluate_product({**EMPTY, "INCIDENCIA_PISCOFINS": "OUTRA COISA"}, SIMPLES, OFF)
|
||||
assert unknown["needs_review"] and unknown["blockers"] == [] and unknown["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW"
|
||||
|
||||
|
||||
def test_incidence_text_is_encoding_and_accent_tolerant():
|
||||
for text in ("MONOFÁSICA", "MONOFASICA", "MONOF�SICA", "monofásica"):
|
||||
assert incidence_kind(text) == "MONOFASICA"
|
||||
assert incidence_kind("SUBST.TRIBUTÁRIA") == "SUBSTITUICAO" and incidence_kind(None) == "NONE" and incidence_kind(" nenhuma ") == "NONE"
|
||||
assert incidence_kind("SUBST.TRIBUTÁRIA") == "SUBSTITUICAO" and incidence_kind(None) == "NONE" and incidence_kind(" nenhuma ") == "NONE" and incidence_kind("???") == "UNKNOWN"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- ONE definition of complete, ONE queue
|
||||
@@ -105,7 +173,8 @@ def test_blocker_categories_feed_the_single_dashboard_breakdown():
|
||||
|
||||
|
||||
def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products():
|
||||
products = [{**EMPTY, "CODIGO": "1"}, {**EMPTY, "CODIGO": "2", "COD_SAIDA_PIS": "2"}, {**EMPTY, "CODIGO": "3", "CCLASSTRIB": "200003"}, {**EMPTY, "CODIGO": "4", "INCIDENCIA_PISCOFINS": "???"}]
|
||||
products = [{**EMPTY, "CODIGO": "1"}, {**EMPTY, "CODIGO": "2", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {**EMPTY, "CODIGO": "3", "CCLASSTRIB": "200003"},
|
||||
{**EMPTY, "CODIGO": "4", "INCIDENCIA_PISCOFINS": "???"}]
|
||||
|
||||
def run(rtc):
|
||||
states = [classify_product_state(p, {"status": "EXACT_NCM"}, fiscal=evaluate_product(p, SIMPLES, rtc)) for p in products]
|
||||
@@ -118,14 +187,7 @@ def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products():
|
||||
|
||||
|
||||
def test_fiscal_blockers_never_hide_or_override_a_decision_blocker():
|
||||
fiscal = evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2"}, SIMPLES, OFF)
|
||||
fiscal = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, SIMPLES, OFF)
|
||||
state = classify_product_state({"CODIGO": "1"}, {"status": "INVALID_NCM"}, fiscal=fiscal)
|
||||
assert state["state"] == "BLOCKED_MISSING_EVIDENCE" # the NCM blocker stays the primary state
|
||||
assert classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=None)["state"] == "COMPLETE"
|
||||
|
||||
|
||||
def test_unpadded_single_digit_cst_is_flagged_with_the_official_code_as_a_hint_only():
|
||||
row = {**EMPTY, "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
|
||||
r = evaluate_product(row, NORMAL, OFF)
|
||||
assert kinds(r) == {"PIS_CST_INVALID", "COFINS_CST_INVALID"} and any("UNPADDED_USE_04" in b["reason"] for b in r["blockers"])
|
||||
assert evaluate_product({**EMPTY, "COD_SAIDA_PIS": "049"}, NORMAL, OFF)["blockers"][0]["reason"] == "EXIT" # 3 digits: ambiguous, no hint
|
||||
|
||||
@@ -236,3 +236,31 @@ def test_partial_reprocess_never_closes_fiscal_blockers_of_other_products():
|
||||
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": {"evaluated_all": False}})
|
||||
with Session(engine) as s:
|
||||
assert len(s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()) == 1
|
||||
|
||||
|
||||
def test_rtc_on_then_off_and_pis_fix_leave_no_stale_open_blockers_and_never_duplicate():
|
||||
from central.app.models import BlockedProduct
|
||||
client, engine = _client()
|
||||
hb(client)
|
||||
rtc_missing = _fiscal_blocker("20", "RTC", "CCLASSTRIB_MISSING", "r1")
|
||||
pis_invalid = _fiscal_blocker("21", "PIS", "PIS_CST_INVALID", "p1")
|
||||
op = {**OPERATIONAL, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0}
|
||||
finish(client, engine, {"operational": {**op, "COMPLETE": 519, "BLOCKED_FISCAL_DATA": 2, "SAFE_BLOCKED": 2}, "blocked_products": [rtc_missing, pis_invalid, dict(rtc_missing)], "resolved_products": [],
|
||||
"fiscal_context": {"rtc_enabled": True, "evaluated_all": True}})
|
||||
with Session(engine) as s:
|
||||
assert len(s.scalars(select(BlockedProduct)).all()) == 2 # the repeated blocker is one row
|
||||
age_last_sync(engine, 4000)
|
||||
hb(client)
|
||||
# RTC switched off in the ERP AND the PIS value was normalised/fixed: the next full pass reports nothing
|
||||
finish(client, engine, {"operational": {**op, "COMPLETE": 521}, "blocked_products": [], "resolved_products": [], "fiscal_context": {"rtc_enabled": False, "evaluated_all": True}})
|
||||
with Session(engine) as s:
|
||||
rows = s.scalars(select(BlockedProduct)).all()
|
||||
assert [r.resolved_at is not None for r in rows] == [True, True] and {r.resolution_source for r in rows} == {"AUTO_REEVALUATION"}
|
||||
assert s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all() == []
|
||||
age_last_sync(engine, 4000)
|
||||
hb(client)
|
||||
# RTC comes back on: the SAME blocker (same fingerprint) reopens instead of being duplicated
|
||||
finish(client, engine, {"operational": {**op, "COMPLETE": 520, "BLOCKED_FISCAL_DATA": 1, "SAFE_BLOCKED": 1}, "blocked_products": [rtc_missing], "resolved_products": [], "fiscal_context": {"rtc_enabled": True, "evaluated_all": True}})
|
||||
with Session(engine) as s:
|
||||
rows = s.scalars(select(BlockedProduct)).all()
|
||||
assert len(rows) == 2 and [(r.product_code, r.resolved_at is None) for r in rows] == [("20", True), ("21", False)]
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
# PIS/COFINS — aplicabilidade no cadastro de produto (SGV)
|
||||
|
||||
Implementação: `central/app/unified_fiscal_coverage.py` (`pis_cofins_applicability`, `evaluate_pis_cofins`). Somente leitura; nenhum valor é preenchido.
|
||||
`ACCESSED_AT` de todas as fontes: **2026-09-24** (hashes em `docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json` e `docs/sources/SOURCES_MANIFEST_V3.json`).
|
||||
Implementação: `central/app/unified_fiscal_coverage.py` (`pis_cofins_applicability`, `evaluate_pis_cofins`, `canonical_cst`). Somente leitura; nenhum valor é preenchido nem normalizado no ERP.
|
||||
`ACCESSED_AT` das fontes: **2026-09-24** (hashes em `docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json` e `SOURCES_MANIFEST_V3.json`).
|
||||
|
||||
## 1. O que o campo do produto é (e o que não é)
|
||||
|
||||
Campos: `COD_ENTRADA_PIS`, `COD_SAIDA_PIS`, `ALIQ_PIS`, `COD_ENTRADA_COFINS`, `COD_SAIDA_COFINS`, `ALIQ_COFINS`, `PISCOFINS` (N/S), `INCIDENCIA_PISCOFINS` (NENHUMA / MONOFÁSICA / SUBST.TRIBUTÁRIA).
|
||||
|
||||
- São *defaults de cadastro* (tela Produtos > Config. Impostos > CST PIS/COFINS; binding provado por DFM em `docs/SGV_PRODUCT_FISCAL_UI_MAP_V1.json`). Os campos CST usam um seletor com a lista oficial "NN - descrição" (2 dígitos).
|
||||
- O SGV copia esses defaults para os itens de documentos de entrada (SQL de inserção do item no executável: `COD_SAIDA_PIS, COD_ENTRADA_PIS, COD_SAIDA_COFINS, COD_ENTRADA_COFINS, ... cst_ipi`).
|
||||
- Não são a tributação efetiva do documento. **DOCUMENT_PIS_COFINS_SOURCE**: em `NFCE_ITENS.CST_PIS_COFINS` o CST **não vem do cadastro do produto**. Evidência (TESTESOFICIAIS): os 3.336 itens com CST `01` são todos de dezembro/2022 (dia 01 a 31, sequência contínua de notas) e independentes do produto (produtos com `PISCOFINS=N`, campos vazios); antes (jun–nov/2022) e depois (2023–2026) os itens saem com CST em branco; o produto com CST `04`/MONOFÁSICA cadastrado emitiu em branco. Nas 4 bases de clientes (~570 mil itens) 100% saem em branco, mesmo para produtos com CST `04` no cadastro (RINCAO). O código do emissor de NFC-e não está disponível localmente (o executável SGV 5.0.6/5.1.0.4 não contém `CST_PIS_COFINS`), então a regra exata do documento **não é comprovável** aqui; o que é comprovado é que ela não lê o cadastro do produto.
|
||||
- São *defaults de cadastro* (tela Produtos > Config. Impostos > CST PIS/COFINS; binding por DFM em `docs/SGV_PRODUCT_FISCAL_UI_MAP_V1.json`). Os campos CST usam um seletor com a lista oficial "NN - descrição" (2 dígitos).
|
||||
- O SGV copia esses defaults para os itens de documentos de entrada (SQL de inserção do item no executável).
|
||||
- Não são a tributação efetiva do documento. **DOCUMENT_PIS_COFINS_SOURCE**: em `NFCE_ITENS.CST_PIS_COFINS` o CST **não vem do cadastro do produto**. TESTESOFICIAIS: os 3.336 itens com CST `01` são todos de dezembro/2022 (dias 01–31, notas contíguas), independentes do produto (`PISCOFINS=N`, campos vazios); antes e depois os itens saem em branco; o produto com CST `04` cadastrado emitiu em branco. Nas 4 bases de clientes (~570 mil itens) 100% saem em branco. O emissor de NFC-e não existe localmente (o executável SGV não contém `CST_PIS_COFINS`), então a regra exata do documento **não é comprovável** aqui.
|
||||
|
||||
## 2. Modelo de aplicabilidade (por produto)
|
||||
## 2. Modelo de aplicabilidade — não circular
|
||||
A aplicabilidade usa **somente sinais independentes dos seis campos**: regime da empresa (CRT), flag `PISCOFINS`, `INCIDENCIA_PISCOFINS` e as regras oficiais abaixo. **Campo vazio é estado de dado, nunca prova de não aplicabilidade**: o veredito é idêntico com os campos vazios ou preenchidos (teste `test_applicability_never_depends_on_whether_the_six_fields_are_empty`).
|
||||
|
||||
| Estado | Condição (dados do próprio produto) | Efeito |
|
||||
| Estado | Condição | Efeito |
|
||||
|---|---|---|
|
||||
| `NOT_APPLICABLE` | flag N/vazio, incidência NENHUMA/vazia, os 6 campos vazios | não entra na completude; sem blocker |
|
||||
| `REQUIRED` | flag `S`, ou incidência MONOFÁSICA / SUBST.TRIBUTÁRIA | CST de **saída** válido obrigatório; alíquota conforme CST |
|
||||
| `OPTIONAL` | sem alegação especial, mas algum CST/alíquota digitado | nada é obrigatório; o que existir tem de ser válido |
|
||||
| `NEEDS_REVIEW` | incidência fora do vocabulário do SGV | único caso não decidível → "Não classificado" |
|
||||
| `REQUIRED` | flag `S`, ou incidência MONOFÁSICA / SUBST.TRIBUTÁRIA (declaração própria do produto) | CST de **saída** válido obrigatório (vazio ⇒ `*_CST_MISSING`); alíquota conforme o CST |
|
||||
| `NOT_APPLICABLE` | CRT 1/2/4 (Simples) **sem** tratamento especial declarado | não entra na completude; valor digitado vira só *warning* |
|
||||
| `OPTIONAL` | regime tributado (CRT 3 ou outro) **e** declaração explícita "sem tratamento especial" (flag `N` + incidência NENHUMA) | nada obrigatório; o que existir tem de ser válido |
|
||||
| `NEEDS_REVIEW` | incidência fora do vocabulário do SGV, ou regime tributado com flag/incidência em branco (declaração ilegível) | único caso não decidível → "Não classificado" |
|
||||
|
||||
Base de `NOT_APPLICABLE`: `SIMPLES_DAS` para CRT 1/2/4; `COMPANY_DEFAULT` para CRT 3 e demais (o produto sem tratamento próprio herda o padrão da empresa).
|
||||
Por quê (fontes): Simples — LC 123/2006 art. 13, IV e V (Cofins e PIS no DAS) e art. 18, §4º-A, I (segregação da receita monofásica): o produto sem tratamento especial não tem PIS/COFINS de documento a calcular. Regime normal — Lei 9.718/1998 arts. 2º–3º (cumulativo), Lei 10.637/2002 art. 1º (PIS) e Lei 10.833/2003 art. 1º (Cofins) (não cumulativo): o tributo incide, mas o perfil (cumulativo/não cumulativo) é fato da empresa; o default do produto é opcional porque o padrão vem da empresa. Regimes especiais por NCM (SPED 4.3.10–4.3.15, Lei 10.147/2000) servem para *coerência de CST* e para o motor de correção — não tornam obrigatório um produto que não declara nada, porque o SGV não usa o campo na emissão e a extração das tabelas por NCM é parcial. Documento — NT 2025.002-RTC v1.51, Grupos Q/S: o grupo é obrigatório no item da NF-e (rejeições 745/748), o que vale para o documento e não para o cadastro.
|
||||
|
||||
### Por que é assim (fontes)
|
||||
|
||||
1. **Simples Nacional (CRT 1, 2, 4)** — SOURCE Planalto; DOCUMENT LC 123/2006 compilada; ARTICLES art. 13, IV (Cofins) e V (PIS/Pasep): recolhidos mensalmente no DAS; art. 18, §4º-A, I: o contribuinte deve segregar a receita sujeita à tributação monofásica. Logo o produto sem regime especial não tem PIS/COFINS de documento a calcular; só a monofasia exige segregação — e ela é *declarada* pelo produto (flag S / incidência).
|
||||
2. **Regime normal (CRT 3)** — Lei 9.718/1998 art. 2º-3º (cumulativo, faturamento), Lei 10.637/2002 art. 1º (PIS não cumulativo), Lei 10.833/2003 art. 1º (Cofins não cumulativa). O regime (cumulativo/não cumulativo) é fato da empresa e alimenta o padrão da empresa, não o produto; por isso o produto vazio é `NOT_APPLICABLE (COMPANY_DEFAULT)` e o perfil da empresa continua sendo pergunta de empresa (`OPERATOR_COMPANY_QUESTIONS_V1.md`).
|
||||
3. **Regimes especiais por NCM** — SPED EFD-Contribuições, tabelas 4.3.10/4.3.11 (monofásica, CST 02/03/04), 4.3.12 (ST, CST 05), 4.3.13 (alíquota zero, CST 06), 4.3.14 (isenção, CST 07), 4.3.15 (sem incidência, CST 08) — versões 2026 baixadas de `sped.rfb.gov.br/arquivo/download/{1638,5786,1642,1643,1646,1651}` (`SPED_PISCOFINS_TABLES_V3.json`; 4.3.13 v1.34 de 16/04/2026). Lei 10.147/2000 (monofasia de medicamentos/perfumaria). Servem para *validar coerência* (CST × incidência) e para o motor de correção de produto; **não** tornam obrigatório preencher um produto que não declara nada, porque no SGV o campo não alimenta o documento (item 1).
|
||||
4. **Domínio de CST** — SPED tabelas 4.3.3 (CST-PIS) e 4.3.4 (CST-COFINS): 01–09, 49, 50–56, 60–67, 70–75, 98, 99. O seletor do SGV grava 2 dígitos.
|
||||
5. **Documento fiscal** — NT 2025.002-RTC v1.51 (Portal NF-e, publicada 04/08/2026), Grupos Q (PIS) e S (COFINS): o grupo é obrigatório no item da NF-e (rejeições 745/748), com exceções por finalidade. Isso vale para o **documento**; não obriga o cadastro do produto a ter o default preenchido.
|
||||
|
||||
## 3. Regras de validação (blockers)
|
||||
|
||||
Entrada e saída são avaliadas separadamente; PIS e COFINS são independentes.
|
||||
## 3. CST: normalização canônica (somente leitura)
|
||||
`canonical_cst`: aceita **exatamente** 2 dígitos oficiais (SPED 4.3.3/4.3.4: 01–09, 49, 50–56, 60–67, 70–75, 98, 99) ou **um** dígito cujo preenchimento com zero à esquerda seja um código oficial (`4` → `04`). Nada mais é normalizado (`049`, `000`, `060`, `2x`, `0` continuam inválidos).
|
||||
- `VALID_AFTER_NORMALIZATION`: semanticamente válido para a cobertura e para as regras (CST × incidência, alíquota); gera *warning* não bloqueante `CST_PIS_NON_CANONICAL_FORMAT` / `CST_COFINS_NON_CANONICAL_FORMAT` (contagem e amostras em `fiscal_context.non_canonical_warnings`).
|
||||
- Nada é escrito no Firebird; escrever `04` seria decisão futura.
|
||||
|
||||
## 4. Blockers (só onde o campo se aplica: REQUIRED/OPTIONAL)
|
||||
| Blocker | Quando |
|
||||
|---|---|
|
||||
| `{PIS,COFINS}_CST_INVALID` | CST de entrada ou saída digitado que não é um código oficial de 2 dígitos (ex.: `2`, `4`, `049`). Dica no motivo quando um dígito vira código oficial com zero à esquerda (`4` → `04`); nunca aplicada |
|
||||
| `{PIS,COFINS}_CST_INVALID` | CST digitado que não é canônico nem normalizável |
|
||||
| `{PIS,COFINS}_CST_MISSING` | produto `REQUIRED` sem CST de saída |
|
||||
| `{PIS,COFINS}_RATE_MISSING` | CST de saída 01/02 sem alíquota |
|
||||
| `{PIS,COFINS}_RATE_MISSING` | CST de saída (canônico) 01/02 sem alíquota |
|
||||
| `{PIS,COFINS}_RATE_INVALID` | alíquota não numérica ou > 100 |
|
||||
| `{PIS,COFINS}_CST_INCIDENCE_MISMATCH` | incidência MONOFÁSICA com CST fora de {02,03,04}; SUBST.TRIBUTÁRIA com CST ≠ 05 |
|
||||
| `{PIS,COFINS}_CST_INCIDENCE_MISMATCH` | MONOFÁSICA com CST fora de {02,03,04}; SUBST.TRIBUTÁRIA com CST ≠ 05 |
|
||||
| `PIS_COFINS_FLAG_INCONSISTENT` | flag N com incidência especial; flag S sem incidência e sem códigos |
|
||||
|
||||
Alíquota: CST 01/02 exigem percentual; 03 (valor por unidade) e 04–09 não têm percentual; demais CSTs, opcional (validada se existir).
|
||||
Warnings (nunca bloqueiam): `CST_*_NON_CANONICAL_FORMAT`, `CST_*_INVALID_IGNORED` (valor inválido digitado em produto `NOT_APPLICABLE`).
|
||||
|
||||
## 4. Regimes no corpus SGV
|
||||
Encontrados: CRT 1 (Simples: CONSTRUCAO, MMMODAS, UNIAO, TESTESOFICIAIS) e CRT 3 (RINCAO, via EMISSOR). O motor não assume CRT 1: a base de `NOT_APPLICABLE` e as regras de validade dependem apenas dos dados do produto; o CRT só escolhe a *base* (DAS × padrão da empresa) e será usado pelos perfis de empresa.
|
||||
## 5. Regimes no corpus SGV
|
||||
CRT 1 (CONSTRUCAO, MMMODAS, UNIAO, TESTESOFICIAIS) e CRT 3 (RINCAO, via EMISSOR). Nenhuma regra assume CRT 1.
|
||||
|
||||
## 5. Campos ignorados quando não aplicáveis
|
||||
Produto `NOT_APPLICABLE`: os 6 campos, `PISCOFINS` e `INCIDENCIA_PISCOFINS` não contam. Candidatos a *write target* futuro (não usados nesta versão, sem escrita): `COD_SAIDA_PIS`, `COD_SAIDA_COFINS` (a partir das tabelas 4.3.10–4.3.15, já aplicadas na missão Local Completion V2 nas cópias autorizadas).
|
||||
## 6. Campos ignorados quando não aplicáveis / write targets candidatos
|
||||
Produto `NOT_APPLICABLE`: os 6 campos, `PISCOFINS` e `INCIDENCIA_PISCOFINS` não contam. Candidatos futuros (sem escrita nesta versão): `COD_SAIDA_PIS`, `COD_SAIDA_COFINS` e a normalização `4→04`.
|
||||
|
||||
@@ -35,8 +35,19 @@ O schema do produto não modela alíquota IBS/CBS, valor IBS/CBS nem `cCredPres`
|
||||
|
||||
Nota fiscal vs. regra do produto: para CRT 1 a legislação só exige IBS/CBS no documento a partir de 2027; a regra deste motor segue o **interruptor do ERP** — quando o cliente liga a reforma no SGV, o cadastro passa a ser cobrado.
|
||||
|
||||
## Achado de corpus (somente leitura)
|
||||
A cópia local `TESTESOFICIAIS.FDB` tem `USAR_REFORMA_TRIBUTARIA=1` e 0 produtos com `CCLASSTRIB`, enquanto o piloto confirmado pelo operador tem `0`. Nada foi alterado no arquivo; o resultado do piloto é reproduzido por simulação pura (`RTC_ENABLED=false`, `scripts/unified_coverage_e2e.py`).
|
||||
## Fonte da verdade (ERP_RTC_ENABLED)
|
||||
Quem decide é o **Agent**, lendo o banco real daquela instalação **a cada varredura** (nunca uma decisão guardada no Hub, nunca cache local):
|
||||
`SELECT FIRST 20 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS` — todas as linhas. Uma linha (ou linhas iguais) truthy ⇒ ativa; linhas divergentes ⇒ **não** ativa e `rtc_param_conflict=true` (nunca "adivinha" ligado); parâmetro ausente/nulo ⇒ desativada.
|
||||
O estado observado vai em `fiscal_context` do resultado do job e aparece no painel com proveniência (tooltip): `rtc_enabled`/`erp_rtc_enabled`, `rtc_source` (`PARAMETROS.USAR_REFORMA_TRIBUTARIA`), `rtc_param`, `rtc_param_rows`, `rtc_param_conflict`, `rtc_observed_at` (UTC), `rtc_supported`, `rtc_catalog_valid`.
|
||||
O fingerprint dos blockers inclui o estado RTC: 0→1 cria blockers novos; 1→0 faz a varredura seguinte não reenviar os RTC e o Hub os fecha (`AUTO_REEVALUATION`).
|
||||
|
||||
## ERP_RTC_ENABLED × LEGAL_EFFECTIVE_CONTEXT (registrados separadamente)
|
||||
- `ERP_RTC_ENABLED` é a regra operacional do produto e a única que entra na completude.
|
||||
- `legal_effective_context(crt)` é **informativo** (`informational_only=true`, nunca blocker): para CRT 1/2/4 a NT 2025.002 v1.51 diz que as orientações de IBS/CBS em documentos virão em NT futura e que a tributação desses contribuintes ocorre a partir de 2027 (LC 214/2025 art. 348); para CRT 3 o preenchimento é obrigatório desde 01/01/2026. Uma empresa Simples com o interruptor ligado no SGV é validada; uma com o interruptor desligado não é, seja qual for o calendário legal.
|
||||
|
||||
## Achado de corpus e causa da divergência de RTC (somente leitura)
|
||||
Estado atual do parâmetro nas cópias locais: `CONSTRUCAO=1`, `TESTESOFICIAIS=1`, `MMMODAS=0`, `RINCAO=0`, `UNIAO=0` (cada uma com **uma** linha em `PARAMETROS`, `ODS 11.2`, Firebird 2.5.9). O discovery de `TESTESOFICIAIS.FDB` registrou `0` às 15:25 de 24/09 e a leitura de hoje (mesmo arquivo, mesmas 521 linhas) retorna `1`. Causas descartadas: linha errada / várias linhas (há 1), cache do Hub (a leitura é direta no Firebird, o Agent lê ao vivo), outro arquivo (mesmo caminho canônico). Causa real comprovada: **o valor mudou no próprio banco depois do discovery** (mesmo arquivo, mesma linha única). O autor da mudança não é rastreável (o Firebird 2.5 não guarda histórico de valores e o SGV não tem tabela de log de parâmetros); a hipótese mais simples é a alteração do parâmetro "usar reforma tributária" no SGV 5.1.0.4, que é quem lê esse parâmetro. Por isso o Agent nunca reaproveita um estado anterior.
|
||||
Consequência medida (TESTESOFICIAIS, RTC ligada de fato): 521/521 produtos com `CCLASSTRIB_MISSING` ⇒ cobertura real 0%; com RTC desligada (estado do piloto) 94,82%.
|
||||
|
||||
## Testes
|
||||
RTC off/on, `CCLASSTRIB` vazio/válido/inválido/órfão/expirado, catálogo inválido: `central/tests/test_unified_fiscal_coverage.py`, `agent/tests/test_unified_fiscal.py`.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"mode": "FULL (Hub decision + fiscal)",
|
||||
"database": "TESTESOFICIAIS",
|
||||
"crt": "1",
|
||||
"products": 521,
|
||||
@@ -19,21 +20,18 @@
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 0.0,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 2,
|
||||
"COFINS_BLOCKERS": 2,
|
||||
"PIS_BLOCKERS": 1,
|
||||
"COFINS_BLOCKERS": 1,
|
||||
"RTC_BLOCKERS": 521,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"CCLASSTRIB_MISSING": 521,
|
||||
"COFINS_CST_INCIDENCE_MISMATCH": 1,
|
||||
"COFINS_CST_INVALID": 1,
|
||||
"PIS_CST_INCIDENCE_MISMATCH": 1,
|
||||
"PIS_CST_INVALID": 1
|
||||
"PIS_CST_INCIDENCE_MISMATCH": 1
|
||||
},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 518,
|
||||
"REQUIRED": 2,
|
||||
"OPTIONAL": 1
|
||||
"NOT_APPLICABLE": 519,
|
||||
"REQUIRED": 2
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"REQUIRED": 521
|
||||
@@ -281,11 +279,26 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"non_canonical_warnings": 1,
|
||||
"context": {
|
||||
"rtc_enabled": true,
|
||||
"erp_rtc_enabled": true,
|
||||
"rtc_supported": true,
|
||||
"rtc_catalog_valid": true,
|
||||
"rtc_param": 1,
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_param_rows": 1,
|
||||
"rtc_param_conflict": false,
|
||||
"rtc_observed_at": "2026-09-24T20:40:30+00:00",
|
||||
"legal_effective_context": {
|
||||
"crt": "1",
|
||||
"informational_only": true,
|
||||
"ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING",
|
||||
"sources": [
|
||||
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027",
|
||||
"LC 214/2025 art. 348"
|
||||
]
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
@@ -296,37 +309,67 @@
|
||||
"valid": true
|
||||
},
|
||||
"crt": "1",
|
||||
"products_evaluated": 521
|
||||
"products_evaluated": 521,
|
||||
"pis_cofins_applicability": {
|
||||
"NOT_APPLICABLE": 519,
|
||||
"REQUIRED": 2
|
||||
},
|
||||
"non_canonical_warnings": {
|
||||
"products": 1,
|
||||
"by_type": {
|
||||
"CST_COFINS_NON_CANONICAL_FORMAT": 2,
|
||||
"CST_PIS_NON_CANONICAL_FORMAT": 2
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"product_code": "001434",
|
||||
"type": "CST_PIS_NON_CANONICAL_FORMAT",
|
||||
"reason": "ENTRY:2->02"
|
||||
},
|
||||
{
|
||||
"product_code": "001434",
|
||||
"type": "CST_PIS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:2->02"
|
||||
},
|
||||
{
|
||||
"product_code": "001434",
|
||||
"type": "CST_COFINS_NON_CANONICAL_FORMAT",
|
||||
"reason": "ENTRY:2->02"
|
||||
},
|
||||
{
|
||||
"product_code": "001434",
|
||||
"type": "CST_COFINS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:2->02"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RTC_OFF_SIMULATION": {
|
||||
"rtc_enabled": false,
|
||||
"TOTAL_PRODUCTS": 521,
|
||||
"COMPLETE_PRODUCTS": 493,
|
||||
"SAFE_BLOCKED_PRODUCTS": 28,
|
||||
"COMPLETE_PRODUCTS": 494,
|
||||
"SAFE_BLOCKED_PRODUCTS": 27,
|
||||
"FAILED_PRODUCTS": 0,
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 94.63,
|
||||
"COVERAGE_PERCENT": 94.82,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 2,
|
||||
"COFINS_BLOCKERS": 2,
|
||||
"PIS_BLOCKERS": 1,
|
||||
"COFINS_BLOCKERS": 1,
|
||||
"RTC_BLOCKERS": 0,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"COFINS_CST_INCIDENCE_MISMATCH": 1,
|
||||
"COFINS_CST_INVALID": 1,
|
||||
"PIS_CST_INCIDENCE_MISMATCH": 1,
|
||||
"PIS_CST_INVALID": 1
|
||||
"PIS_CST_INCIDENCE_MISMATCH": 1
|
||||
},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 518,
|
||||
"REQUIRED": 2,
|
||||
"OPTIONAL": 1
|
||||
"NOT_APPLICABLE": 519,
|
||||
"REQUIRED": 2
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"NOT_APPLICABLE": 521
|
||||
},
|
||||
"changed_products": 2,
|
||||
"changed_products": 1,
|
||||
"examples": [
|
||||
{
|
||||
"product": "000700",
|
||||
@@ -336,17 +379,9 @@
|
||||
"COFINS_CST_INCIDENCE_MISMATCH",
|
||||
"PIS_CST_INCIDENCE_MISMATCH"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "001434",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"non_canonical_warnings": 1
|
||||
},
|
||||
"RTC_ON_SIMULATION": {
|
||||
"rtc_enabled": true,
|
||||
@@ -357,21 +392,18 @@
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 0.0,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 2,
|
||||
"COFINS_BLOCKERS": 2,
|
||||
"PIS_BLOCKERS": 1,
|
||||
"COFINS_BLOCKERS": 1,
|
||||
"RTC_BLOCKERS": 521,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"CCLASSTRIB_MISSING": 521,
|
||||
"COFINS_CST_INCIDENCE_MISMATCH": 1,
|
||||
"COFINS_CST_INVALID": 1,
|
||||
"PIS_CST_INCIDENCE_MISMATCH": 1,
|
||||
"PIS_CST_INVALID": 1
|
||||
"PIS_CST_INCIDENCE_MISMATCH": 1
|
||||
},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 518,
|
||||
"REQUIRED": 2,
|
||||
"OPTIONAL": 1
|
||||
"NOT_APPLICABLE": 519,
|
||||
"REQUIRED": 2
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"REQUIRED": 521
|
||||
@@ -618,7 +650,8 @@
|
||||
"CCLASSTRIB_MISSING"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"non_canonical_warnings": 1
|
||||
}
|
||||
},
|
||||
"SAFETY": {
|
||||
|
||||
@@ -276,11 +276,26 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"non_canonical_warnings": 0,
|
||||
"context": {
|
||||
"rtc_enabled": true,
|
||||
"erp_rtc_enabled": true,
|
||||
"rtc_supported": true,
|
||||
"rtc_catalog_valid": true,
|
||||
"rtc_param": 1,
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_param_rows": 1,
|
||||
"rtc_param_conflict": false,
|
||||
"rtc_observed_at": "2026-09-24T20:40:41+00:00",
|
||||
"legal_effective_context": {
|
||||
"crt": "1",
|
||||
"informational_only": true,
|
||||
"ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING",
|
||||
"sources": [
|
||||
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027",
|
||||
"LC 214/2025 art. 348"
|
||||
]
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
@@ -291,7 +306,15 @@
|
||||
"valid": true
|
||||
},
|
||||
"crt": "1",
|
||||
"products_evaluated": 3712
|
||||
"products_evaluated": 3712,
|
||||
"pis_cofins_applicability": {
|
||||
"NOT_APPLICABLE": 3712
|
||||
},
|
||||
"non_canonical_warnings": {
|
||||
"products": 0,
|
||||
"by_type": {},
|
||||
"samples": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"RTC_OFF_SIMULATION": {
|
||||
@@ -315,7 +338,8 @@
|
||||
"NOT_APPLICABLE": 3712
|
||||
},
|
||||
"changed_products": 0,
|
||||
"examples": []
|
||||
"examples": [],
|
||||
"non_canonical_warnings": 0
|
||||
},
|
||||
"RTC_ON_SIMULATION": {
|
||||
"rtc_enabled": true,
|
||||
@@ -581,7 +605,8 @@
|
||||
"CCLASSTRIB_MISSING"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"non_canonical_warnings": 0
|
||||
}
|
||||
},
|
||||
"SAFETY": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"mode": "FISCAL_ONLY (no Hub decision)",
|
||||
"database": "MMMODAS",
|
||||
"crt": "2",
|
||||
"crt": "1",
|
||||
"products": 20997,
|
||||
"fiscal_products_read": 20997,
|
||||
"BEFORE": {
|
||||
@@ -26,19 +26,33 @@
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {},
|
||||
"applicability": {
|
||||
"OPTIONAL": 1,
|
||||
"NOT_APPLICABLE": 20995
|
||||
"NOT_APPLICABLE": 20996
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"NOT_APPLICABLE": 20996
|
||||
},
|
||||
"changed_products": 0,
|
||||
"examples": [],
|
||||
"non_canonical_warnings": 0,
|
||||
"context": {
|
||||
"rtc_enabled": false,
|
||||
"erp_rtc_enabled": false,
|
||||
"rtc_supported": true,
|
||||
"rtc_catalog_valid": true,
|
||||
"rtc_param": 0,
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_param_rows": 1,
|
||||
"rtc_param_conflict": false,
|
||||
"rtc_observed_at": "2026-09-24T20:40:49+00:00",
|
||||
"legal_effective_context": {
|
||||
"crt": "1",
|
||||
"informational_only": true,
|
||||
"ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING",
|
||||
"sources": [
|
||||
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027",
|
||||
"LC 214/2025 art. 348"
|
||||
]
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
@@ -48,8 +62,16 @@
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
},
|
||||
"crt": "2",
|
||||
"products_evaluated": 20997
|
||||
"crt": "1",
|
||||
"products_evaluated": 20997,
|
||||
"pis_cofins_applicability": {
|
||||
"NOT_APPLICABLE": 20997
|
||||
},
|
||||
"non_canonical_warnings": {
|
||||
"products": 0,
|
||||
"by_type": {},
|
||||
"samples": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"RTC_OFF_SIMULATION": {
|
||||
@@ -67,14 +89,14 @@
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {},
|
||||
"applicability": {
|
||||
"OPTIONAL": 1,
|
||||
"NOT_APPLICABLE": 20995
|
||||
"NOT_APPLICABLE": 20996
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"NOT_APPLICABLE": 20996
|
||||
},
|
||||
"changed_products": 0,
|
||||
"examples": []
|
||||
"examples": [],
|
||||
"non_canonical_warnings": 0
|
||||
},
|
||||
"RTC_ON_SIMULATION": {
|
||||
"rtc_enabled": true,
|
||||
@@ -93,8 +115,7 @@
|
||||
"CCLASSTRIB_MISSING": 20996
|
||||
},
|
||||
"applicability": {
|
||||
"OPTIONAL": 1,
|
||||
"NOT_APPLICABLE": 20995
|
||||
"NOT_APPLICABLE": 20996
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"REQUIRED": 20996
|
||||
@@ -341,7 +362,8 @@
|
||||
"CCLASSTRIB_MISSING"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"non_canonical_warnings": 0
|
||||
}
|
||||
},
|
||||
"SAFETY": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"mode": "FISCAL_ONLY (no Hub decision)",
|
||||
"database": "RINCAO",
|
||||
"crt": null,
|
||||
"crt": "3",
|
||||
"products": 894,
|
||||
"fiscal_products_read": 894,
|
||||
"BEFORE": {
|
||||
@@ -13,292 +14,30 @@
|
||||
"ERP_AS_FOUND": {
|
||||
"rtc_enabled": false,
|
||||
"TOTAL_PRODUCTS": 894,
|
||||
"COMPLETE_PRODUCTS": 815,
|
||||
"SAFE_BLOCKED_PRODUCTS": 79,
|
||||
"COMPLETE_PRODUCTS": 893,
|
||||
"SAFE_BLOCKED_PRODUCTS": 1,
|
||||
"FAILED_PRODUCTS": 0,
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 91.16,
|
||||
"COVERAGE_PERCENT": 99.89,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 79,
|
||||
"COFINS_BLOCKERS": 79,
|
||||
"PIS_BLOCKERS": 1,
|
||||
"COFINS_BLOCKERS": 1,
|
||||
"RTC_BLOCKERS": 0,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"COFINS_CST_INVALID": 79,
|
||||
"PIS_CST_INVALID": 79
|
||||
"COFINS_CST_INVALID": 1,
|
||||
"PIS_CST_INVALID": 1
|
||||
},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 815,
|
||||
"OPTIONAL": 79
|
||||
"OPTIONAL": 894
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"NOT_APPLICABLE": 894
|
||||
},
|
||||
"changed_products": 79,
|
||||
"changed_products": 1,
|
||||
"examples": [
|
||||
{
|
||||
"product": "000139",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000140",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000141",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000142",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000143",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000144",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000145",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000146",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000147",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000148",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000149",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000150",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000156",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000157",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000158",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000159",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000160",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000161",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000165",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000166",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000167",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000168",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000169",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000170",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000172",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000173",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000174",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000175",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000176",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000177",
|
||||
"product": "000866",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
@@ -307,11 +46,26 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"non_canonical_warnings": 78,
|
||||
"context": {
|
||||
"rtc_enabled": false,
|
||||
"erp_rtc_enabled": false,
|
||||
"rtc_supported": true,
|
||||
"rtc_catalog_valid": true,
|
||||
"rtc_param": 0,
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_param_rows": 1,
|
||||
"rtc_param_conflict": false,
|
||||
"rtc_observed_at": "2026-09-24T20:40:58+00:00",
|
||||
"legal_effective_context": {
|
||||
"crt": "3",
|
||||
"informational_only": true,
|
||||
"ibs_cbs_in_documents": "MANDATORY_FROM_2026-01-01",
|
||||
"sources": [
|
||||
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027",
|
||||
"LC 214/2025 art. 348"
|
||||
]
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
@@ -321,299 +75,99 @@
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
},
|
||||
"crt": null,
|
||||
"products_evaluated": 894
|
||||
"crt": "3",
|
||||
"products_evaluated": 894,
|
||||
"pis_cofins_applicability": {
|
||||
"OPTIONAL": 894
|
||||
},
|
||||
"non_canonical_warnings": {
|
||||
"products": 78,
|
||||
"by_type": {
|
||||
"CST_COFINS_NON_CANONICAL_FORMAT": 78,
|
||||
"CST_PIS_NON_CANONICAL_FORMAT": 78
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"product_code": "000139",
|
||||
"type": "CST_PIS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000139",
|
||||
"type": "CST_COFINS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000140",
|
||||
"type": "CST_PIS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000140",
|
||||
"type": "CST_COFINS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000141",
|
||||
"type": "CST_PIS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000141",
|
||||
"type": "CST_COFINS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000142",
|
||||
"type": "CST_PIS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000142",
|
||||
"type": "CST_COFINS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000143",
|
||||
"type": "CST_PIS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
},
|
||||
{
|
||||
"product_code": "000143",
|
||||
"type": "CST_COFINS_NON_CANONICAL_FORMAT",
|
||||
"reason": "EXIT:4->04"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RTC_OFF_SIMULATION": {
|
||||
"rtc_enabled": false,
|
||||
"TOTAL_PRODUCTS": 894,
|
||||
"COMPLETE_PRODUCTS": 815,
|
||||
"SAFE_BLOCKED_PRODUCTS": 79,
|
||||
"COMPLETE_PRODUCTS": 893,
|
||||
"SAFE_BLOCKED_PRODUCTS": 1,
|
||||
"FAILED_PRODUCTS": 0,
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 91.16,
|
||||
"COVERAGE_PERCENT": 99.89,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 79,
|
||||
"COFINS_BLOCKERS": 79,
|
||||
"PIS_BLOCKERS": 1,
|
||||
"COFINS_BLOCKERS": 1,
|
||||
"RTC_BLOCKERS": 0,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"COFINS_CST_INVALID": 79,
|
||||
"PIS_CST_INVALID": 79
|
||||
"COFINS_CST_INVALID": 1,
|
||||
"PIS_CST_INVALID": 1
|
||||
},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 815,
|
||||
"OPTIONAL": 79
|
||||
"OPTIONAL": 894
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"NOT_APPLICABLE": 894
|
||||
},
|
||||
"changed_products": 79,
|
||||
"changed_products": 1,
|
||||
"examples": [
|
||||
{
|
||||
"product": "000139",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000140",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000141",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000142",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000143",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000144",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000145",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000146",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000147",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000148",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000149",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000150",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000156",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000157",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000158",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000159",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000160",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000161",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000165",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000166",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000167",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000168",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000169",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000170",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000172",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000173",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000174",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000175",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000176",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID",
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "000177",
|
||||
"product": "000866",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
@@ -621,7 +175,8 @@
|
||||
"PIS_CST_INVALID"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"non_canonical_warnings": 78
|
||||
},
|
||||
"RTC_ON_SIMULATION": {
|
||||
"rtc_enabled": true,
|
||||
@@ -632,18 +187,17 @@
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 0.0,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 79,
|
||||
"COFINS_BLOCKERS": 79,
|
||||
"PIS_BLOCKERS": 1,
|
||||
"COFINS_BLOCKERS": 1,
|
||||
"RTC_BLOCKERS": 894,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"CCLASSTRIB_MISSING": 894,
|
||||
"COFINS_CST_INVALID": 79,
|
||||
"PIS_CST_INVALID": 79
|
||||
"COFINS_CST_INVALID": 1,
|
||||
"PIS_CST_INVALID": 1
|
||||
},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 815,
|
||||
"OPTIONAL": 79
|
||||
"OPTIONAL": 894
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"REQUIRED": 894
|
||||
@@ -890,7 +444,8 @@
|
||||
"CCLASSTRIB_MISSING"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"non_canonical_warnings": 78
|
||||
}
|
||||
},
|
||||
"SAFETY": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"mode": "FISCAL_ONLY (no Hub decision)",
|
||||
"database": "UNIAO",
|
||||
"crt": "3",
|
||||
"crt": "1",
|
||||
"products": 8942,
|
||||
"fiscal_products_read": 8942,
|
||||
"BEFORE": {
|
||||
@@ -14,50 +14,45 @@
|
||||
"ERP_AS_FOUND": {
|
||||
"rtc_enabled": false,
|
||||
"TOTAL_PRODUCTS": 8942,
|
||||
"COMPLETE_PRODUCTS": 8940,
|
||||
"SAFE_BLOCKED_PRODUCTS": 2,
|
||||
"COMPLETE_PRODUCTS": 8942,
|
||||
"SAFE_BLOCKED_PRODUCTS": 0,
|
||||
"FAILED_PRODUCTS": 0,
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 99.98,
|
||||
"COVERAGE_PERCENT": 100.0,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 0,
|
||||
"COFINS_BLOCKERS": 2,
|
||||
"COFINS_BLOCKERS": 0,
|
||||
"RTC_BLOCKERS": 0,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"COFINS_CST_INVALID": 2
|
||||
},
|
||||
"blockers_by_type": {},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 8170,
|
||||
"OPTIONAL": 772
|
||||
"NOT_APPLICABLE": 8942
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"NOT_APPLICABLE": 8942
|
||||
},
|
||||
"changed_products": 2,
|
||||
"examples": [
|
||||
{
|
||||
"product": "000304",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "003090",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID"
|
||||
]
|
||||
}
|
||||
],
|
||||
"changed_products": 0,
|
||||
"examples": [],
|
||||
"non_canonical_warnings": 2,
|
||||
"context": {
|
||||
"rtc_enabled": false,
|
||||
"erp_rtc_enabled": false,
|
||||
"rtc_supported": true,
|
||||
"rtc_catalog_valid": true,
|
||||
"rtc_param": 0,
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_param_rows": 1,
|
||||
"rtc_param_conflict": false,
|
||||
"rtc_observed_at": "2026-09-24T20:41:07+00:00",
|
||||
"legal_effective_context": {
|
||||
"crt": "1",
|
||||
"informational_only": true,
|
||||
"ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING",
|
||||
"sources": [
|
||||
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027",
|
||||
"LC 214/2025 art. 348"
|
||||
]
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
@@ -67,52 +62,54 @@
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
},
|
||||
"crt": "3",
|
||||
"products_evaluated": 8942
|
||||
"crt": "1",
|
||||
"products_evaluated": 8942,
|
||||
"pis_cofins_applicability": {
|
||||
"NOT_APPLICABLE": 8942
|
||||
},
|
||||
"non_canonical_warnings": {
|
||||
"products": 2,
|
||||
"by_type": {
|
||||
"CST_COFINS_INVALID_IGNORED": 2
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"product_code": "000304",
|
||||
"type": "CST_COFINS_INVALID_IGNORED",
|
||||
"reason": "ENTRY:000"
|
||||
},
|
||||
{
|
||||
"product_code": "003090",
|
||||
"type": "CST_COFINS_INVALID_IGNORED",
|
||||
"reason": "EXIT:060"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RTC_OFF_SIMULATION": {
|
||||
"rtc_enabled": false,
|
||||
"TOTAL_PRODUCTS": 8942,
|
||||
"COMPLETE_PRODUCTS": 8940,
|
||||
"SAFE_BLOCKED_PRODUCTS": 2,
|
||||
"COMPLETE_PRODUCTS": 8942,
|
||||
"SAFE_BLOCKED_PRODUCTS": 0,
|
||||
"FAILED_PRODUCTS": 0,
|
||||
"UNCLASSIFIED_PRODUCTS": 0,
|
||||
"COVERAGE_PERCENT": 99.98,
|
||||
"COVERAGE_PERCENT": 100.0,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 0,
|
||||
"COFINS_BLOCKERS": 2,
|
||||
"COFINS_BLOCKERS": 0,
|
||||
"RTC_BLOCKERS": 0,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"COFINS_CST_INVALID": 2
|
||||
},
|
||||
"blockers_by_type": {},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 8170,
|
||||
"OPTIONAL": 772
|
||||
"NOT_APPLICABLE": 8942
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"NOT_APPLICABLE": 8942
|
||||
},
|
||||
"changed_products": 2,
|
||||
"examples": [
|
||||
{
|
||||
"product": "000304",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"product": "003090",
|
||||
"BEFORE_STATUS": "COMPLETE",
|
||||
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
|
||||
"REASON": [
|
||||
"COFINS_CST_INVALID"
|
||||
]
|
||||
}
|
||||
]
|
||||
"changed_products": 0,
|
||||
"examples": [],
|
||||
"non_canonical_warnings": 2
|
||||
},
|
||||
"RTC_ON_SIMULATION": {
|
||||
"rtc_enabled": true,
|
||||
@@ -124,16 +121,14 @@
|
||||
"COVERAGE_PERCENT": 0.0,
|
||||
"INVARIANT_PASS": true,
|
||||
"PIS_BLOCKERS": 0,
|
||||
"COFINS_BLOCKERS": 2,
|
||||
"COFINS_BLOCKERS": 0,
|
||||
"RTC_BLOCKERS": 8942,
|
||||
"OTHER_BLOCKERS": 0,
|
||||
"blockers_by_type": {
|
||||
"CCLASSTRIB_MISSING": 8942,
|
||||
"COFINS_CST_INVALID": 2
|
||||
"CCLASSTRIB_MISSING": 8942
|
||||
},
|
||||
"applicability": {
|
||||
"NOT_APPLICABLE": 8170,
|
||||
"OPTIONAL": 772
|
||||
"NOT_APPLICABLE": 8942
|
||||
},
|
||||
"rtc_applicability": {
|
||||
"REQUIRED": 8942
|
||||
@@ -380,7 +375,8 @@
|
||||
"CCLASSTRIB_MISSING"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"non_canonical_warnings": 2
|
||||
}
|
||||
},
|
||||
"SAFETY": {
|
||||
|
||||
@@ -39,26 +39,15 @@ Cada blocker fiscal é um blocker comum: `blocker_type=BLOCKED_FISCAL_DATA`, `fi
|
||||
## Segurança
|
||||
Somente leitura. `FIREBIRD_WRITES=0`: a avaliação abre uma conexão `READ COMMITTED READ ONLY`, executa apenas SELECT e nunca preenche `CCLASSTRIB`, PIS ou COFINS. Testes provam que só SELECT é emitido.
|
||||
|
||||
## Validação no banco real (somente leitura)
|
||||
`scripts/unified_coverage_e2e.py` → `docs/runs/unified_coverage_e2e*.json`. TESTESOFICIAIS (521 produtos, CRT 1):
|
||||
## Validação nos bancos reais (somente leitura, cinco bancos)
|
||||
`scripts/unified_coverage_e2e.py` → `docs/runs/unified_coverage_e2e*.json`. RTC lida **do banco como está hoje**; "RTC desligada" é simulação pura do avaliador (o parâmetro nunca é tocado).
|
||||
|
||||
| | Antes (só decisão) | Depois — RTC desligada (estado do piloto) |
|
||||
|---|---|---|
|
||||
| Completos | 495 (95,01%) | 493 (94,63%) |
|
||||
| Bloqueados | 26 | 28 |
|
||||
| Falhas / Não classificados | 0 / 0 | 0 / 0 |
|
||||
| RTC blockers | — | 0 |
|
||||
| PIS / COFINS blockers | — | 2 / 2 (2 produtos) |
|
||||
| Banco | CRT | RTC no banco | Produtos | Completos | Bloqueados | Não class. | Cobertura (como está) | Cobertura com RTC off |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| CONSTRUCAO | 1 | **ligada** | 3.712 | 0 | 3.712 (RTC) | 0 | 0% | 100% |
|
||||
| MMMODAS | 1 | desligada | 20.997 | 20.997 | 0 | 0 | 100% | 100% |
|
||||
| RINCAO | 3 | desligada | 894 | 893 | 1 | 0 | 99,89% (antes da normalização: 91,16%) | 99,89% |
|
||||
| UNIAO | 1 | desligada | 8.942 | 8.942 | 0 | 0 | 100% | 100% |
|
||||
| TESTESOFICIAIS | 1 | **ligada** | 521 | 0 | 521 (RTC) | 0 | 0% | 94,82% (494/521) |
|
||||
|
||||
Aplicabilidade PIS/COFINS: 518 `NOT_APPLICABLE`, 2 `REQUIRED`, 1 `OPTIONAL`, 0 `NEEDS_REVIEW` (antes: 492 `UNKNOWN_APPLICABILITY`). Os 2 produtos bloqueados são legítimos: um com CST `2`/`2` (formato inválido) e um com incidência SUBST.TRIBUTÁRIA e CST 01 (incompatível com CST 05).
|
||||
|
||||
### Corpus de clientes (RTC desligada, somente PIS/COFINS; `--fiscal-only`, sem decisão do Hub)
|
||||
| Banco | CRT | Produtos | NOT_APPLICABLE | OPTIONAL | Blockers PIS/COFINS | Cobertura da parte fiscal |
|
||||
|---|---|---|---|---|---|---|
|
||||
| CONSTRUCAO | 1 | 3.712 | 3.712 | 0 | 0 | 100% |
|
||||
| MMMODAS | 1 | 20.997 | 20.995 | 1 | 0 | 100% |
|
||||
| UNIAO | 1 | 8.942 | 8.170 | 772 | 2 produtos (COFINS `000`/`060`, estilo CST de ICMS) | 99,98% |
|
||||
| RINCAO | 3 | 894 | 815 | 79 | 79 produtos (78 com CST `4` sem zero à esquerda + 1) | 91,16% |
|
||||
| TESTESOFICIAIS | 1 | 521 | 518 | 1 | 2 produtos | 99,62% (94,63% com a decisão do Hub) |
|
||||
|
||||
`OPTIONAL` = valor digitado sem flag/incidência especial. RINCAO: `4` deve ser `04` (o seletor do SGV grava 2 dígitos); o motivo do blocker traz a dica `UNPADDED_USE_04`, nunca aplicada.
|
||||
Bancos além do piloto: medida só da parte fiscal (`--fiscal-only`, sem decisão do Hub). TESTESOFICIAIS: com a decisão do Hub (antes 495/521 = 95,01%). Bloqueios PIS/COFINS: TESTESOFICIAIS 1 produto (incidência SUBST.TRIBUTÁRIA com CST 01), RINCAO 1 produto (`049`/`000`), demais 0. Produtos com warning (não bloqueante): RINCAO 78 (`4`→`04`, não canônico), UNIAO 2 (CST `000`/`060` inválido, ignorado por ser Simples), TESTESOFICIAIS 1 (não canônico). Aplicabilidade: Simples ⇒ `NOT_APPLICABLE`; RINCAO ⇒ `OPTIONAL` (894); TESTESOFICIAIS 519 `NOT_APPLICABLE` + 2 `REQUIRED`.
|
||||
|
||||
@@ -34,6 +34,18 @@ def old_unknown(product):
|
||||
return not evidence
|
||||
|
||||
|
||||
def resolved_crt(name, path):
|
||||
"""Company regime exactly as the Agent resolves it (deterministic company-context resolver)."""
|
||||
from gti_fiscal_agent import company_context as cc
|
||||
conn = lc.connect(path)
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
result = cc.resolve(cc.collect_evidence(cur), cc.config_from_env(lc._cfg_env(name)))
|
||||
return result.get("company_crt") if result.get("status") == "PASS" else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--db", default="TESTESOFICIAIS"); ap.add_argument("--out", default="docs/runs/unified_coverage_e2e.json"); ap.add_argument("--crt", default=None); ap.add_argument("--fiscal-only", action="store_true", help="skip the Hub classification (decisions = EXACT_NCM); measures only the PIS/COFINS + RTC contribution")
|
||||
a = ap.parse_args()
|
||||
@@ -47,7 +59,7 @@ def main():
|
||||
cursor.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); crts = [r[0] for r in cursor.fetchall()]
|
||||
finally:
|
||||
connection.close()
|
||||
crt = a.crt or (crts[0] if len(crts) == 1 else None)
|
||||
crt = a.crt or resolved_crt(a.db, path) or (crts[0] if len(crts) == 1 else None)
|
||||
client = HubClient(os.environ.get("HUB_URL", lc.HUB), os.environ["AGENT_DEV_TOKEN"])
|
||||
decisions = [{"status": "EXACT_NCM"} for _ in products] if a.fiscal_only else client.classify_batch([to_hub_product(p) for p in products])
|
||||
before_states = [classify_product_state(p, d) for p, d in zip(products, decisions)]
|
||||
@@ -76,9 +88,9 @@ def main():
|
||||
"INVARIANT_PASS": total == summary["COMPLETE_PRODUCTS"] + summary["SAFE_BLOCKED"] + summary["FAILED_TECHNICAL"] + summary["UNCLASSIFIED"],
|
||||
"PIS_BLOCKERS": by_category["PIS"], "COFINS_BLOCKERS": by_category["COFINS"], "RTC_BLOCKERS": by_category["RTC"], "OTHER_BLOCKERS": by_category["OTHER"], "blockers_by_type": dict(sorted(by_type.items())),
|
||||
"applicability": dict(Counter(f["applicability"]["PIS_COFINS"] for f in findings.values())), "rtc_applicability": dict(Counter(f["applicability"]["RTC"] for f in findings.values())),
|
||||
"changed_products": len(changed), "examples": changed[:30]}
|
||||
"changed_products": len(changed), "examples": changed[:30], "non_canonical_warnings": out["context"]["non_canonical_warnings"]["products"]}
|
||||
if label == "ERP_AS_FOUND":
|
||||
runs[label]["context"] = {k: v for k, v in out["context"].items() if k != "capabilities"}
|
||||
runs[label]["context"] = {k: v for k, v in out["context"].items() if k not in ("capabilities",)}
|
||||
total = before["TOTAL_PRODUCTS"]
|
||||
report = {"mode": "FISCAL_ONLY (no Hub decision)" if a.fiscal_only else "FULL (Hub decision + fiscal)", "database": a.db, "crt": crt, "products": len(products), "fiscal_products_read": len(fiscal_products),
|
||||
"BEFORE": {"COMPLETE_PRODUCTS": before["COMPLETE_PRODUCTS"], "SAFE_BLOCKED_PRODUCTS": before["SAFE_BLOCKED"], "COVERAGE_PERCENT": round(100 * before["COMPLETE_PRODUCTS"] / total, 2),
|
||||
@@ -87,7 +99,7 @@ def main():
|
||||
Path(ROOT / a.out).write_text(json.dumps(report, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
|
||||
print(json.dumps({k: report[k] for k in ("database", "crt", "products", "BEFORE")}, ensure_ascii=False))
|
||||
for label, r in runs.items():
|
||||
print(label, {k: r[k] for k in ("rtc_enabled", "TOTAL_PRODUCTS", "COMPLETE_PRODUCTS", "SAFE_BLOCKED_PRODUCTS", "FAILED_PRODUCTS", "UNCLASSIFIED_PRODUCTS", "COVERAGE_PERCENT", "INVARIANT_PASS", "PIS_BLOCKERS", "COFINS_BLOCKERS", "RTC_BLOCKERS", "OTHER_BLOCKERS")})
|
||||
print(label, {k: r[k] for k in ("rtc_enabled", "TOTAL_PRODUCTS", "COMPLETE_PRODUCTS", "SAFE_BLOCKED_PRODUCTS", "FAILED_PRODUCTS", "UNCLASSIFIED_PRODUCTS", "COVERAGE_PERCENT", "INVARIANT_PASS", "PIS_BLOCKERS", "COFINS_BLOCKERS", "RTC_BLOCKERS", "OTHER_BLOCKERS", "non_canonical_warnings")})
|
||||
print(" applicability", r["applicability"], "blockers", r["blockers_by_type"])
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user