Pular para conteúdo

06 — CNAB (Remessa e Retorno)

CNAB (Centro Nacional de Automação Bancária) é o padrão FEBRABAN para troca de arquivos entre empresa e banco. A PyCobrança implementa os dois layouts de uso geral na cobrança registrada:

Layout Tamanho do registro Uso típico Status
CNAB 240 240 posições Padrão FEBRABAN mais recente, estrutura em lotes. ✅
CNAB 400 400 posições Layout legado por banco, ainda muito usado. ✅
CNAB 444 444 posições Variante do Itaú — o 400 acrescido de 44 posições de mensagem. 🚧

CNAB 444 ainda não é implementado

O 444 é uma variante do Itaú: o registro de 400 posições acrescido de 44 para mensagem no boleto. Não há suporte na PyCobrança — nem na remessa, nem no retorno. Está registrado no roadmap e entra sob o mesmo critério dos demais layouts: manual oficial com exemplo numérico validável, e comparação byte a byte contra vetor de referência.

Status: remessa CNAB 400 implementada para 14 bancos (Itaú, Bradesco, Banco do Brasil, Santander, Sicoob, Unicred, Banrisul, Banco do Nordeste, BRB/DCB, Citibank, CrediSIS, C6, Inter e Safra) e CNAB 240 para 7 bancos (Ailos, Banco do Brasil, Caixa, Santander, Sicoob, Sicredi e Unicred). As fixtures ficam congeladas em tests/fixtures/; o que cada uma prova — paridade, manual ou invariante do formato — está em docs/bancos/.

Bolepix na remessa: com um PagamentoPix, a remessa acrescenta o registro tipo 8 (CNAB 400: Itaú, Bradesco, C6, Santander) ou o segmento Y-03 (CNAB 240: Banco do Brasil, Caixa, Sicoob) após cada título — classes Remessa*Pix, validadas byte a byte.

Conceitos

  • Remessa: arquivo gerado pela empresa e enviado ao banco solicitando registro/baixa/alteração de títulos. Fluxo: Boleto[] → registros → string do arquivo.
  • Retorno: arquivo devolvido pelo banco com o resultado do processamento (liquidação, rejeição, baixa). Fluxo: string do arquivo → registros → list[dict].

Estrutura do subsistema

cnab/
├── pagamento.py          # título a registrar (campos + formatadores CNAB)
├── formatacao.py         # format_size / format_valor / remover_acentos (fiéis ao Ruby)
│                         # + campo_numerico / confere_tamanhos (recusam o que não cabe)
├── cnab400/
│   ├── base.py           # header/detalhe/trailer + gera_arquivo (400 posições)
│   └── <banco>.py        # RemessaXxx400 (14 bancos)
├── cnab240/
│   ├── base.py           # header arq/lote + segmentos P/Q/R + trailers (240 posições)
│   └── <banco>.py        # RemessaXxx240 (7 bancos)
└── retorno/
    ├── base.py           # RegistroRetorno + extração posicional + motivo
    ├── cnab400.py        # mapas por banco (header = código nas pos. 76–78)
    ├── cnab240.py        # combinação dos segmentos T/U (header = código nas pos. 0–2)
    └── ocorrencias.py    # rótulos legíveis dos códigos de ocorrência (convenience)

Retorno — status

16 dos 19 bancos têm layout de retorno próprio — 14 em 400 e 5 em 240, com sobreposição. CNAB 400: Itaú, Bradesco, Banco do Brasil, Santander (com campos PIX), Banco do Nordeste, Banrisul, CrediSIS, C6, Unicred, BRB, Inter, Safra, Sicredi e Sicoob. CNAB 240: base/Caixa, Santander, Ailos, Sicredi e Sicoob (dois segmentos, T = dados gerais e U = valores, combinados em um registro). Ficam sem layout próprio Banestes (021), HSBC (399) e Citibank (745) — o parser os lê com o layout de reserva e avisa (ver abaixo).

Procedência, sem arredondar. Onze arquivos .RET foram validados campo a campo contra vetores de referência; três são arquivos reais de banco (fixtures/retorno/externos); e os de Inter, Safra e Sicoob 400 foram montados a partir do manual/planilha do banco — provam o mapeamento das posições, não o que o banco emite.

Retorno.ler aceita caminho, bytes ou objeto com .read(), e detecta o layout pelo tamanho do registro e o banco pelo header.

Layouts declarativos

Cada registro é descrito por uma tabela de campos posicionais (início, fim, tipo, formato, default). Isso permite testar posições isoladamente e reusar definições entre bancos.

# Exemplo conceitual de definição posicional (layout declarativo)
HEADER_ARQUIVO_240 = [
    Campo("banco", 1, 3, tipo="num"),
    Campo("lote", 4, 7, tipo="num", default="0000"),
    Campo("tipo_registro", 8, 8, tipo="num", default="0"),
    # ...
    Campo("nome_empresa", 73, 102, tipo="alfa"),
    # ...
]

Estrutura CNAB 240 (em lotes)

Header de Arquivo        (registro 0)
  Header de Lote         (registro 1)
    Detalhe Segmento P   (registro 3) ─┐ título
    Detalhe Segmento Q   (registro 3) ─┤ sacado
    Detalhe Segmento R   (registro 3) ─┘ multa/desconto/PIX
  Trailer de Lote        (registro 5)
Trailer de Arquivo       (registro 9)

Estrutura CNAB 400 (por registro)

Header       (tipo 0)
Detalhe      (tipo 1)  — um por título
...
Trailer      (tipo 9)  — totais

Remessa — API

Não há uma classe genérica: cada banco e layout tem a sua, porque as posições e as regras mudam de um para o outro. O nome segue Remessa<Banco><Layout>, com o sufixo Pix na variante que inclui o segmento de Bolepix.

from datetime import date

from pycobranca.cnab import Pagamento, RemessaItau400

pagamentos = [
    Pagamento(
        nosso_numero="12345678",
        numero="DOC0001",
        valor=199.90,
        data_vencimento=date(2026, 8, 15),
        data_emissao=date(2026, 7, 23),
        documento_sacado="52998224725",
        nome_sacado="Cliente Final da Silva",
        endereco_sacado="Rua das Flores, 100",
        bairro_sacado="Centro",
        cep_sacado="30110000",
        cidade_sacado="Belo Horizonte",
        uf_sacado="MG",
    ),
]

remessa = RemessaItau400(
    empresa_mae="Empresa Exemplo LTDA",
    documento_cedente="11222333000181",
    agencia="0057",
    conta_corrente="12345",
    digito_conta="7",
    carteira="109",
    data_geracao=date(2026, 7, 23),
    pagamentos=pagamentos,
)

arquivo = remessa.gera_arquivo()  # -> str, com os registros separados por \n

gera_arquivo() devolve o conteúdo; gravar em disco é responsabilidade de quem chama — a biblioteca não escreve arquivos.

Para validar os encargos antes de gerar, chame Pagamento.validar() em cada item (verifica valor > 0 e a coerência de mora, multa e desconto) e Remessa.validar() no conjunto.

As classes disponíveis saem do próprio pacote:

import pycobranca.cnab as cnab

sorted(n for n in dir(cnab) if n.startswith("Remessa"))
# ['RemessaAilos240', 'RemessaBancoBrasil240', 'RemessaBancoBrasil240Pix', ...]

Quais bancos têm cada layout está em 05 — Bancos Suportados. Exemplos executáveis em examples/.

Encargos (juros/mora, multa, desconto, IOF, abatimento)

Cada encargo do Pagamento é um trio código/tipo → valor → data (a data é opcional):

Encargo Código/tipo Valor Data
Multa codigo_multa (0=isento, 1=valor fixo, 2=percentual) percentual_multa (%) data_multa
Juros/Mora tipo_mora (1=valor/dia, 2=taxa mensal %, 3=isento) valor_mora (R$, tipo 1) · percentual_mora (%, tipo 2) data_mora
Desconto 1º/2º/3º cod_desconto / cod_segundo_desconto / cod_terceiro_desconto valor_desconto / valor_segundo_desconto / valor_terceiro_desconto data_desconto / data_segundo_desconto / data_terceiro_desconto
IOF — valor_iof —
Abatimento — valor_abatimento —

Todos os campos são opcionais e têm default neutro ("0"/0.0/None); sem informá-los, o arquivo sai idêntico ao anterior (encargos zerados).

Semântica por layout:

  • CNAB 240 — segmento P: tipo_mora + data de mora + valor/percentual de mora (percentual quando tipo_mora == "2", conforme FEBRABAN "Taxa Mensal") + 1º desconto + IOF + abatimento. Segmento R: 2º e 3º desconto + multa (código + data + percentual). As datas de mora e multa usam o campo informado; na ausência, caem para o vencimento (Caixa e Sicredi/Unicred usam vencimento + 1 dia), como prevê o padrão FEBRABAN.
  • CNAB 400 — a multa é sempre percentual (percentual_multa); a mora é valor ao dia (valor_mora). Larguras variam por banco (ex.: Sicoob 6, Banrisul 12, Brasília/DCB 14). Só o C6 usa data_mora no detalhe; os demais 400 não carregam data de mora.

Observação: no padrão FEBRABAN não existe "valor de multa" monetário — a multa é sempre percentual. O método Pagamento.formata_valor_multa é mantido apenas por compatibilidade e é um alias de formata_percentual_multa.

Suporte por banco

O Pagamento sempre aceita os campos; eles entram no arquivo onde o layout tem posição.

CNAB 240 — suporte completo e uniforme (segmentos P/R):

Banco (CNAB 240) Mora (valor/%) Multa Desc. 1º Desc. 2º Desc. 3º IOF Abat.
Banco do Brasil (001) ✅ ✅ ✅ ✅ ✅ ✅ ✅
Caixa (104) ✅ ✅ ✅ ✅ ✅ ✅ ✅
Santander (033) ✅ ✅ ✅ ✅ ✅ ✅ ✅
Sicoob (756) ✅ ✅ ✅ ✅ ✅ ✅ ✅
Sicredi (748) ✅ ✅ ✅ ✅ ✅ ✅ ✅
Unicred (136) ✅ ✅ ✅ ✅ ✅ ✅ ✅
Ailos (085) ✅ ✅⁴ ✅ ✅ ✅ ✅ ✅

CNAB 400 — varia por layout do banco:

Banco (CNAB 400) Mora Multa Desc. 1º Desc. 2º IOF Abat.
Santander (033) ✅ ✅ ✅ 📅² ✅ ✅
Inter (077) ✅ ✅ ✅ — — —
Bradesco (237) ✅ ✅ ✅ — ✅ ✅
Sicoob (756) ✅ ✅ ✅ — ✅ ✅
Banrisul (041) ✅ ✅ ✅ — ✅ ✅
Banco do Nordeste (004) ✅ ✅ ✅ — ✅ ✅
C6 (336) ✅ ✅ ✅ — — ✅
Unicred (136) ✅ ✅ ✅ — — ✅
CrediSIS (097) ✅ ✅ ✅³ — — —
Itaú (341) ✅ 📝¹ ✅ — ✅ ✅
Banco do Brasil (001) ✅ 📝¹ ✅ — ✅ ✅
Citibank (745) ✅ — ✅ ✅ ✅ ✅
BRB/Brasília (070) ✅ — ✅ — — ✅

Legenda: ✅ campo posicional na remessa · — sem campo no layout · Desc. 3º apenas no CNAB 240. 📝¹ Itaú/BB (400): multa vai por instrução (código), não como percentual posicional. 📅² Santander (400): 2º desconto só a data. ³ CrediSIS: 1º desconto sem campo de data. ⁴ Ailos (240): segmento R (multa + 2º/3º desconto) emitido só quando há multa. Banestes (021), HSBC (399) e Safra (422) só emitem boleto (sem remessa CNAB).

O Inter (077) aceita multa e juros em valor fixo ou percentual, escolhidos por um código próprio, e desconto em valor fixo ou percentual do nominal — cada um com data limite obrigatória. Não tem campo de IOF nem de abatimento no layout, e não tem CNAB 240 de cobrança: o manual oferece CNAB 400 ou API, e o CNAB 240 que o banco publica é de pagamentos.

Códigos de juros do Inter divergem da FEBRABAN

A biblioteca segue a FEBRABAN, onde tipo_mora="3" é isento; o Inter usa "0" para sem juros. A tradução é automática — quem informa tipo_mora não precisa saber disso. Sem ela, o valor padrão sairia no arquivo como um código que o banco não define.

Retorno — API

from pycobranca.cnab.retorno import Retorno

retorno = Retorno.ler("retorno.ret")  # layout e banco detectados automaticamente
for registro in retorno.registros:
    print(
        registro.nosso_numero,
        registro.codigo_ocorrencia,
        retorno.descricao_ocorrencia(registro),
        registro.valor_recebido,
        registro.data_credito,
    )

# Serialização JSON-friendly para conciliação
dados = retorno.to_dict()  # list[dict], nulos removidos

Validação: Retorno.ler levanta RetornoInvalido para um arquivo vazio ou sem header de banco reconhecível (em vez de devolver lista vazia). Ver contrato de erros.

Retorno.ler aceita caminho, bytes ou objeto com .read() — um serviço que recebe o .RET por upload não precisa passar por arquivo temporário:

Retorno.ler("arquivo.ret")  # caminho
Retorno.ler(upload.read())  # bytes
Retorno.ler(open("a.ret", "rb"))  # file-like

A decodificação é Latin-1, que mapeia os 256 bytes e por isso nunca substitui caractere: o CNAB é posicional, e um byte perdido deslocaria todas as posições seguintes do registro.

Os valores são devolvidos como strings cruas do arquivo (ex.: valor_recebido "0000000003790" = R$ 37,90; datas no formato do banco), preservando fidelidade ao retorno; a interpretação numérica/monetária fica a cargo do consumidor (aplicação).

Quando o banco não tem layout próprio

Cada banco põe os campos onde quer. Para os que ainda não têm mapa aqui, o parser recorre a um layout genérico — o arquivo é lido até o fim, nenhum erro é levantado, e os campos podem sair de posições que não são as daquele banco.

É a forma de falha mais perigosa do parsing, porque a saída é plausível. Dois casos medidos: o Inter grava a ocorrência em 90–91 onde o genérico lê 109–110; o Sicredi usa quinze posições de nosso número a partir da 48, e o genérico lia a 63–70 — que ali é filler — devolvendo 00000000, zeros com toda a aparência de número válido.

Por isso a leitura com layout genérico avisa:

import warnings
from pycobranca.exceptions import LayoutGenerico

# só para saber
with warnings.catch_warnings(record=True) as avisos:
    warnings.simplefilter("always", LayoutGenerico)
    retorno = Retorno.ler("arquivo.ret")

# ou, em processamento em lote, falhar em vez de seguir adiante
warnings.simplefilter("error", LayoutGenerico)

O aviso não muda o comportamento — arquivos que eram lidos continuam sendo. Ele é o sinal que faltava. LayoutGenerico herda de UserWarning, não de PyCobrancaError: avisos e erros são hierarquias separadas em Python, e except PyCobrancaError não captura avisos.

Quando o layout não tem o campo que você informou

carteira existe em toda remessa porque está na base — mas oito layouts não têm esse campo: a CrediSIS e o Santander no 400, e seis dos sete 240, onde a FEBRABAN separa o código da carteira (posição 58 do segmento P, 1 a 4) da modalidade do banco.

Nesses, informar carteira não faz nada. O arquivo sai correto — com a carteira do padrão, não com a que você pediu. Quem monta a remessa reaproveitando o dicionário do boleto, que é o caminho natural, acreditava ter escolhido a carteira.

import warnings
from pycobranca.exceptions import CampoIgnorado

warnings.simplefilter("error", CampoIgnorado)  # falhar em vez de seguir adiante
CampoIgnorado: RemessaSicoob240: `carteira` não é gravada neste layout
(use 'modalidade_carteira'). O arquivo sai correto, mas sem a carteira que você informou.

O campo que cada layout grava no lugar está em campo_de_carteira. Como o LayoutGenerico, este aviso não muda o comportamento e herda de UserWarning, não de PyCobrancaError.

Um arquivo, uma conta

Cada instância de Remessa* representa um arquivo para uma conta: banco, layout, convênio, carteira e conta vêm do construtor, e os pagamentos são os títulos daquela conta. A biblioteca não separa uma lista heterogênea em sublotes — quem tem títulos de contas ou bancos diferentes agrupa antes e instancia uma remessa por grupo.

from itertools import groupby


def chave(pagamento):
    return (pagamento.banco, pagamento.conta)  # e convênio, carteira… conforme o seu modelo


for (banco, conta), titulos in groupby(sorted(pagamentos, key=chave), key=chave):
    remessa = REMESSA_POR_BANCO[banco](
        conta_corrente=conta,
        pagamentos=list(titulos),
        **dados_do_cedente,
    )
    arquivos.append(remessa.gera_arquivo())

O que a biblioteca garante por conta própria: os totais de header/trailer são calculados a partir dos registros efetivamente montados, e a conferência de tamanho recusa o arquivo malformado antes de devolvê-lo. Custo de gera_arquivo() em lote e as demais fronteiras do hospedeiro estão em 19 — Integração.

Mapeamento de ocorrências

O parsing traduz códigos numéricos de ocorrência/motivo para rótulos legíveis (ex.: 06 → "Liquidação normal", 09 → "Baixa"). O mapa é declarado por banco/layout, pois os códigos variam.

Estratégia de testes

  • Fixtures de referência: arquivos de remessa/retorno de referência viram fixtures de teste.
  • Comparação byte-a-byte na remessa para os bancos prioritários.
  • Round-trip onde aplicável: gerar remessa, reprocessar posições e conferir campos.
  • Casos de retorno reais (anonimizados) para validar o mapeamento de ocorrências.

Ver 08 — Testes e Qualidade.