Browse Source

Merge branch 'master3' into master3-nfse_imperial

pull/110/head
pal0schi 8 years ago
committed by GitHub
parent
commit
c659ddbf1d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      pytrustnfe/Servidores.py
  2. 188
      pytrustnfe/nfe/danfe.py
  3. 61
      pytrustnfe/nfse/dsf/__init__.py
  4. 0
      pytrustnfe/nfse/dsf/templates/cancelar.xml
  5. 0
      pytrustnfe/nfse/dsf/templates/consulta_notas.xml
  6. 0
      pytrustnfe/nfse/dsf/templates/consultarLote.xml
  7. 22
      pytrustnfe/nfse/dsf/templates/consultarNFSeRps.xml
  8. 0
      pytrustnfe/nfse/dsf/templates/enviar.xml
  9. 0
      pytrustnfe/nfse/dsf/templates/soap_header.xml
  10. 119
      pytrustnfe/nfse/floripa/__init__.py
  11. 7
      pytrustnfe/nfse/floripa/templates/cancelar_nota.xml
  12. 40
      pytrustnfe/nfse/floripa/templates/processar_nota.xml
  13. 1
      pytrustnfe/nfse/ginfes/__init__.py
  14. 2
      pytrustnfe/nfse/ginfes/templates/Rps.xml
  15. 4
      pytrustnfe/nfse/paulistana/__init__.py
  16. 13
      pytrustnfe/xml/__init__.py
  17. 10
      requirements.txt
  18. 11
      setup.py

8
pytrustnfe/Servidores.py

@ -332,8 +332,8 @@ UFBA = {
WS_NFE_SITUACAO: 'webservices/NfeStatusServico/NfeStatusServico.asmx', WS_NFE_SITUACAO: 'webservices/NfeStatusServico/NfeStatusServico.asmx',
WS_NFE_INUTILIZACAO: 'webservices/nfenw/nfeinutilizacao2.asmx', WS_NFE_INUTILIZACAO: 'webservices/nfenw/nfeinutilizacao2.asmx',
WS_NFE_CADASTRO: 'webservices/nfenw/CadConsultaCadastro2.asmx', WS_NFE_CADASTRO: 'webservices/nfenw/CadConsultaCadastro2.asmx',
WS_NFE_RECEPCAO_EVENTO: 'webservices/sre/recepcaoevento',
WS_NFE_CANCELAMENTO: 'webservices/sre/recepcaoevento',
WS_NFE_RECEPCAO_EVENTO: 'webservices/sre/recepcaoevento.asmx',
WS_NFE_CANCELAMENTO: 'webservices/sre/recepcaoevento.asmx',
}, },
NFE_AMBIENTE_HOMOLOGACAO: { NFE_AMBIENTE_HOMOLOGACAO: {
'servidor': 'hnfe.sefaz.ba.gov.br', 'servidor': 'hnfe.sefaz.ba.gov.br',
@ -344,8 +344,8 @@ UFBA = {
WS_NFE_SITUACAO: 'webservices/NfeStatusServico/NfeStatusServico.asmx', WS_NFE_SITUACAO: 'webservices/NfeStatusServico/NfeStatusServico.asmx',
WS_NFE_INUTILIZACAO: 'webservices/nfenw/nfeinutilizacao2.asmx', WS_NFE_INUTILIZACAO: 'webservices/nfenw/nfeinutilizacao2.asmx',
WS_NFE_CADASTRO: 'webservices/nfenw/CadConsultaCadastro2.asmx', WS_NFE_CADASTRO: 'webservices/nfenw/CadConsultaCadastro2.asmx',
WS_NFE_RECEPCAO_EVENTO: 'webservices/sre/recepcaoevento',
WS_NFE_CANCELAMENTO: 'webservices/sre/recepcaoevento',
WS_NFE_RECEPCAO_EVENTO: 'webservices/sre/recepcaoevento.asmx',
WS_NFE_CANCELAMENTO: 'webservices/sre/recepcaoevento.asmx',
} }
} }

188
pytrustnfe/nfe/danfe.py

@ -16,6 +16,7 @@ from reportlab.graphics.barcode import code128
from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.enums import TA_CENTER from reportlab.lib.enums import TA_CENTER
from reportlab.platypus import Paragraph, Image from reportlab.platypus import Paragraph, Image
from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
@ -41,11 +42,9 @@ def getdateUTC(cDateUTC):
return '/'.join(cDt), cDateUTC[11:16] return '/'.join(cDt), cDateUTC[11:16]
def format_number(cNumber, precision=0, group_sep='.', decimal_sep=','):
def format_number(cNumber):
if cNumber: if cNumber:
number = float(cNumber)
return ("{:,." + str(precision) + "f}").format(number).\
replace(",", "X").replace(".", ",").replace("X", ".")
return cNumber.replace(",", "X").replace(".", ",").replace("X", ".")
return "" return ""
@ -57,6 +56,7 @@ def tagtext(oNode=None, cTag=None):
cText = '' cText = ''
return cText return cText
REGIME_TRIBUTACAO = { REGIME_TRIBUTACAO = {
'1': 'Simples Nacional', '1': 'Simples Nacional',
'2': 'Simples Nacional, excesso sublimite de receita bruta', '2': 'Simples Nacional, excesso sublimite de receita bruta',
@ -72,8 +72,9 @@ def get_image(path, width=1*cm):
class danfe(object): class danfe(object):
def __init__(self, sizepage=A4, list_xml=None, recibo=True, def __init__(self, sizepage=A4, list_xml=None, recibo=True,
orientation='portrait', logo=None):
orientation='portrait', logo=None, cce_xml=None):
path = os.path.join(os.path.dirname(__file__), 'fonts') path = os.path.join(os.path.dirname(__file__), 'fonts')
pdfmetrics.registerFont( pdfmetrics.registerFont(
@ -91,7 +92,7 @@ class danfe(object):
self.nlin = self.nTop self.nlin = self.nTop
self.logo = logo self.logo = logo
self.oFrete = {'0': '0 - Emitente', self.oFrete = {'0': '0 - Emitente',
'1': '1 - Dest/Remet',
'1': '1 - Destinatário',
'2': '2 - Terceiros', '2': '2 - Terceiros',
'9': '9 - Sem Frete'} '9': '9 - Sem Frete'}
@ -177,7 +178,10 @@ class danfe(object):
list_cod_prod=list_cod_prod) list_cod_prod=list_cod_prod)
self.newpage() self.newpage()
if cce_xml:
for xml in cce_xml:
self._generate_cce(cce_xml=xml, oXML=oXML)
self.newpage()
self.canvas.save() self.canvas.save()
def ide_emit(self, oXML=None): def ide_emit(self, oXML=None):
@ -189,7 +193,8 @@ class danfe(object):
elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide")
cChave = elem_infNFe.attrib.get('Id')[3:] cChave = elem_infNFe.attrib.get('Id')[3:]
barcode128 = code128.Code128(cChave, barHeight=10*mm, barWidth=0.25*mm)
barcode128 = code128.Code128(
cChave, barHeight=10 * mm, barWidth=0.25 * mm)
self.canvas.setLineWidth(.5) self.canvas.setLineWidth(.5)
self.rect(self.nLeft, self.nlin + 1, self.nLeft + 75, 32) self.rect(self.nLeft, self.nlin + 1, self.nLeft + 75, 32)
@ -223,7 +228,8 @@ class danfe(object):
self.stringcenter(self.nLeft + 100, self.nlin + 32, cPag) self.stringcenter(self.nLeft + 100, self.nlin + 32, cPag)
self.canvas.setFont('NimbusSanL-Regu', 6) self.canvas.setFont('NimbusSanL-Regu', 6)
self.string(self.nLeft + 86, self.nlin + 8, 'Documento Auxiliar da') self.string(self.nLeft + 86, self.nlin + 8, 'Documento Auxiliar da')
self.string(self.nLeft+86, self.nlin+10.5, 'Nota Fiscal Eletrônica')
self.string(self.nLeft + 86, self.nlin +
10.5, 'Nota Fiscal Eletrônica')
self.string(self.nLeft + 86, self.nlin + 16, '0 - Entrada') self.string(self.nLeft + 86, self.nlin + 16, '0 - Entrada')
self.string(self.nLeft + 86, self.nlin + 19, '1 - Saída') self.string(self.nLeft + 86, self.nlin + 19, '1 - Saída')
self.rect(self.nLeft + 105, self.nlin + 15, 8, 6) self.rect(self.nLeft + 105, self.nlin + 15, 8, 6)
@ -352,8 +358,12 @@ class danfe(object):
self.canvas.setFont('NimbusSanL-Regu', 8) self.canvas.setFont('NimbusSanL-Regu', 8)
self.string(self.nLeft + 1, self.nlin + 7.5, self.string(self.nLeft + 1, self.nlin + 7.5,
tagtext(oNode=elem_dest, cTag='xNome')) tagtext(oNode=elem_dest, cTag='xNome'))
self.string(nMr-69, self.nlin+7.5,
format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CNPJ')))
cnpj_cpf = tagtext(oNode=elem_dest, cTag='CNPJ')
if cnpj_cpf:
cnpj_cpf = format_cnpj_cpf(cnpj_cpf)
else:
cnpj_cpf = format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CPF'))
self.string(nMr - 69, self.nlin + 7.5, cnpj_cpf)
cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhEmi')) cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhEmi'))
self.string(nMr - 24, self.nlin + 7.7, cDt + ' ' + cHr) self.string(nMr - 24, self.nlin + 7.7, cDt + ' ' + cHr)
cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhSaiEnt')) cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhSaiEnt'))
@ -415,8 +425,7 @@ class danfe(object):
self.string(self.nLeft + nCol + 17, self.nlin + nLin, cDt) self.string(self.nLeft + nCol + 17, self.nlin + nLin, cDt)
self.stringRight( self.stringRight(
self.nLeft + nCol + 47, self.nlin + nLin, self.nLeft + nCol + 47, self.nlin + nLin,
format_number(tagtext(oNode=oXML_dup, cTag='vDup'),
precision=2))
format_number(tagtext(oNode=oXML_dup, cTag='vDup')))
if nPar == 3: if nPar == 3:
nLin = 7 nLin = 7
@ -484,41 +493,40 @@ obsCont[@xCampo='NomeVendedor']")
self.canvas.setFont('NimbusSanL-Regu', 8) self.canvas.setFont('NimbusSanL-Regu', 8)
self.stringRight( self.stringRight(
self.nLeft + 34, self.nlin + 7.7, self.nLeft + 34, self.nlin + 7.7,
format_number(tagtext(oNode=el_total, cTag='vBC'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vBC')))
self.stringRight( self.stringRight(
self.nLeft + 64, self.nlin + 7.7, self.nLeft + 64, self.nlin + 7.7,
format_number(tagtext(oNode=el_total, cTag='vICMS'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vICMS')))
self.stringRight( self.stringRight(
self.nLeft + 94, self.nlin + 7.7, self.nLeft + 94, self.nlin + 7.7,
format_number(tagtext(oNode=el_total, cTag='vBCST'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vBCST')))
self.stringRight( self.stringRight(
nMr - 66, self.nlin + 7.7, nMr - 66, self.nlin + 7.7,
format_number(tagtext(oNode=el_total, cTag='vST'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vST')))
self.stringRight( self.stringRight(
nMr - 36, self.nlin + 7.7, nMr - 36, self.nlin + 7.7,
format_number(tagtext(oNode=el_total, cTag='vTotTrib'),
precision=2))
format_number(tagtext(oNode=el_total, cTag='vTotTrib')))
self.stringRight( self.stringRight(
nMr - 1, self.nlin + 7.7, nMr - 1, self.nlin + 7.7,
format_number(tagtext(oNode=el_total, cTag='vProd'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vProd')))
self.stringRight( self.stringRight(
self.nLeft + 34, self.nlin + 14.1, self.nLeft + 34, self.nlin + 14.1,
format_number(tagtext(oNode=el_total, cTag='vFrete'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vFrete')))
self.stringRight( self.stringRight(
self.nLeft + 64, self.nlin + 14.1, self.nLeft + 64, self.nlin + 14.1,
format_number(tagtext(oNode=el_total, cTag='vSeg'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vSeg')))
self.stringRight( self.stringRight(
self.nLeft + 94, self.nlin + 14.1, self.nLeft + 94, self.nlin + 14.1,
format_number(tagtext(oNode=el_total, cTag='vDesc'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vDesc')))
self.stringRight( self.stringRight(
self.nLeft + 124, self.nlin + 14.1, self.nLeft + 124, self.nlin + 14.1,
format_number(tagtext(oNode=el_total, cTag='vOutro'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vOutro')))
self.stringRight( self.stringRight(
self.nLeft + 154, self.nlin + 14.1, self.nLeft + 154, self.nlin + 14.1,
format_number(tagtext(oNode=el_total, cTag='vIPI'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vIPI')))
self.stringRight( self.stringRight(
nMr - 1, self.nlin + 14.1, nMr - 1, self.nlin + 14.1,
format_number(tagtext(oNode=el_total, cTag='vNF'), precision=2))
format_number(tagtext(oNode=el_total, cTag='vNF')))
self.nlin += 17 # Nr linhas ocupadas pelo bloco self.nlin += 17 # Nr linhas ocupadas pelo bloco
@ -587,10 +595,10 @@ obsCont[@xCampo='NomeVendedor']")
tagtext(oNode=el_transp, cTag='nVol')) tagtext(oNode=el_transp, cTag='nVol'))
self.stringRight( self.stringRight(
nMr - 27, self.nlin + 21.2, nMr - 27, self.nlin + 21.2,
format_number(tagtext(oNode=el_transp, cTag='pesoB'), precision=3))
format_number(tagtext(oNode=el_transp, cTag='pesoB')))
self.stringRight( self.stringRight(
nMr - 1, self.nlin + 21.2, nMr - 1, self.nlin + 21.2,
format_number(tagtext(oNode=el_transp, cTag='pesoL'), precision=3))
format_number(tagtext(oNode=el_transp, cTag='pesoL')))
self.nlin += 23 self.nlin += 23
@ -626,10 +634,10 @@ obsCont[@xCampo='NomeVendedor']")
self.stringcenter(nMr - 44, self.nlin + 5.5, 'BC ICMS') self.stringcenter(nMr - 44, self.nlin + 5.5, 'BC ICMS')
self.vline(nMr - 64, self.nlin + 2, nH) self.vline(nMr - 64, self.nlin + 2, nH)
self.stringcenter(nMr - 57, self.nlin + 5.5, 'VLR TOTAL') self.stringcenter(nMr - 57, self.nlin + 5.5, 'VLR TOTAL')
self.vline(nMr-77, self.nlin+2, nH)
self.vline(nMr - 78, self.nlin + 2, nH)
self.stringcenter(nMr - 70.5, self.nlin + 5.5, 'VLR UNIT') self.stringcenter(nMr - 70.5, self.nlin + 5.5, 'VLR UNIT')
self.vline(nMr - 90, self.nlin + 2, nH) self.vline(nMr - 90, self.nlin + 2, nH)
self.stringcenter(nMr-83.5, self.nlin+5.5, 'QTD')
self.stringcenter(nMr - 83.8, self.nlin + 5.5, 'QTD')
self.vline(nMr - 96, self.nlin + 2, nH) self.vline(nMr - 96, self.nlin + 2, nH)
self.stringcenter(nMr - 93, self.nlin + 5.5, 'UNID') self.stringcenter(nMr - 93, self.nlin + 5.5, 'UNID')
self.vline(nMr - 102, self.nlin + 2, nH) self.vline(nMr - 102, self.nlin + 2, nH)
@ -641,7 +649,8 @@ obsCont[@xCampo='NomeVendedor']")
nWidth_Prod = nMr - 135 - self.nLeft - 11 nWidth_Prod = nMr - 135 - self.nLeft - 11
nCol_ = self.nLeft + 20 + (nWidth_Prod / 2) nCol_ = self.nLeft + 20 + (nWidth_Prod / 2)
self.stringcenter(nCol_, self.nlin+5.5, 'DESCRIÇÃO DO PRODUTO/SERVIÇO')
self.stringcenter(nCol_, self.nlin + 5.5,
'DESCRIÇÃO DO PRODUTO/SERVIÇO')
# Conteúdo campos # Conteúdo campos
self.canvas.setFont('NimbusSanL-Regu', 5) self.canvas.setFont('NimbusSanL-Regu', 5)
@ -657,9 +666,9 @@ obsCont[@xCampo='NomeVendedor']")
".//{http://www.portalfiscal.inf.br/nfe}ICMS") ".//{http://www.portalfiscal.inf.br/nfe}ICMS")
el_imp_IPI = el_imp.find( el_imp_IPI = el_imp.find(
".//{http://www.portalfiscal.inf.br/nfe}IPI") ".//{http://www.portalfiscal.inf.br/nfe}IPI")
cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \ cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \
tagtext(oNode=el_imp_ICMS, cTag='CST')
(tagtext(oNode=el_imp_ICMS, cTag='CST') or
tagtext(oNode=el_imp_ICMS, cTag='CSOSN'))
vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC') vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC')
vICMS = tagtext(oNode=el_imp_ICMS, cTag='vICMS') vICMS = tagtext(oNode=el_imp_ICMS, cTag='vICMS')
pICMS = tagtext(oNode=el_imp_ICMS, cTag='pICMS') pICMS = tagtext(oNode=el_imp_ICMS, cTag='pICMS')
@ -674,22 +683,20 @@ obsCont[@xCampo='NomeVendedor']")
tagtext(oNode=el_prod, cTag='CFOP')) tagtext(oNode=el_prod, cTag='CFOP'))
self.stringcenter(nMr - 93, nLin, self.stringcenter(nMr - 93, nLin,
tagtext(oNode=el_prod, cTag='uCom')) tagtext(oNode=el_prod, cTag='uCom'))
self.stringRight(nMr-77.5, nLin, format_number(
tagtext(oNode=el_prod, cTag='qCom'), precision=4))
self.stringRight(nMr - 78.5, nLin, format_number(
tagtext(oNode=el_prod, cTag='qCom')))
self.stringRight(nMr - 64.5, nLin, format_number( self.stringRight(nMr - 64.5, nLin, format_number(
tagtext(oNode=el_prod, cTag='vUnCom'), precision=2))
self.stringRight(nMr-50.5, nLin, format_number(
tagtext(oNode=el_prod, cTag='vProd'), precision=2))
self.stringRight(nMr-38.5, nLin, format_number(vBC, precision=2))
self.stringRight(nMr-26.5, nLin, format_number(vICMS, precision=2))
self.stringRight(nMr-7.5, nLin, format_number(pICMS, precision=2))
tagtext(oNode=el_prod, cTag='vUnCom')))
self.stringRight(nMr - 50.5, nLin,
tagtext(oNode=el_prod, cTag='vProd'))
self.stringRight(nMr - 38.5, nLin, format_number(vBC))
self.stringRight(nMr - 26.5, nLin, format_number(vICMS))
self.stringRight(nMr - 7.5, nLin, format_number(pICMS))
if vIPI: if vIPI:
self.stringRight(nMr-14.5, nLin,
format_number(vIPI, precision=2))
self.stringRight(nMr - 14.5, nLin, format_number(vIPI))
if pIPI: if pIPI:
self.stringRight(nMr-0.5, nLin,
format_number(pIPI, precision=2))
self.stringRight(nMr - 0.5, nLin, format_number(pIPI))
# Código Item # Código Item
line_cod = nLin line_cod = nLin
@ -718,7 +725,8 @@ obsCont[@xCampo='NomeVendedor']")
self.canvas.setFont('NimbusSanL-Bold', 6) self.canvas.setFont('NimbusSanL-Bold', 6)
self.string(self.nLeft + 1, self.nlin + 1, 'DADOS ADICIONAIS') self.string(self.nLeft + 1, self.nlin + 1, 'DADOS ADICIONAIS')
self.canvas.setFont('NimbusSanL-Regu', 5) self.canvas.setFont('NimbusSanL-Regu', 5)
self.string(self.nLeft+1, self.nlin+4, 'INFORMAÇÕES COMPLEMENTARES')
self.string(self.nLeft + 1, self.nlin + 4,
'INFORMAÇÕES COMPLEMENTARES')
self.string((self.width / 2) + 1, self.nlin + 4, 'RESERVADO AO FISCO') self.string((self.width / 2) + 1, self.nlin + 4, 'RESERVADO AO FISCO')
self.rect(self.nLeft, self.nlin + 2, self.rect(self.nLeft, self.nlin + 2,
self.width - self.nLeft - self.nRight, 42) self.width - self.nLeft - self.nRight, 42)
@ -729,7 +737,6 @@ obsCont[@xCampo='NomeVendedor']")
styleN.fontSize = 6 styleN.fontSize = 6
styleN.fontName = 'NimbusSanL-Regu' styleN.fontName = 'NimbusSanL-Regu'
styleN.leading = 7 styleN.leading = 7
fisco = tagtext(oNode=el_infAdic, cTag='infAdFisco') fisco = tagtext(oNode=el_infAdic, cTag='infAdFisco')
observacoes = tagtext(oNode=el_infAdic, cTag='infCpl') observacoes = tagtext(oNode=el_infAdic, cTag='infCpl')
if fisco: if fisco:
@ -761,18 +768,19 @@ obsCont[@xCampo='NomeVendedor']")
self.string(self.nLeft + 1, self.nlin + 10.2, 'DATA DE RECEBIMENTO') self.string(self.nLeft + 1, self.nlin + 10.2, 'DATA DE RECEBIMENTO')
self.string(self.nLeft + 41, self.nlin + 10.2, self.string(self.nLeft + 41, self.nlin + 10.2,
'IDENTIFICAÇÃO E ASSINATURA DO RECEBEDOR') 'IDENTIFICAÇÃO E ASSINATURA DO RECEBEDOR')
self.stringcenter(self.width-self.nRight-(nW/2), self.nlin+2, 'NF-e')
self.stringcenter(self.width - self.nRight -
(nW / 2), self.nlin + 2, 'NF-e')
# Conteúdo campos # Conteúdo campos
self.canvas.setFont('NimbusSanL-Bold', 8) self.canvas.setFont('NimbusSanL-Bold', 8)
cNF = tagtext(oNode=el_ide, cTag='nNF') cNF = tagtext(oNode=el_ide, cTag='nNF')
cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") cNF = '{0:011,}'.format(int(cNF)).replace(",", ".")
self.string(self.width-self.nRight-nW+2, self.nlin+8, "%s" % (cNF))
self.string(self.width - self.nRight - nW +
2, self.nlin + 8, "%s" % (cNF))
self.string(self.width - self.nRight - nW + 2, self.nlin + 14, self.string(self.width - self.nRight - nW + 2, self.nlin + 14,
"SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie'))) "SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie')))
cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi')) cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi'))
cTotal = format_number(tagtext(oNode=el_total, cTag='vNF'),
precision=2)
cTotal = format_number(tagtext(oNode=el_total, cTag='vNF'))
cEnd = tagtext(oNode=el_dest, cTag='xNome') + ' - ' cEnd = tagtext(oNode=el_dest, cTag='xNome') + ' - '
cEnd += tagtext(oNode=el_dest, cTag='xLgr') + ', ' + tagtext( cEnd += tagtext(oNode=el_dest, cTag='xLgr') + ', ' + tagtext(
@ -837,3 +845,81 @@ obsCont[@xCampo='NomeVendedor']")
pdf_out = self.oPDF_IO.getvalue() pdf_out = self.oPDF_IO.getvalue()
self.oPDF_IO.close() self.oPDF_IO.close()
fileObj.write(pdf_out) fileObj.write(pdf_out)
def _generate_cce(self, cce_xml=None, oXML=None):
self.canvas.setLineWidth(.2)
# labels
self.canvas.setFont('NimbusSanL-Bold', 12)
self.stringcenter(105, 10, u"Carta de Correção")
self.canvas.setFont('NimbusSanL-Regu', 6)
self.string(10, 18, u"RAZÃO SOCIAL DO EMITENTE")
self.string(10, 24, u"CNPJ DO EMITENTE")
self.string(10, 30, u"CHAVE DE ACESSO DA NF-E")
self.string(10, 36, u"DATA DA CORREÇÃO")
self.string(10, 42, u"ID")
self.stringcenter(105, 48, u"CORREÇÃO")
# lines
self.hline(9, 14, 200)
self.hline(9, 20, 200)
self.hline(9, 26, 200)
self.hline(9, 32, 200)
self.hline(9, 38, 200)
self.hline(9, 44, 200)
self.hline(9, 50, 200)
# values
infNFe = oXML.find(
".//{http://www.portalfiscal.inf.br/nfe}infNFe")
res_partner = infNFe.find(
".//{http://www.portalfiscal.inf.br/nfe}xNome")
elem_infNFe = cce_xml.find(
".//{http://www.portalfiscal.inf.br/nfe}infEvento")
res_partner = tagtext(oNode=infNFe, cTag='xNome')
self.string(82, 18, res_partner)
cnpj = format_cnpj_cpf(tagtext
(oNode=elem_infNFe, cTag='CNPJ'))
self.string(82, 24, cnpj)
chave_acesso = tagtext(oNode=elem_infNFe, cTag='chNFe')
self.string(82, 30, chave_acesso)
data_correcao = getdateUTC(tagtext(
oNode=elem_infNFe, cTag='dhEvento'))
data_correcao = data_correcao[0] + " " + data_correcao[1]
self.string(82, 36, data_correcao)
cce_id = elem_infNFe.values()[0]
self.string(82, 42, cce_id)
correcao = tagtext(oNode=elem_infNFe, cTag='xCorrecao')
w, h, paragraph = self._paragraph(
correcao, 'NimbusSanL-Regu', 10, 190 * mm, 20 * mm)
paragraph.drawOn(self.canvas, 10 * mm, (297 - 52) * mm - h)
self.hline(9, 54 + (h / mm), 200)
self.stringcenter(105, 58 + (h / mm), u"CONDIÇÃO DE USO")
self.hline(9, 60 + (h / mm), 200)
condicoes = tagtext(oNode=elem_infNFe, cTag='xCondUso')
w2, h2, paragraph = self._paragraph(
condicoes, 'NimbusSanL-Regu', 10, 190 * mm, 20 * mm)
paragraph.drawOn(self.canvas, 10 * mm, (297 - 62) * mm - h - h2)
self.hline(9, 68 + ((h + h2) / mm), 200)
self.vline(80, 14, 30)
self.vline(9, 14, 54 + ((h + h2) / mm))
self.vline(200, 14, 54 + ((h + h2) / mm))
def _paragraph(self, text, font, font_size, x, y):
ptext = '<font size=%s>%s</font>' % (font_size, text)
style = ParagraphStyle(name='Normal',
fontName=font,
fontSize=font_size,
)
paragraph = Paragraph(ptext, style=style)
w, h = paragraph.wrapOn(self.canvas, x, y)
return w, h, paragraph

61
pytrustnfe/nfse/campinas/__init__.py → pytrustnfe/nfse/dsf/__init__.py

@ -24,13 +24,45 @@ def _render(certificado, method, **kwargs):
return xml_send return xml_send
def _get_url(**kwargs):
try:
cod_cidade = kwargs['nfse']['cidade']
except (KeyError, TypeError):
raise KeyError("Código de cidade inválido!")
urls = {
# Belém - PA
'2715': 'http://www.issdigitalbel.com.br/WsNFe2/LoteRps.jws',
# Sorocaba - SP
'7145': 'http://issdigital.sorocaba.sp.gov.br/WsNFe2/LoteRps.jws',
# Teresina - PI
'1219': 'http://www.issdigitalthe.com.br/WsNFe2/LoteRps.jws',
# Campinas - SP
'6291': 'http://issdigital.campinas.sp.gov.br/WsNFe2/LoteRps.jws?wsdl',
# Uberlandia - MG
'5403': 'http://udigital.uberlandia.mg.gov.br/WsNFe2/LoteRps.jws',
# São Luis - MA
'0921': 'https://stm.semfaz.saoluis.ma.gov.br/WsNFe2/LoteRps?wsdl',
# Campo Grande - MS
'2729': 'http://issdigital.pmcg.ms.gov.br/WsNFe2/LoteRps.jws?wsdl',
}
try:
return urls[str(cod_cidade)]
except KeyError:
raise KeyError("DSF não emite notas da cidade {}!".format(
cod_cidade))
def _send(certificado, method, **kwargs): def _send(certificado, method, **kwargs):
url = 'http://issdigital.campinas.sp.gov.br/WsNFe2/LoteRps.jws?wsdl' # noqa
url = _get_url(**kwargs)
path = os.path.join(os.path.dirname(__file__), 'templates') path = os.path.join(os.path.dirname(__file__), 'templates')
xml_send = _render(path, method, **kwargs) xml_send = _render(path, method, **kwargs)
client = get_client(url) client = get_client(url)
response = False
if certificado: if certificado:
cert, key = extract_cert_and_key_from_pfx( cert, key = extract_cert_and_key_from_pfx(
@ -48,6 +80,11 @@ def _send(certificado, method, **kwargs):
'received_xml': e.fault.faultstring, 'received_xml': e.fault.faultstring,
'object': None 'object': None
} }
except Exception as e:
if response:
raise Exception(response)
else:
raise e
return { return {
'sent_xml': xml_send, 'sent_xml': xml_send,
@ -56,11 +93,23 @@ def _send(certificado, method, **kwargs):
} }
def xml_enviar(certificado, **kwargs):
return _render(certificado, 'enviar', **kwargs)
def enviar(certificado, **kwargs): def enviar(certificado, **kwargs):
if "xml" not in kwargs:
kwargs['xml'] = xml_enviar(certificado, **kwargs)
return _send(certificado, 'enviar', **kwargs) return _send(certificado, 'enviar', **kwargs)
def xml_teste_enviar(certificado, **kwargs):
return _render(certificado, 'testeEnviar', **kwargs)
def teste_enviar(certificado, **kwargs): def teste_enviar(certificado, **kwargs):
if "xml" not in kwargs:
kwargs['xml'] = xml_teste_enviar(certificado, **kwargs)
return _send(certificado, 'testeEnviar', **kwargs) return _send(certificado, 'testeEnviar', **kwargs)
@ -72,5 +121,11 @@ def consulta_lote(**kwargs):
return _send(False, 'consultarLote', **kwargs) return _send(False, 'consultarLote', **kwargs)
def consultar_lote_rps(certificado, **kwarg):
return _send(certificado, 'consultarNFSeRps', **kwarg)
def xml_consultar_nfse_rps(certificado, **kwargs):
return _render(certificado, 'consultarNFSeRps', **kwargs)
def consultar_nfse_rps(certificado, **kwargs):
if "xml" not in kwargs:
kwargs['xml'] = xml_consultar_nfse_rps(certificado, **kwargs)
return _send(certificado, 'consultarNFSeRps', **kwargs)

0
pytrustnfe/nfse/campinas/templates/cancelar.xml → pytrustnfe/nfse/dsf/templates/cancelar.xml

0
pytrustnfe/nfse/campinas/templates/consulta_notas.xml → pytrustnfe/nfse/dsf/templates/consulta_notas.xml

0
pytrustnfe/nfse/campinas/templates/consultarLote.xml → pytrustnfe/nfse/dsf/templates/consultarLote.xml

22
pytrustnfe/nfse/dsf/templates/consultarNFSeRps.xml

@ -0,0 +1,22 @@
<ns1:ReqConsultaNFSeRPS
xmlns:ns1="http://localhost:8080/WsNFe2/lote"
xmlns:tipos="http://localhost:8080/WsNFe2/tp"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://localhost:8080/WsNFe2/lote http://localhost:8080/WsNFe2/xsd/ReqConsultaNFSeRPS.xsd">
<Cabecalho>
<CodCidade>{{ nfse.cidade }}</CodCidade>
<CPFCNPJRemetente>{{ nfse.cpf_cnpj }}</CPFCNPJRemetente>
<transacao>true</transacao>
<Versao>1</Versao>
</Cabecalho>
<Lote Id="lote:{{ nfse.lote }}">
{% for rps in nfse.lista_rps -%}
<RPSConsulta>
<RPS Id="rps:{{ rps.numero }}">
<InscricaoMunicipalPrestador>{{ rps.prestador.inscricao_municipal }}</InscricaoMunicipalPrestador>
<NumeroRPS>{{ rps.numero }}</NumeroRPS>
<SeriePrestacao>{{ rps.serie_prestacao }}</SeriePrestacao>
</RPS>
</RPSConsulta>
{% endfor %}
</Lote>
</ns1:ReqConsultaNFSeRPS>

0
pytrustnfe/nfse/campinas/templates/enviar.xml → pytrustnfe/nfse/dsf/templates/enviar.xml

0
pytrustnfe/nfse/campinas/templates/soap_header.xml → pytrustnfe/nfse/dsf/templates/soap_header.xml

119
pytrustnfe/nfse/floripa/__init__.py

@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
# © 2017 Danimar Ribeiro, Trustcode
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import os
import hashlib
import base64
import requests
from pytrustnfe.xml import render_xml, sanitize_response
from pytrustnfe.certificado import extract_cert_and_key_from_pfx, save_cert_key
from pytrustnfe.nfse.assinatura import Assinatura
URLS = {
'producao': {
'processar_nota': 'https://nfps-e.pmf.sc.gov.br/api/v1/processamento/notas/processa',
'cancelar_nota': 'https://nfps-e.pmf.sc.gov.br/api/v1/cancelamento/notas/cancela'
},
'homologacao': {
'processar_nota': 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/processamento/notas/processa',
'cancelar_nota': 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/cancelamento/notas/cancela'
}
}
def _render(certificado, method, **kwargs):
path = os.path.join(os.path.dirname(__file__), 'templates')
xml_send = render_xml(path, '%s.xml' % method, False, **kwargs)
cert, key = extract_cert_and_key_from_pfx(
certificado.pfx, certificado.password)
cert, key = save_cert_key(cert, key)
signer = Assinatura(cert, key, certificado.password)
xml_send = signer.assina_xml(xml_send, '')
return xml_send
def _get_oauth_token(**kwargs):
if kwargs['ambiente'] == 'producao':
url = 'https://nfps-e.pmf.sc.gov.br/api/v1/autenticacao/oauth/token'
else:
url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/autenticacao/oauth/token'
m = hashlib.md5()
secret = "%s:%s" % (kwargs["client_id"], kwargs["secret_id"])
auth = base64.b64encode(secret.encode('utf-8'))
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic %s" % auth.decode('utf-8').replace('\n', '')
}
m.update(kwargs["password"].encode('utf-8'))
password = m.hexdigest().upper()
dados = "grant_type=password&username=%s&password=%s&client_id=%s&client_secret=%s" % (
kwargs["username"], password, kwargs["client_id"], kwargs["secret_id"])
r = requests.post(url, data=dados, headers=headers)
if r.status_code == 200:
return r.json()
else:
return r.json()
def _send(certificado, method, **kwargs):
url = URLS[kwargs['ambiente']][method]
xml_send = kwargs['xml']
token = _get_oauth_token(**kwargs)
if "access_token" not in token:
raise Exception("%s - %s: %s" % (token["status"], token["error"],
token["message"]))
kwargs.update({"numero": 1, 'access_token': token["access_token"]})
headers = {"Accept": "application/xml;charset=UTF-8",
"Content-Type": "application/xml",
"Authorization": "Bearer %s" % kwargs['access_token']}
r = requests.post(url, headers=headers, data=xml_send)
response, obj = sanitize_response(r.text.strip().encode('utf-8'))
return {
'sent_xml': xml_send,
'received_xml': response,
'object': obj,
'status_code': r.status_code,
}
def xml_processar_nota(certificado, **kwargs):
return _render(certificado, 'processar_nota', **kwargs)
def processar_nota(certificado, **kwargs):
if "xml" not in kwargs:
kwargs['xml'] = xml_processar_nota(certificado, **kwargs)
return _send(certificado, 'processar_nota', **kwargs)
def xml_cancelar_nota(certificado, **kwargs):
return _render(certificado, 'cancelar_nota', **kwargs)
def cancelar_nota(certificado, **kwargs):
if "xml" not in kwargs:
kwargs['xml'] = xml_cancelar_nota(certificado, **kwargs)
return _send(certificado, 'cancelar_nota', **kwargs)
def consultar_nota(certificado, **kwargs):
if kwargs['ambiente'] == 'producao':
url = "https://nfps-e.pmf.sc.gov.br/api/v1/consultas/notas/numero/%s" % (kwargs["numero"])
else:
url = "https://nfps-e-hml.pmf.sc.gov.br/api/v1/consultas/notas/numero/%s" % (kwargs["numero"])
headers = {"Accept": "application/json",
"Authorization": "Bearer %s" % kwargs['access_token']}
r = requests.get(url, headers=headers)
print(r.status_code)
if r.status_code == 200:
return r.text
else:
return r.text

7
pytrustnfe/nfse/floripa/templates/cancelar_nota.xml

@ -0,0 +1,7 @@
<?xml version="1.0"?>
<xmlCancelamentoNfpse>
<motivoCancelamento>{{ cancelamento.motivo }}</motivoCancelamento>
<nuAedf>{{ cancelamento.aedf }}</nuAedf>
<nuNotaFiscal>{{ cancelamento.numero }}</nuNotaFiscal>
<codigoVerificacao>{{ cancelamento.codigo_verificacao }}</codigoVerificacao>
</xmlCancelamentoNfpse>

40
pytrustnfe/nfse/floripa/templates/processar_nota.xml

@ -0,0 +1,40 @@
<?xml version="1.0"?>
<xmlProcessamentoNfpse>
<bairroTomador>{{ rps.tomador.bairro|normalize|escape }}</bairroTomador>
<baseCalculo>{{ rps.base_calculo }}</baseCalculo>
<baseCalculoSubstituicao>0.0</baseCalculoSubstituicao>
<cfps>{{ rps.cfps }}</cfps>
<codigoMunicipioTomador>{{ rps.tomador.cidade }}</codigoMunicipioTomador>
<codigoPostalTomador>{{ rps.tomador.cep }}</codigoPostalTomador>
<complementoEnderecoTomador>{{ rps.tomador.complemento|normalize|escape }}</complementoEnderecoTomador>
<dadosAdicionais>{{ rps.observacoes|normalize|escape }}</dadosAdicionais>
<dataEmissao>{{ rps.data_emissao }}</dataEmissao>
<emailTomador>{{ rps.tomador.email }}</emailTomador>
<identificacao>{{ rps.numero }}</identificacao>
<identificacaoTomador>{{ rps.tomador.cnpj_cpf }}</identificacaoTomador>
<inscricaoMunicipalTomador>{{ rps.tomador.inscricao_municipal }}</inscricaoMunicipalTomador>
<itensServico>
{% for item in rps.itens_servico -%}
<itemServico>
<aliquota>{{ item.aliquota }}</aliquota>
<cst>{{ item.cst_servico }}</cst>
<descricaoServico>{{ item.descricao|normalize|escape }}</descricaoServico>
<idCNAE>{{ item.cnae }}</idCNAE>
<quantidade>{{ item.quantidade }}</quantidade>
<valorTotal>{{ item.valor_total }}</valorTotal>
<valorUnitario>{{ item.valor_unitario }}</valorUnitario>
</itemServico>
{% endfor %}
</itensServico>
<logradouroTomador>{{ rps.tomador.logradouro|normalize|escape }}</logradouroTomador>
<nomeMunicipioTomador></nomeMunicipioTomador>
<numeroAEDF>{{ rps.aedf }}</numeroAEDF>
<numeroEnderecoTomador>{{ rps.tomador.numero }}</numeroEnderecoTomador>
<paisTomador>1058</paisTomador>
<razaoSocialTomador>{{ rps.tomador.razao_social|normalize|escape }}</razaoSocialTomador>
<telefoneTomador>{{ rps.tomador.telefone }}</telefoneTomador>
<ufTomador>{{ rps.tomador.uf }}</ufTomador>
<valorISSQN>{{rps.valor_issqn }}</valorISSQN>
<valorISSQNSubstituicao>0.0</valorISSQNSubstituicao>
<valorTotalServicos>{{ rps.valor_total }}</valorTotalServicos>
</xmlProcessamentoNfpse>

1
pytrustnfe/nfse/ginfes/__init__.py

@ -4,7 +4,6 @@
import os import os
import suds import suds
from lxml import etree
from pytrustnfe.xml import render_xml, sanitize_response from pytrustnfe.xml import render_xml, sanitize_response
from pytrustnfe.client import get_authenticated_client from pytrustnfe.client import get_authenticated_client
from pytrustnfe.certificado import extract_cert_and_key_from_pfx, save_cert_key from pytrustnfe.certificado import extract_cert_and_key_from_pfx, save_cert_key

2
pytrustnfe/nfse/ginfes/templates/Rps.xml

@ -27,11 +27,11 @@
<ValorCsll>{{ rps.valor_csll }}</ValorCsll> <ValorCsll>{{ rps.valor_csll }}</ValorCsll>
<IssRetido>{{ rps.iss_retido }}</IssRetido> <IssRetido>{{ rps.iss_retido }}</IssRetido>
<ValorIss>{{ rps.valor_iss }}</ValorIss> <ValorIss>{{ rps.valor_iss }}</ValorIss>
<ValorIssRetido>{{ rps.valor_iss_retido }}</ValorIssRetido>
<OutrasRetencoes>{{ rps.outras_retencoes }}</OutrasRetencoes> <OutrasRetencoes>{{ rps.outras_retencoes }}</OutrasRetencoes>
<BaseCalculo>{{ rps.base_calculo }}</BaseCalculo> <BaseCalculo>{{ rps.base_calculo }}</BaseCalculo>
<Aliquota>{{ rps.aliquota_issqn }}</Aliquota> <Aliquota>{{ rps.aliquota_issqn }}</Aliquota>
<ValorLiquidoNfse>{{ rps.valor_liquido_nfse }}</ValorLiquidoNfse> <ValorLiquidoNfse>{{ rps.valor_liquido_nfse }}</ValorLiquidoNfse>
<ValorIssRetido>{{ rps.valor_iss_retido }}</ValorIssRetido>
<DescontoIncondicionado>{{ rps.desconto_incondicionado }}</DescontoIncondicionado> <DescontoIncondicionado>{{ rps.desconto_incondicionado }}</DescontoIncondicionado>
<DescontoCondicionado>{{ rps.desconto_condicionado }}</DescontoCondicionado> <DescontoCondicionado>{{ rps.desconto_condicionado }}</DescontoCondicionado>
</Valores> </Valores>

4
pytrustnfe/nfse/paulistana/__init__.py

@ -54,10 +54,10 @@ def _send(certificado, method, **kwargs):
'object': None 'object': None
} }
response, obj = sanitize_response(response)
response, obj = sanitize_response(response.encode('utf-8'))
return { return {
'sent_xml': xml_send, 'sent_xml': xml_send,
'received_xml': response,
'received_xml': response.decode(),
'object': obj 'object': obj
} }

13
pytrustnfe/xml/__init__.py

@ -15,6 +15,18 @@ def recursively_empty(e):
return all((recursively_empty(c) for c in e.iterchildren())) return all((recursively_empty(c) for c in e.iterchildren()))
def recursively_normalize(vals):
for item in vals:
if type(vals[item]) is str:
vals[item] = vals[item].strip()
elif type(vals[item]) is dict:
recursively_normalize(vals[item])
elif type(vals[item]) is list:
for a in vals[item]:
recursively_normalize(a)
return vals
def render_xml(path, template_name, remove_empty, **nfe): def render_xml(path, template_name, remove_empty, **nfe):
nfe = recursively_normalize(nfe) nfe = recursively_normalize(nfe)
env = Environment( env = Environment(
@ -28,7 +40,6 @@ def render_xml(path, template_name, remove_empty, **nfe):
env.filters["comma"] = filters.format_with_comma env.filters["comma"] = filters.format_with_comma
template = env.get_template(template_name) template = env.get_template(template_name)
xml = template.render(**nfe) xml = template.render(**nfe)
parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, parser = etree.XMLParser(remove_blank_text=True, remove_comments=True,
strip_cdata=False) strip_cdata=False)

10
requirements.txt

@ -1,14 +1,14 @@
lxml >= 3.5.0, < 4
lxml >= 3.5.0, < 5
coveralls coveralls
Jinja2 Jinja2
signxml signxml
urllib3 >= 1.22 urllib3 >= 1.22
suds-jurko >= 0.6 suds-jurko >= 0.6
suds-jurko-requests >= 1.1 suds-jurko-requests >= 1.1
defusedxml >= 0.4.1, < 0.6
eight >= 0.3.0, < 0.5
cryptography >= 1.8, < 1.10
pyOpenSSL >= 16.0.0, < 17
defusedxml >= 0.4.1, < 1
eight >= 0.3.0, < 1
cryptography >= 1.8, < 3
pyOpenSSL >= 16.0.0, < 18
certifi >= 2015.11.20.1 certifi >= 2015.11.20.1
xmlsec >= 1.3.3 xmlsec >= 1.3.3
reportlab reportlab

11
setup.py

@ -1,7 +1,9 @@
# coding=utf-8 # coding=utf-8
from setuptools import setup, find_packages from setuptools import setup, find_packages
VERSION = "0.9.3"
VERSION = "0.9.11"
setup( setup(
name="PyTrustNFe3", name="PyTrustNFe3",
@ -27,12 +29,13 @@ later (LGPLv2+)',
'nfe/templates/*xml', 'nfe/templates/*xml',
'nfe/fonts/*ttf', 'nfe/fonts/*ttf',
'nfse/paulistana/templates/*xml', 'nfse/paulistana/templates/*xml',
'nfse/campinas/templates/*xml',
'nfse/dsf/templates/*xml',
'nfse/ginfes/templates/*xml', 'nfse/ginfes/templates/*xml',
'nfse/simpliss/templates/*xml', 'nfse/simpliss/templates/*xml',
'nfse/betha/templates/*xml', 'nfse/betha/templates/*xml',
'nfse/susesu/templates/*xml', 'nfse/susesu/templates/*xml',
'nfse/imperial/templates/*xml', 'nfse/imperial/templates/*xml',
'nfse/floripa/templates/*xml',
'xml/schemas/*xsd', 'xml/schemas/*xsd',
]}, ]},
url='https://github.com/danimaribeiro/PyTrustNFe', url='https://github.com/danimaribeiro/PyTrustNFe',
@ -42,9 +45,9 @@ later (LGPLv2+)',
install_requires=[ install_requires=[
'Jinja2 >= 2.8', 'Jinja2 >= 2.8',
'signxml >= 2.4.0', 'signxml >= 2.4.0',
'lxml >= 3.5.0, < 4',
'lxml >= 3.5.0, < 5',
'suds-jurko >= 0.6', 'suds-jurko >= 0.6',
'suds-jurko-requests >= 1.1',
'suds-jurko-requests >= 1.2',
'reportlab' 'reportlab'
], ],
tests_require=[ tests_require=[

Loading…
Cancel
Save