DEV Community

AppVerix
AppVerix

Posted on Edited on

Conferindo as contas de IBS/CBS no XML da NF-e com Python (base vezes alíquota, vIBS e IBSCBSTot)

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)