# -*- coding: utf-8 -*-
"""
pyboleto.data
~~~~~~~~~~~~~
Base para criação dos módulos dos bancos. Comtém funções genéricas
relacionadas a geração dos dados necessários para o boleto bancário.
:copyright: © 2011 - 2012 by Eduardo Cereto Carvalho
:license: BSD, see LICENSE for more details.
"""
import datetime
from decimal import Decimal
[documentos]class BoletoException(Exception):
""" Exceções para erros no pyboleto"""
def __init__(self, message):
Exception.__init__(self, message)
_EPOCH = datetime.date(1997, 10, 7)
[documentos]class CustomProperty(object):
"""Função para criar propriedades nos boletos
Cria propriedades com getter, setter e delattr.
Propriedades criadas com essa função sempre são strings internamente.
O Setter sempre tentará remover qualquer digito verificador se existir.
Aceita um numero com ou sem DV e remove o DV caso exista. Então preenxe
com zfill até o tamanho adequado. Note que sempre que possível não use DVs
ao entrar valores no pyboleto. De preferência o pyboleto vai calcular
todos os DVs quando necessário.
:param name: O nome da propriedade.
:type name: string
:param length: Tamanho para preencher com '0' na frente.
:type length: integer
"""
def __init__(self, name, length):
self.name = name
self.length = length
self._instance_state = {}
def __set__(self, instance, value):
if instance is None:
raise TypeError("can't modify custom class properties")
if '-' in value:
values = value.split('-')
values[0] = values[0].zfill(self.length)
value = '-'.join(values)
else:
value = value.zfill(self.length)
self._instance_state[instance] = value
def __get__(self, instance, class_):
if instance is None:
return self
return self._instance_state.get(instance, '0' * self.length)
[documentos]class BoletoData(object):
"""Interface para implementações específicas de bancos
Esta classe geralmente nunca será usada diretamente. Geralmente o usuário
irá usar uma das subclasses com a implementação específica de cada banco.
As classes dentro do pacote :mod:`pyboleto.bank` extendem essa classe
para implementar as especificações de cada banco.
Portanto as especificações dentro desta classe são genéricas seguindo as
normas da FEBRABAN.
Todos os parâmetros devem ser passados como ``**kwargs`` para o construtor
ou então devem ser passados depois, porém antes de imprimir o boleto.
eg::
bData = BoletoData(agencia='123', valor='650')
bData.cedente = u'João Ninguém'
bData.cedente_cidade = u'Rio de Janeiro'
bData.cedente_uf = u'RJ'
# Assim por diante até preencher todos os campos obrigatórios.
**Parâmetros obrigatórios**:
:param aceite: 'N' para o caixa não acetitar o boleto após a
validade ou 'A' para aceitar. *(default: 'N')*
:param agencia_cedente: Tamanho pode variar com o banco.
:param carteira: Depende do Banco.
:param cedente: Nome do Cedente
:param cedente_cidade:
:param cedente_uf:
:param cedente_logradouro: Endereço do Cedente
:param cedente_bairro:
:param cedente_cep:
:param cedente_documento: CPF ou CNPJ do Cedente.
:param conta_cedente: Conta do Cedente sem o dígito verificador.
:param data_documento:
:type data_documento: `datetime.date`
:param data_processamento:
:type data_processamento: `datetime.date`
:param data_vencimento:
:type data_vencimento: `datetime.date`
:param numero_documento: Número Customizado para controle. Pode ter até 13
caracteres dependendo do banco.
:param sacado_nome: Nome do Sacado
:param sacado_documento: CPF ou CNPJ do Sacado
:param sacado_cidade:
:param sacado_uf:
:param sacado_endereco: Endereco do Sacado
:param sacado_bairro:
:param sacado_cep:
**Parâmetros não obrigatórios**:
:param quantidade:
:param especie: Nunca precisa mudar essa opção *(default: 'R$')*
:param especie_documento:
:param local_pagamento: *(default: 'Pagável em qualquer banco
até o vencimento')*
:param moeda: Nunca precisa mudar essa opção *(default: '9')*
"""
def __init__(self, **kwargs):
# otherwise the printed value might diffent from the value in
# the barcode.
self.aceite = kwargs.pop('aceite', "N")
self.agencia_cedente = kwargs.pop('agencia_cedente', "")
self.carteira = kwargs.pop('carteira', "")
self.cedente = kwargs.pop('cedente', "")
self.cedente_cidade = kwargs.pop('cedente_cidade', "")
self.cedente_uf = kwargs.pop('cedente_uf', "")
self.cedente_logradouro = kwargs.pop('cedente_logradouro', "")
self.cedente_bairro = kwargs.pop('cedente_bairro', "")
self.cedente_cep = kwargs.pop('cedente_cep', "")
self.cedente_documento = kwargs.pop('cedente_documento', "")
self.codigo_banco = kwargs.pop('codigo_banco', "")
self.conta_cedente = kwargs.pop('conta_cedente', "")
self.data_documento = kwargs.pop('data_documento', "")
self.data_processamento = kwargs.pop('data_processamento',
datetime.date.today())
self.data_vencimento = kwargs.pop('data_vencimento', "")
self.especie = kwargs.pop('especie', "R$")
self.especie_documento = kwargs.pop('especie_documento', "")
self.local_pagamento = kwargs.pop(
'local_pagamento', "Pagável em qualquer banco até o vencimento")
self.logo_image = kwargs.pop('logo_image', "")
self.moeda = kwargs.pop('moeda', "9")
self.numero_documento = kwargs.pop('numero_do_documento', "")
self.quantidade = kwargs.pop('quantidade', "")
self.sacado_nome = kwargs.pop('sacado_nome', "")
self.sacado_documento = kwargs.pop('sacado_documento', "")
self.sacado_cidade = kwargs.pop('sacado_cidade', "")
self.sacado_uf = kwargs.pop('sacado_uf', "")
self.sacado_endereco = kwargs.pop('sacado_endereco', "")
self.sacado_bairro = kwargs.pop('sacado_bairro', "")
self.sacado_cep = kwargs.pop('sacado_cep', "")
if kwargs:
raise TypeError("Paramêtro(s) desconhecido: %r" % (kwargs, ))
self._cedente_endereco = None
self._demonstrativo = []
self._instrucoes = []
self._sacado = None
self._valor = None
self._valor_documento = None
@property
def barcode(self):
"""Essa função sempre é a mesma para todos os bancos. Então basta
implementar o método :func:`barcode` para o pyboleto calcular a linha
digitável.
Posição # Conteúdo
01 a 03 03 Número do banco
04 01 Código da Moeda - 9 para Real
05 01 Digito verificador do Código de Barras
06 a 09 04 Data de vencimento em dias partis de 07/10/1997
10 a 19 10 Valor do boleto (8 inteiros e 2 decimais)
20 a 44 25 Campo Livre definido por cada banco
Total 44
"""
for attr, length, data_type in [
('codigo_banco', 3, str),
('moeda', 1, str),
('data_vencimento', None, datetime.date),
('valor_documento', -1, str),
('campo_livre', 25, str)]:
value = getattr(self, attr)
if not isinstance(value, data_type):
raise TypeError("%s.%s must be a %s, got %r (type %s)" % (
self.__class__.__name__, attr, data_type.__name__, value,
type(value).__name__))
if (data_type == str and
length != -1 and
len(value) != length):
raise ValueError(
"%s.%s must have a length of %d, not %r (len: %d)" %
(self.__class__.__name__,
attr,
length,
value,
len(value)))
due_date_days = (self.data_vencimento - _EPOCH).days
if not (9999 >= due_date_days >= 0):
raise TypeError(
"Invalid date, must be between 1997/07/01 and "
"2024/11/15")
num = "%s%1s%04d%010d%24s" % (self.codigo_banco,
self.moeda,
due_date_days,
Decimal(self.valor_documento) * 100,
self.campo_livre)
dv = self.calculate_dv_barcode(num)
barcode = num[:4] + str(dv) + num[4:]
if len(barcode) != 44:
raise BoletoException(
'The barcode must have 44 characteres, found %d' %
len(barcode))
return barcode
@property
def campo_livre(self):
"""Must be overriden by child class property
:exception NotImplementedError: Needs to be implemented by derived
class
"""
raise NotImplementedError(
'This method has not been implemented by this class'
)
[documentos] def calculate_dv_barcode(self, line):
"""Calcula DV para código de barras
Está é uma implementação genérica mas pode ser reimplementada pela
classe derivada dependendo das definições de cada bancoGeralmente
é implementado pela classe derivada.
"""
resto2 = self.modulo11(line, 9, 1)
if resto2 in [0, 1, 10]:
dv = 1
else:
dv = 11 - resto2
return dv
nosso_numero = CustomProperty('nosso_numero', 13)
"""Nosso Número geralmente tem 13 posições
Algumas subclasses podem alterar isso dependendo das normas do banco
"""
agencia_cedente = CustomProperty('agencia_cedente', 4)
"""Agência do Cedente geralmente tem 4 posições
Algumas subclasses podem alterar isso dependendo das normas do banco
"""
conta_cedente = CustomProperty('conta_cedente', 7)
"""Conta do Cedente geralmente tem 7 posições
Algumas subclasses podem alterar isso dependendo das normas do banco
"""
def _cedente_endereco_get(self):
if self._cedente_endereco is None:
self._cedente_endereco = '%s - %s - %s - %s - %s' % (
self.cedente_logradouro,
self.cedente_bairro,
self.cedente_cidade,
self.cedente_uf,
self.cedente_cep
)
return self._cedente_endereco
def _cedente_endereco_set(self, endereco):
if len(endereco) > 80:
raise BoletoException(
u'Linha de endereço possui mais que 80 caracteres')
self._cedente_endereco = endereco
cedente_endereco = property(_cedente_endereco_get, _cedente_endereco_set)
"""Endereço do Cedente com no máximo 80 caracteres"""
def _get_valor(self):
if self._valor is not None:
return "%.2f" % self._valor
def _set_valor(self, val):
if type(val) is Decimal:
self._valor = val
else:
self._valor = Decimal(str(val))
valor = property(_get_valor, _set_valor)
"""Valor convertido para :class:`Decimal`.
Geralmente valor e valor_documento são o mesmo número.
:type: Decimal
"""
def _get_valor_documento(self):
if self._valor_documento is not None:
return "%.2f" % self._valor_documento
def _set_valor_documento(self, val):
if type(val) is Decimal:
self._valor_documento = val
else:
self._valor_documento = Decimal(str(val))
valor_documento = property(_get_valor_documento, _set_valor_documento)
"""Valor do Documento convertido para :class:`Decimal`.
De preferência para passar um valor em :class:`Decimal`, se não for passado
outro tipo será feito um cast para :class:`Decimal`.
"""
def _instrucoes_get(self):
return self._instrucoes
def _instrucoes_set(self, list_inst):
if isinstance(list_inst, str):
list_inst = list_inst.splitlines()
if len(list_inst) > 7:
raise BoletoException(
u'Número de linhas de instruções maior que 7')
for line in list_inst:
if len(line) > 90:
raise BoletoException(
u'Linha de instruções possui mais que 90 caracteres')
self._instrucoes = list_inst
instrucoes = property(_instrucoes_get, _instrucoes_set)
"""Instruções para o caixa do banco que recebe o bilhete
Máximo de 7 linhas com 90 caracteres cada.
Geralmente contém instruções para aplicar multa ou não aceitar caso tenha
passado a data de validade.
"""
def _demonstrativo_get(self):
return self._demonstrativo
def _demonstrativo_set(self, list_dem):
if isinstance(list_dem, str):
list_dem = list_dem.splitlines()
if len(list_dem) > 12:
raise BoletoException(
u'Número de linhas de demonstrativo maior que 12')
for line in list_dem:
if len(line) > 90:
raise BoletoException(
u'Linha de demonstrativo possui mais que 90 caracteres')
self._demonstrativo = list_dem
demonstrativo = property(_demonstrativo_get, _demonstrativo_set)
"""Texto que vai impresso no corpo do Recibo do Sacado
Máximo de 12 linhas com 90 caracteres cada.
"""
def _sacado_get(self):
"""Tenta usar o sacado que foi setado ou constroi um
Se você não especificar um sacado o boleto tentará construir um sacado
a partir de outras proriedades setadas.
Para facilitar você deve sempre setar essa propriedade.
"""
if self._sacado is None:
self.sacado = [
'%s - CPF/CNPJ: %s' % (self.sacado_nome,
self.sacado_documento),
self.sacado_endereco,
'%s - %s - %s - %s' % (
self.sacado_bairro,
self.sacado_cidade,
self.sacado_uf,
self.sacado_cep
)
]
return self._sacado
def _sacado_set(self, list_sacado):
if len(list_sacado) > 3:
raise BoletoException(u'Número de linhas do sacado maior que 3')
self._sacado = list_sacado
sacado = property(_sacado_get, _sacado_set)
"""Campo sacado composto por até 3 linhas.
A primeira linha precisa ser o nome do sacado.
As outras duas linhas devem ser usadas para o endereço do sacado.
"""
@property
def agencia_conta_cedente(self):
return "%s/%s" % (self.agencia_cedente, self.conta_cedente)
@property
def codigo_dv_banco(self):
cod = "%s-%s" % (self.codigo_banco, self.modulo11(self.codigo_banco))
return cod
@property
def linha_digitavel(self):
"""Monta a linha digitável a partir do barcode
Esta é a linha que o cliente pode utilizar para digitar se o código
de barras não estiver legível.
"""
linha = self.barcode
if not linha:
raise BoletoException("Boleto doesn't have a barcode")
def monta_campo(campo):
campo_dv = "%s%s" % (campo, self.modulo10(campo))
return "%s.%s" % (campo_dv[0:5], campo_dv[5:])
return ' '.join([monta_campo(linha[0:4] + linha[19:24]),
monta_campo(linha[24:34]),
monta_campo(linha[34:44]),
linha[4],
linha[5:19]])
@staticmethod
[documentos] def modulo10(num):
if not isinstance(num, str):
raise TypeError
soma = 0
peso = 2
for c in reversed(num):
parcial = int(c) * peso
if parcial > 9:
s = str(parcial)
parcial = int(s[0]) + int(s[1])
soma += parcial
if peso == 2:
peso = 1
else:
peso = 2
resto10 = soma % 10
if resto10 == 0:
modulo10 = 0
else:
modulo10 = 10 - resto10
return modulo10
@staticmethod
[documentos] def modulo11(num, base=9, r=0):
if not isinstance(num, str):
raise TypeError
soma = 0
fator = 2
for c in reversed(num):
soma += int(c) * fator
if fator == base:
fator = 1
fator += 1
if r == 0:
soma = soma * 10
digito = soma % 11
if digito == 10:
digito = 0
return digito
if r == 1:
resto = soma % 11
return resto