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á emdocs/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 — classesRemessa*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
.RETforam 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.leraceita caminho,bytesou 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)¶
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 quandotipo_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 usadata_morano 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 deformata_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.lerlevantaRetornoInvalidopara 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.