# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2011-2017 Async Open Source
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public License
## as published by the Free Software Foundation; either version 2
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
"""Boleto generation code.
"""
import collections
import datetime
import logging
from kiwi.environ import environ
from kiwi.currency import currency
from kiwi.datatypes import converter
from stoqlib.lib.algorithms import (modulo10,
modulo11,
calculaDuploDigito)
from stoqlib.lib.dateutils import localtoday
from stoqlib.lib.cnab.bb import BBCnab
from stoqlib.lib.cnab.bradesco import BradescoCnab
from stoqlib.lib.cnab.caixa import CaixaCnab
from stoqlib.lib.cnab.itau400 import ItauCnab400
from stoqlib.lib.cnab.santander import SantanderCnab
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
log = logging.getLogger(__name__)
(BILL_OPTION_BANK_BRANCH,
BILL_OPTION_BANK_ACCOUNT,
BILL_OPTION_CUSTOM) = range(3)
[docs]class BoletoException(Exception):
pass
def _validate_number(value, min_value=0, max_value=None):
if value == '':
raise BoletoException(_("Value cannot be empty"))
try:
value = int(value)
except ValueError:
raise BoletoException(_("Value must be a number"))
if min_value is not None and value < min_value:
raise BoletoException(
_("Value must be a number greater than {}").format(min_value))
if max_value is not None and value > max_value:
raise BoletoException(
_("Value must be a number lower than {}").format(max_value))
[docs]def custom_property(name, num_length):
"""Function to create properties on boleto
It accepts a number with or without a DV and zerofills it
"""
internal_attr = '_%s' % name
def _set_attr(self, val):
val = val.split('-')
if len(val) is 1:
val[0] = str(val[0]).zfill(num_length)
setattr(self, internal_attr, val[0])
elif len(val) is 2:
val[0] = str(val[0]).zfill(num_length)
setattr(self, internal_attr, '-'.join(val))
else:
raise BoletoException(_('Wrong value format'))
return property(
lambda self: getattr(self, internal_attr),
_set_attr,
lambda self: delattr(self, internal_attr),
name
)
[docs]class BankInfo(object):
"""Base class for generating bill information
For each bank, a subclass of this should be created defining how the value
is formatted for the bill.
"""
aceite = 'N'
especie_documento = 'DM'
especie = "R$"
moeda = "9"
# This 2 values are usually empty in the printed bill
quantidade = None
valor = None
# Override in base class
description = None
bank_name = None
bank_number = None
options = {}
logo = ''
validate_field_func = None
validate_field_dv = None
nosso_numero = custom_property('nosso_numero', 13)
agencia = custom_property('agencia', 4)
conta = custom_property('conta', 7)
def __init__(self, payment):
self.payment = payment
self.payer = payment.group.payer
self.branch = payment.branch
self.bank = payment.method.destination_account.bank
self.data_processamento = localtoday()
props = self.get_properties(payment)
for key, value in props.items():
setattr(self, key, value)
self.logo_image_path = ""
if self.logo:
self.logo_image_path = environ.get_resource_filename(
'stoq', 'pixmaps', self.logo)
[docs] def get_properties(self, payment):
"""Get values necesary for bill emission.
This includes nosso numero, bank agency and bank account.
Also, any other custom options that the bank require will be present
here.
"""
props = dict(
nosso_numero=str(int(payment.identifier)),
agencia=self.bank.bank_branch,
conta=self.bank.bank_account)
for opt in self.bank.options:
props[opt.option] = opt.value
return props
#
# Properties
#
@property
def local_pagamento(self):
return sysparam.get_string('BILL_PAYMENT_PLACE')
@property
def numero_documento(self):
payment = self.payment
sale = payment.group.sale
if sale:
return sale.invoice.invoice_number
else:
return payment.identifier
@property
def penalty_percentage(self):
return sysparam.get_decimal('BILL_PENALTY')
@property
def interest_percentage(self):
return sysparam.get_decimal('BILL_INTEREST')
@property
def discount_percentage(self):
return sysparam.get_decimal('BILL_DISCOUNT')
@property
def instrucoes(self):
instructions = []
payment = self.payment
# FIXME: Penalty and interest are also defined on the payment method. We
# should use that information instead. We could also add a discount
# information on the payment method.
penalty = currency(self.penalty_percentage / 100 * payment.value)
interest = currency(self.interest_percentage / 100 * payment.value)
discount = currency(self.discount_percentage / 100 * payment.value)
data = sysparam.get_string('BILL_INSTRUCTIONS')
for line in data.split('\n')[:4]:
line = line.replace('$DATE', payment.due_date.strftime('%d/%m/%Y'))
line = line.replace('$PENALTY', converter.as_string(currency, penalty))
line = line.replace('$INTEREST', converter.as_string(currency, interest))
line = line.replace('$DISCOUNT', converter.as_string(currency, discount))
line = line.replace('$INVOICE_NUMBER', str(self.numero_documento))
instructions.append(line)
# FIXME: Only add this if not a stoq.link subscriber
#instructions.append('')
#instructions.append('\n' + _('Stoq Retail Management') + ' - www.stoq.com.br')
return instructions
@property
def demonstrativo(self):
payment = self.payment
demonstrativo = [payment.group.get_description().capitalize()]
sale = payment.group.sale
if sale:
for item in sale.get_items():
demonstrativo.append(' - %s' % item.get_description())
return demonstrativo
@property
def campo_livre(self): # pragma no cover
raise NotImplementedError
@property
def barcode(self):
num = "%03d%1s%1s%4s%10s%25s" % (
self.bank_number,
self.moeda,
'X',
self.fator_vencimento,
self.formata_valor(self.payment.value, 10),
self.campo_livre
)
dv = self.calculate_dv_barcode(num.replace('X', '', 1))
num = num.replace('X', str(dv), 1)
if len(num) != 44:
raise BoletoException(
_('The barcode must have 44 caracteres, found %d') % len(num))
return num
@property
def dv_nosso_numero(self): # pragma no cover
"""Returns nosso número DV
It should be implemented by derived class
"""
raise NotImplementedError
@property
def dv_agencia(self):
if '-' in self.agencia:
return self.agencia.split('-')[1]
else:
return ''
@property
def dv_conta(self):
if '-' in self.conta:
return self.conta.split('-')[1]
else:
return ''
@property
def dv_agencia_conta(self):
"""Verifier digit for agency and account
Some banks don't require this. The ones that does, should override this
method
"""
return ''
[docs] def calculate_dv_barcode(self, line):
resto2 = modulo11(line, 9, 1)
if resto2 in [0, 1, 10]:
dv = 1
else:
dv = 11 - resto2
return dv
@property
def fator_vencimento(self):
date_ref = datetime.date(2000, 7, 3) # Fator = 1000
delta = self.payment.due_date.date() - date_ref
return delta.days + 1000
@property
def agencia_conta(self):
return "%s/%s" % (self.agencia, self.conta)
@property
def codigo_dv_banco(self):
num = '%03d' % (self.bank_number, )
cod = "%s-%s" % (num, modulo11(num))
return cod
@property
def linha_digitavel(self):
"""Linha que o cliente pode utilizar para digitar se o código
de barras não puder ser lido
Posição Conteúdo
1 a 3 Número do banco
4 Código da Moeda - 9 para Real
5 Digito verificador do Código de Barras
6 a 19 Valor (12 inteiros e 2 decimais)
20 a 44 Campo Livre definido por cada banco
"""
linha = self.barcode
assert linha, "Boleto doesn't have a barcode"
def monta_campo(campo):
campo_dv = "%s%s" % (campo, modulo10(campo))
return "%s.%s" % (campo_dv[0:5], campo_dv[5:])
campo1 = monta_campo(linha[0:4] + linha[19:24])
campo2 = monta_campo(linha[24:34])
campo3 = monta_campo(linha[34:44])
campo4 = linha[4]
campo5 = linha[5:19]
return "%s %s %s %s %s" % (campo1, campo2, campo3, campo4, campo5)
#
# Class Methods
#
@classmethod
[docs] def get_cnab(cls, payments):
branch = payments[0].branch
bank = payments[0].method.destination_account.bank
info = cls(payments[0])
cnab = cls.cnab_class(branch, bank, info)
cnab.setup(payments)
return cnab.as_string()
@classmethod
@classmethod
[docs] def validate_field(cls, field):
if ' ' in field:
raise BoletoException(_('The field cannot have spaces'))
if '.' in field or ',' in field:
raise BoletoException(_('The field cannot have dots of commas'))
dv = None
if '-' in field:
if field.count('-') != 1:
raise BoletoException(_('More than one hyphen found'))
field, dv = field.split('-', 1)
if not dv:
raise BoletoException(_('Verifier digit cannot be empty'))
try:
int(field)
except ValueError:
raise BoletoException(_('Account needs to be a number'))
if dv and cls.validate_field_dv is not None:
func = cls.validate_field_func
if func == 'modulo11':
ret = modulo11(field)
elif func == 'modulo10':
ret = modulo10(field)
else:
ret = None
if dv.lower() in [cls.validate_field_dv]:
# FIXME: Is it correct that the rest of 0 is
# the same as 10?
if ret == 0:
pass
elif (ret is not None and
str(ret) != dv.lower() and
ret < 10):
raise BoletoException(_('Invalid verifier digit'))
else:
try:
dv = int(dv)
except ValueError:
raise BoletoException(
_('Verifier digit must be a number or %s') %
cls.validate_field_dv)
if ret is not None and ret != dv:
raise BoletoException(_('Invalid verifier digit'))
@classmethod
[docs] def validate_option(cls, option, value): # pragma no cover
pass
@staticmethod
@staticmethod
_banks = []
[docs]def register_bank(bank_class):
if not issubclass(bank_class, BankInfo): # pragma no cover
raise TypeError
assert not bank_class in _banks
_banks.append(bank_class)
return bank_class
@register_bank
[docs]class BankBanrisul(BankInfo):
description = 'Banrisul'
bank_number = 41
logo = 'logo_banrisul.jpg'
options = {u'agencia': BILL_OPTION_BANK_BRANCH,
u'conta': BILL_OPTION_BANK_BRANCH}
nosso_numero = custom_property('nosso_numero', 8)
conta = custom_property('conta', 6)
@property
def campo_livre(self):
content = '21%04d%07d%08d40' % (int(self.agencia),
int(self.conta),
int(self.nosso_numero))
dv = calculaDuploDigito(content)
return '%s%s' % (content, dv)
@register_bank
[docs]class BankBradesco(BankInfo):
description = 'Bradesco'
bank_name = 'BRADESCO'
cnab_class = BradescoCnab
bank_number = 237
logo = "logo_bancobradesco.jpg"
options = {u'carteira': BILL_OPTION_CUSTOM,
u'convenio': BILL_OPTION_CUSTOM,
u'identificacao_produto': BILL_OPTION_CUSTOM,
u'agencia': BILL_OPTION_BANK_BRANCH,
u'conta': BILL_OPTION_BANK_BRANCH}
validate_field_func = 'modulo11'
validate_field_dv = '0'
# Nosso numero (sem dv) sao 11 digitos
nosso_numero = custom_property('nosso_numero', 11)
agencia = custom_property('agencia', 4)
conta = custom_property('conta', 7)
@property
def dv_nosso_numero(self):
resto2 = modulo11(self.nosso_numero, 7, 1)
digito = 11 - resto2
if digito == 10:
dv = 'P'
elif digito == 11:
dv = 0
else:
dv = digito
return dv
@property
def campo_livre(self):
return "%04d%02d%11s%07d0" % (int(self.agencia.split('-')[0]),
int(self.carteira),
self.nosso_numero,
int(self.conta.split('-')[0]))
@classmethod
[docs] def validate_option(cls, option, value):
if option == 'carteira':
_validate_number(value, max_value=99)
@register_bank
[docs]class BankBB(BankInfo):
description = 'Banco do Brasil'
bank_number = 1
bank_name = 'BANCO DO BRASIL'
logo = 'logo_bb.gif'
options = {u'convenio': BILL_OPTION_CUSTOM,
u'agencia': BILL_OPTION_BANK_BRANCH,
u'conta': BILL_OPTION_BANK_BRANCH}
validate_field_func = 'modulo11'
validate_field_dv = 'x'
agencia = custom_property('agencia', 4)
conta = custom_property('conta', 8)
cnab_class = BBCnab
[docs] def get_properties(self, payment):
props = super(BankBB, self).get_properties(payment)
if not 'carteira' in props:
props['carteira'] = '18'
convenio = props.pop('convenio', None)
if convenio:
self.len_convenio = len(convenio)
self.convenio = convenio
else:
self.len_convenio = 7
self.convenio = ''
self.format_nnumero = 1
if 'format_nnumero' in props:
self.format_nnumero = int(props.pop('format_nnumero'))
props['agencia'] = props['agencia'].split('-')[0]
props['conta'] = props['conta'].split('-')[0]
return props
# Nosso numero (sem dv) sao 11 digitos
@property
def nosso_numero(self):
return self.convenio + self._nosso_numero
@nosso_numero.setter
def nosso_numero(self, val):
val = str(val)
if self.len_convenio == 4:
nn = val.zfill(7)
if self.len_convenio == 6:
if self.format_nnumero == 1:
nn = val.zfill(5)
elif self.format_nnumero == 2:
nn = val.zfill(17)
elif self.len_convenio == 7:
nn = val.zfill(10)
elif self.len_convenio == 8:
nn = val.zfill(9)
self._nosso_numero = nn
@property
def convenio(self):
return self._convenio
@convenio.setter
def convenio(self, val):
self._convenio = str(val).ljust(self.len_convenio, '0')
@property
def agencia_conta(self):
return "%s-%s / %s-%s" % (
self.agencia,
modulo11(self.agencia),
self.conta,
modulo11(self.conta)
)
@property
def dv_nosso_numero(self):
return modulo11(self.nosso_numero)
@property
def campo_livre(self):
if self.len_convenio in (7, 8):
return "%6s%s%s" % ('000000',
self.nosso_numero,
self.carteira)
elif self.len_convenio is 6:
if self.format_nnumero is 1:
return "%s%s%s%s" % (self.nosso_numero,
self.agencia,
self.conta,
self.carteira)
if self.format_nnumero is 2:
return "%s%2s" % (self.nosso_numero,
'21') # numero do serviço
@classmethod
[docs] def validate_option(cls, option, value):
if option == 'convenio':
_validate_number(value)
if len(value) not in [6, 7, 8]:
raise BoletoException(
# TRANSLATORS: Do not translate 'Convenio'
_("Convenio length must be 6, 7 or 8. Try filing it with "
"'0's at the left."))
@register_bank
[docs]class BankCaixa(BankInfo):
description = 'Caixa Econômica Federal'
bank_name = 'CAIXA ECONOMICA FEDERAL'
bank_number = 104
cnab_class = CaixaCnab
logo = 'logo_bancocaixa.jpg'
options = {u'carteira': BILL_OPTION_CUSTOM,
u'agencia': BILL_OPTION_BANK_BRANCH,
u'codigo_beneficiario': BILL_OPTION_CUSTOM,
u'codigo_convenio': BILL_OPTION_CUSTOM,
u'conta': BILL_OPTION_BANK_BRANCH}
inicio_nosso_numero = '80'
conta = custom_property('conta', 11)
# Nosso numero (sem dv) sao 10 digitos
@property
def nosso_numero(self):
'''Nosso Número sem DV, máximo 8 chars'''
return self._nosso_numero
@nosso_numero.setter
def nosso_numero(self, val):
self._nosso_numero = (self.inicio_nosso_numero +
self.formata_numero(val, 8))
@property
def dv_nosso_numero(self):
resto2 = modulo11(self.nosso_numero.split('-')[0], 9, 1)
digito = 11 - resto2
if digito == 10 or digito == 11:
dv = 0
else:
dv = digito
return dv
@property
def campo_livre(self):
return "%10s%4s%11s" % (self.nosso_numero,
self.agencia,
self.conta.split('-')[0])
@register_bank
[docs]class BankItau(BankInfo):
description = 'Banco Itaú'
bank_name = 'BANCO ITAU SA'
bank_number = 341
cnab_class = ItauCnab400
logo = 'logo_itau.gif'
options = collections.OrderedDict([
(u'agencia', BILL_OPTION_BANK_BRANCH),
(u'conta', BILL_OPTION_BANK_BRANCH),
(u'carteira', BILL_OPTION_CUSTOM),
(u'instrucao_1', BILL_OPTION_CUSTOM),
(u'instrucao_2', BILL_OPTION_CUSTOM),
(u'prazo', BILL_OPTION_CUSTOM),
])
nosso_numero = custom_property('nosso_numero', 8)
agencia = custom_property('agencia', 4)
conta = custom_property('conta', 5)
@classmethod
[docs] def validate_option(cls, option, value):
if option in ['instrucao_1', 'instrucao_2', 'prazo']:
_validate_number(value, max_value=99)
if option == 'carteira':
_validate_number(value, min_value=100, max_value=200)
@property
def dac_nosso_numero(self):
agencia = self.agencia.split('-')[0]
conta = self.conta.split('-')[0]
return modulo10(agencia +
conta +
self.carteira +
self.nosso_numero)
@property
def dv_agencia_conta(self):
agencia = self.agencia.split('-')[0]
conta = self.conta.split('-')[0]
return modulo10(agencia + conta)
@property
def agencia_conta(self):
agencia = self.agencia.split('-')[0]
conta = self.conta.split('-')[0]
return '%s / %s-%s' % (agencia,
conta,
self.dv_agencia_conta)
@property
def campo_livre(self):
agencia = self.agencia.split('-')[0]
conta = self.conta.split('-')[0]
return "%3s%8s%1s%4s%5s%1s%3s" % (
self.carteira,
self.nosso_numero,
self.dac_nosso_numero,
agencia,
conta,
modulo10(agencia + conta),
'000'
)
@register_bank
[docs]class BankReal(BankInfo):
description = 'Banco Real'
bank_number = 356
logo = 'logo_bancoreal.jpg'
options = {u'carteira': BILL_OPTION_CUSTOM,
u'agencia': BILL_OPTION_BANK_BRANCH,
u'conta': BILL_OPTION_BANK_BRANCH}
@property
def agencia_conta(self):
return "%s/%s-%s" % (self.agencia,
self.conta,
self.digitao_cobranca)
@property
def digitao_cobranca(self):
num = "%s%s%s" % (self.nosso_numero,
self.agencia,
self.conta)
return modulo10(num)
@property
def campo_livre(self):
return "%4s%7s%1s%13s" % (self.agencia,
self.conta,
self.digitao_cobranca,
self.nosso_numero)
@register_bank
[docs]class BankSantander(BankInfo):
description = 'Banco Santander'
bank_name = description
bank_number = 33
cnab_class = SantanderCnab
logo = 'logo_santander.jpg'
options = {u'carteira': BILL_OPTION_CUSTOM,
u'codigo_transmissao': BILL_OPTION_CUSTOM,
u'agencia': BILL_OPTION_BANK_BRANCH,
u'conta': BILL_OPTION_BANK_BRANCH}
# Numero fixo na posição 5
fixo = '9'
# IOS - somente para Seguradoras (Se 7% informar 7, limitado 9%)
# Demais clientes usar 0 (zero)
ios = '0'
nosso_numero = custom_property('nosso_numero', 7)
carteira = '102'
@property
def campo_livre(self):
conta = self.formata_numero(self.conta, 7)
dv_nosso_numero = modulo11(self.nosso_numero)
return '%s%s00000%s%s%s%s' % (self.fixo,
conta,
self.nosso_numero,
dv_nosso_numero,
self.ios,
self.carteira)
[docs]def get_all_banks():
return _banks
[docs]def get_bank_info_by_number(number):
for bank in _banks:
if bank.bank_number == number:
return bank
raise NotImplementedError(number) # pragma no cover