diff --git a/.gitignore b/.gitignore index 93c111b..b477ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ venv .cache .pytest_cache +.settings \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..0ec7c7d --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + boleto + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..ad74947 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,8 @@ + + + +/${PROJECT_DIR_NAME} + +python interpreter +Default + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..9480eef --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,13 @@ +eclipse.preferences.version=1 +encoding//build/lib/pyboleto/bank/bs2.py=utf-8 +encoding//pyboleto/bank/__init__.py=utf-8 +encoding//pyboleto/bank/bancodobrasil.py=utf-8 +encoding//pyboleto/bank/banrisul.py=utf-8 +encoding//pyboleto/bank/bs2.py=utf-8 +encoding//pyboleto/bank/itau.py=utf-8 +encoding//pyboleto/bank/sicredi.py=utf-8 +encoding//pyboleto/data.py=utf-8 +encoding//pyboleto/pdf.py=utf-8 +encoding//tests/test_banco_bs2.py=utf-8 +encoding//tests/testutils.py=utf-8 +encoding/setup.py=utf-8 diff --git a/README.rst b/README.rst index 6d39b05..ee585f0 100644 --- a/README.rst +++ b/README.rst @@ -65,6 +65,8 @@ Por enquanto, são essas que temos. +----------------------+----------------+-----------------+------------+ | **Cecred** | 1 | Yes | Yes | +----------------------+----------------+-----------------+------------+ + | **BS2** | 1 | Yes | Yes | + +----------------------+----------------+-----------------+------------+ .. _pyboleto-docs: diff --git a/pyboleto/bank/__init__.py b/pyboleto/bank/__init__.py index 48dc5e1..66c18e1 100644 --- a/pyboleto/bank/__init__.py +++ b/pyboleto/bank/__init__.py @@ -2,13 +2,14 @@ from ..data import BoletoException BANCOS_IMPLEMENTADOS = { '001': 'bancodobrasil.BoletoBB', + '033': 'santander.BoletoSantander', '041': 'banrisul.BoletoBanrisul', - '237': 'bradesco.BoletoBradesco', '104': 'caixa.BoletoCaixa', - '399': 'hsbc.BoletoHsbc', + '218': 'bs2.BoletoBs2', + '237': 'bradesco.BoletoBradesco', '341': 'itau.BoletoItau', '356': 'real.BoletoReal', - '033': 'santander.BoletoSantander', + '399': 'hsbc.BoletoHsbc', '748': 'sicredi.BoletoSicredi', '756': 'sicoob.BoletoSicoob', '0851': 'cecred.BoletoCecred', diff --git a/pyboleto/bank/bs2.py b/pyboleto/bank/bs2.py new file mode 100644 index 0000000..4e9aa10 --- /dev/null +++ b/pyboleto/bank/bs2.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +import datetime +from pyboleto.data import BoletoData, CustomProperty + + +class BoletoBs2(BoletoData): + '''Implementa Boleto bs2 + + Gera Dados necessários para criação de boleto para o banco Bs2 + + ''' + + # Nosso numero (sem dv) com 8 digitos + nosso_numero = "9%s" % CustomProperty('nosso_numero', 9) + # Conta (sem dv) com 5 digitos + conta_cedente = CustomProperty('conta_cedente', 5) + # Agência (sem dv) com 4 digitos + agencia_cedente = CustomProperty('agencia_cedente', 4) + + carteira = 21 + + data_limite = False + + def __init__(self): + super(BoletoBs2, self).__init__() + + self.codigo_banco = "218" + self.logo_image = "logo_bs2.png" + self.especie_documento = 'Outro' + self.local_pagamento = 'Pagável em qualquer banco até a data limite de %s' + + @property + def dv_nosso_numero(self): + return self.modulo11("9"+self.nosso_numero.zfill(9)) + + @property + def dv_agencia_conta_cedente(self): + agencia_conta = "%s%s" % (self.agencia_cedente, self.conta_cedente) + return self.modulo11(agencia_conta) + + @property + def agencia_conta_cedente(self): + return "%s/%s-%s" % (self.agencia_cedente, self.conta_cedente, + self.conta_cedente_dv) + + def format_nosso_numero(self): + return "%9s%1s" % ("9"+self.nosso_numero.zfill(9),self.dv_nosso_numero) + + @property + def campo_livre(self): + content = "%3s%9s%1s%10s%1s8" % (self.agencia_cedente.zfill(4)[-3:], + self.conta_cedente.zfill(9), + self.conta_cedente_dv, + '9'+self.nosso_numero.zfill(9), + self.dv_nosso_numero) + return content diff --git a/pyboleto/data.py b/pyboleto/data.py index 1b1b0bd..40c1739 100644 --- a/pyboleto/data.py +++ b/pyboleto/data.py @@ -100,6 +100,7 @@ class BoletoData(object): :param cedente_documento: CPF ou CNPJ do Cedente. :param conta_cedente: Conta do Cedente sem o dígito verificador. :param data_documento: + :param data_limite: :type data_documento: `datetime.date` :param data_processamento: :type data_processamento: `datetime.date` @@ -129,6 +130,7 @@ class BoletoData(object): def __init__(self, **kwargs): # otherwise the printed value might diffent from the value in # the barcode. + self.data_limite = kwargs.pop('data_limite', "") self.aceite = kwargs.pop('aceite', "N") self.agencia_cedente = kwargs.pop('agencia_cedente', "") self.carteira = kwargs.pop('carteira', "") @@ -143,13 +145,11 @@ def __init__(self, **kwargs): self.conta_cedente = kwargs.pop('conta_cedente', "") self.conta_cedente_dv = kwargs.pop('conta_cedente_dv', "") self.data_documento = kwargs.pop('data_documento', "") - self.data_processamento = kwargs.pop('data_processamento', - datetime.date.today()) + 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.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', "") @@ -161,6 +161,7 @@ def __init__(self, **kwargs): self.sacado_endereco = kwargs.pop('sacado_endereco', "") self.sacado_bairro = kwargs.pop('sacado_bairro', "") self.sacado_cep = kwargs.pop('sacado_cep', "") + self.data_limite = kwargs.pop('data_limite', "") if kwargs: raise TypeError("Paramêtro(s) desconhecido: %r" % (kwargs, )) self._cedente_endereco = None diff --git a/pyboleto/media/logo_bs2.png b/pyboleto/media/logo_bs2.png new file mode 100644 index 0000000..6703957 Binary files /dev/null and b/pyboleto/media/logo_bs2.png differ diff --git a/pyboleto/pdf.py b/pyboleto/pdf.py index c6fdc37..9f6089c 100644 --- a/pyboleto/pdf.py +++ b/pyboleto/pdf.py @@ -253,6 +253,12 @@ def _drawReciboSacado(self, boleto_dados, x, y): self.pdf_canvas.setFont('Helvetica', 6) self.delta_title = self.height_line - (6 + 1) + self.pdf_canvas.drawString( + 0, + self.height_line, + 'Sacador / Avalista' + ) + self.pdf_canvas.drawRightString( self.width, self.height_line, @@ -387,6 +393,14 @@ def _drawReciboSacado(self, boleto_dados, x, y): valor_documento ) + if boleto_dados.sacador_documento and boleto_dados.sacador_nome: + self.pdf_canvas.setFont('Helvetica', 6) + self.pdf_canvas.drawString( + 0, + self.height_line - 8, + boleto_dados.sacador_documento + ' - ' + boleto_dados.sacador_nome + ) + self.pdf_canvas.setFont('Courier', 9) demonstrativo = boleto_dados.demonstrativo[0:25] for i in range(len(demonstrativo)): @@ -440,7 +454,7 @@ def _drawReciboCaixa(self, boleto_dados, x, y): y = 1.5 * self.height_line self.pdf_canvas.drawRightString( self.width, - (1.5 * self.height_line) + self.delta_title - 1, + y + self.delta_title - 1, 'Autenticação Mecânica / Ficha de Compensação' ) @@ -452,9 +466,19 @@ def _drawReciboCaixa(self, boleto_dados, x, y): self.width - (45 * mm) + self.space, y + self.space, 'Código de baixa' ) + self.pdf_canvas.drawString(0, y + self.space, 'Sacador / Avalista') + if boleto_dados.sacador_documento and boleto_dados.sacador_nome: + self.pdf_canvas.setFont('Helvetica-Bold', self.font_size_title-0.3) + self.pdf_canvas.drawString( + (20 * mm), + y + self.space, + boleto_dados.sacador_documento + ' / ' + boleto_dados.sacador_nome + ) - y += self.height_line + self.pdf_canvas.setFont('Helvetica', self.font_size_title) + + y += self.height_line + 7 self.pdf_canvas.drawString(0, y + self.delta_title, 'Pagador') sacado = boleto_dados.sacado @@ -515,7 +539,7 @@ def _drawReciboCaixa(self, boleto_dados, x, y): self.pdf_canvas.drawString( 0, y + self.delta_title, - 'Instruções' + 'Informações de Responsabilidade do Beneficiário' ) self.pdf_canvas.setFont('Helvetica', self.font_size_value) @@ -526,6 +550,26 @@ def _drawReciboCaixa(self, boleto_dados, x, y): y - (i * self.delta_font), instrucoes[i] ) + + ln = 6 + if hasattr(boleto_dados, 'tx_multa'): + mt_txt = "Multa para pagamento após o vencimento: {:.2f}%.".format(boleto_dados.tx_multa) + self.pdf_canvas.drawString( + 2 * self.space, + y - (ln * self.delta_font), + mt_txt + ) + ln += 1 + + if hasattr(boleto_dados, 'tx_juros'): + jr_txt = "Juros para pagamento após o vencimento: {:.2f}% ao mês.".format(boleto_dados.tx_juros) + self.pdf_canvas.drawString( + 2 * self.space, + y - (ln * self.delta_font), + jr_txt + ) + + self.pdf_canvas.setFont('Helvetica', self.font_size_title) # Linha horizontal com primeiro campo Uso do Banco @@ -817,7 +861,7 @@ def drawBoleto(self, boleto_dados): y = 10 * mm # margem inferior self._drawHorizontalCorteLine(x, y, self.width) - y += 4 * mm # distancia entre linha de corte e barcode + y += 3 * mm # distancia entre linha de corte e barcode d = self._drawReciboCaixa(boleto_dados, x, y) y += d[1] + (12 * mm) # distancia entre Recibo caixa e linha de corte diff --git a/tests/test_banco_bs2.py b/tests/test_banco_bs2.py new file mode 100644 index 0000000..81b5a73 --- /dev/null +++ b/tests/test_banco_bs2.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +import unittest +import datetime +import difflib +import fnmatch +import os +import re +import subprocess +import tempfile +from xml.etree.ElementTree import fromstring, tostring + +from pyboleto.bank.bs2 import BoletoBs2 +import pyboleto +from pyboleto.pdf import BoletoPDF +from pyboleto.html import BoletoHTML + +def indent(elem, level=0): + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +def pdftoxml(filename, output): + p = subprocess.Popen(['pdftohtml', + '-stdout', + '-xml', + '-noframes', + '-i', + '-q', + filename], + stdout=subprocess.PIPE) + stdout, stderr = p.communicate() + if stderr: + raise SystemExit("Error while runnig pdftohtml: %s" % (stderr, )) + + root = fromstring(stdout) + indent(root) + with open(output, 'wb') as f: + f.write(tostring(root)) + +def _diff(orig, new, short, verbose): + lines = difflib.unified_diff(orig, new) + if not lines: + return '' + + return ''.join('%s: %s' % (short, line) for line in lines) + +def diff_files(orig, new, verbose=False): + with open(orig) as f_orig: + with open(new) as f_new: + return _diff(f_orig.readlines(), + f_new.readlines(), + short=os.path.basename(orig), + verbose=verbose) + +def _get_expected(bank, generated, f_type='xml'): + fname = os.path.join(os.path.dirname(pyboleto.__file__),"..", "tests", f_type, bank + '-expected.' + f_type) + if not os.path.exists(fname): + with open(fname, 'wb') as f: + with open(generated) as g: + f.write(g.read()) + return fname + +def diff_pdf_htmls(original_filename, filename): + # REPLACE all generated dates with %%DATE%% + for fname in [original_filename, filename]: + with open(fname) as f: + data = f.read() + data = re.sub(r'name="date" content="(.*)"', + r'name="date" content="%%DATE%%"', data) + data = re.sub(r']+>', r'', data) + with open(fname, 'w') as f: + f.write(data) + + return diff_files(original_filename, filename) + +dados = [] +for i in range(3): + d = BoletoBs2() + d.carteira = '21' + d.agencia_cedente = '001' + d.conta_cedente = '892700' + d.conta_cedente_dv = '6' + d.cedente = "M&D Com. E Man. DE Equip. para Inform. Ltda" + d.cedente_cidade = "Fazenda Rio Grande" + d.cedente_uf = "PR" + d.cedente_logradouro = "Rua Aruba" + d.cedente_bairro = "96" + d.cedente_cep = "88888-888" + d.cedente_documento = "123" + d.data_vencimento = datetime.date(2021, 6, 30) + d.data_documento = datetime.date(2021, 5, 25) + d.data_processamento = datetime.date(2021, 5, 20) + #d.self.data_limite = datetime.date(2009, 11, 19) + d.valor_documento = 123.45 + d.nosso_numero = str(2399) + d.numero_documento = str(456) + d.sacador_documento = "" + d.sacador_nome = "" + d.instrucoes = ['Após o vencimento cobrar R$ 1,00 multa e ', 'R$ 0,25 ao dia juros por atraso'] + dados.append(d) + +dados = dados[0] +bank = type(dados).__name__ +filename = tempfile.mktemp(prefix="pyboleto-", suffix=".pdf") +boleto = BoletoPDF(filename, True) +boleto.drawBoleto(dados) +boleto.nextPage() +boleto.save() + +generated = filename + '.xml' +pdftoxml(filename, generated) +# expected = _get_expected(bank, generated) +# diff = diff_pdf_htmls(expected, generated) +# os.unlink(generated) +# os.unlink(filename) +# if diff: +# print("Error while checking xml for %r:\n%s" % (bank, diff)) diff --git a/tests/xml/BoletoBs2-expected.xml b/tests/xml/BoletoBs2-expected.xml new file mode 100644 index 0000000..e69de29