Se você mantém um emissor de NF-e/NFC-e ou faz auditoria de XML para escritório contábil, um dos erros mais comuns no grupo IBSCBS não é o campo faltando: é a conta que não fecha. O valor do item não bate com base × alíquota, ou a soma dos itens não bate com o total em IBSCBSTot, quase sempre por arredondamento.

Este post mostra um script pequeno em Python, só com a biblioteca padrão, que faz essa conferência. Ele é um ponto de partida, não um validador completo.

Fonte das regras: NT 2025.002-RTC v1.52, no Portal da NF-e. Confira sempre a versão vigente da NT e o leiaute (XSD) antes de usar qualquer caminho de campo deste exemplo.

As contas que precisam fechar

Para cada item (det/imposto/IBSCBS/gIBSCBS):

  • vIBSUF = vBC × pIBSUF
  • vIBSMun = vBC × pIBSMun
  • vIBS = vIBSUF + vIBSMun
  • vCBS = vBC × pCBS

E no total da nota (total/IBSCBSTot), cada valor deve bater com a soma dos itens. Em 2026 (ano-teste), as alíquotas esperadas são CBS 0,9%, IBS UF 0,1% e IBS Municipal 0% (LC 214/2025, arts. 343 e 346; regras UB18-10, UB37-10 e UB56-10). Por isso não deixe a alíquota fixa no código: ela muda em 2027.

Dois cuidados antes do código

  1. Use Decimal, nunca float. 0.1 + 0.2 em float já dá problema; em dinheiro, isso vira centavo de diferença no total.
  2. Defina a tolerância. Arredondar por item e depois somar pode dar um resultado diferente de somar e depois arredondar. No exemplo usamos tolerância de R$ 0,01, que é um critério nosso de conferência, não uma regra da NT.

O script

import sys from decimal import Decimal, ROUND_HALF_UP import xml.etree.ElementTree as ET NS = {"n": "http://www.portalfiscal.inf.br/nfe"} CENT = Decimal("0.01") TOL = Decimal("0.01") def d(el, path): x = el.find(path, NS) return Decimal(x.text) if x is not None and x.text else None def money(v): return v.quantize(CENT, rounding=ROUND_HALF_UP) def check(path): root = ET.parse(path).getroot() inf = root.find(".//n:infNFe", NS) problems = [] soma = {"vIBSUF": Decimal(0), "vIBSMun": Decimal(0), "vIBS": Decimal(0), "vCBS": Decimal(0)} for det in inf.findall("n:det", NS): item = det.get("nItem") g = det.find("n:imposto/n:IBSCBS/n:gIBSCBS", NS) if det.find("n:imposto/n:IBSCBS", NS) is None: problems.append(f"item {item}: grupo IBSCBS ausente (rejeição 1115)") continue if g is None: continue # CST sem gIBSCBS (ex.: isenção); fora do escopo deste exemplo  vbc = d(g, "n:vBC") pares = [("gIBSUF/n:pIBSUF", "gIBSUF/n:vIBSUF", "vIBSUF"), ("gIBSMun/n:pIBSMun", "gIBSMun/n:vIBSMun", "vIBSMun"), ("gCBS/n:pCBS", "gCBS/n:vCBS", "vCBS")] for p_path, v_path, nome in pares: p, v = d(g, "n:" + p_path), d(g, "n:" + v_path) if p is None or v is None: problems.append(f"item {item}: {nome} ausente") continue esperado = money(vbc * p / 100) if abs(esperado - v) > TOL: problems.append(f"item {item}: {nome} = {v}, esperado {esperado}") soma[nome] += v vibs = d(g, "n:vIBS") if vibs is not None: if vibs != d(g, "n:gIBSUF/n:vIBSUF") + d(g, "n:gIBSMun/n:vIBSMun"): problems.append(f"item {item}: vIBS diferente de vIBSUF + vIBSMun") soma["vIBS"] += vibs tot = inf.find("n:total/n:IBSCBSTot", NS) if tot is not None: # Confira estes caminhos no leiaute da NT vigente  for nome, caminho in [("vIBSUF", "n:gIBS/n:gIBSUF/n:vIBSUF"), ("vIBSMun", "n:gIBS/n:gIBSMun/n:vIBSMun"), ("vIBS", "n:gIBS/n:vIBS"), ("vCBS", "n:gCBS/n:vCBS")]: t = d(tot, caminho) if t is not None and abs(t - soma[nome]) > TOL: problems.append(f"total {nome} = {t}, soma dos itens {soma[nome]}") return problems for f in sys.argv[1:]: probs = check(f) print(f, "OK" if not probs else "") for p in probs: print(" -", p) 

Rodando contra um XML de teste com dois itens (um com vCBS errado e outro sem o grupo IBSCBS):

$ python3 check.py nota.xml nota.xml - item 1: vCBS = 9.50, esperado 9.00 - item 2: grupo IBSCBS ausente (rejeição 1115) - total vCBS = 9.00, soma dos itens 9.50 

O que este script não faz

Para ser claro sobre os limites: ele não confere se CST e cClassTrib existem e são compatíveis, não trata redução de alíquota, diferimento nem os casos sem gIBSCBS, não confere vItem/vNFTot e não valida o XML contra o XSD nem a assinatura. São justamente as partes que dão mais trabalho de manter a cada versão da NT.

Se não quiser manter isso

Nós (AppVerix) fizemos o Checa Nota, um verificador grátis e sem cadastro que já faz essas contas e mais: presença do grupo IBSCBS, CST e cClassTrib, alíquotas de 2026, reduções e diferimento exigidos pelo CST, totais de IBSCBSTot, vItem/vNFTot e ordem e formato dos campos de IBS/CBS. Aceita XML avulso ou .zip com até 2.000 notas, roda inteiro no navegador (os XMLs não são enviados para servidor) e exporta o relatório em CSV.

Ele também tem limites: não cobre NFS-e nem CT-e, e não valida o XML inteiro contra o schema nem a assinatura. Passar nele não garante autorização na SEFAZ.

Se você já tratou o arredondamento de outro jeito no seu emissor (por item, por total, com ajuste no último item), conta nos comentários.