From 4581ea6dbee35eaee2da61527d18a44d01f93095 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Mon, 11 Sep 2017 19:33:14 -0300 Subject: [PATCH 01/31] =?UTF-8?q?Ajuste=20de=20travis=20e=20dependencias?= =?UTF-8?q?=20da=20vers=C3=A3o=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .travis.yml | 5 +++-- README.md | 4 ++-- requirements.txt | 10 ++++------ setup.py | 8 +++----- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 7f2aa806..eff572bf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist/ *egg*/ docs/_build .vscode/tags +.cache diff --git a/.travis.yml b/.travis.yml index c8bec07a..66a9cc87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ -dist: precise language: python python: -- '2.7' +- "3.4" +- "3.5" +- "3.6" virtual_env: system_site_packages: true install: diff --git a/README.md b/README.md index d16772c9..eeadd158 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Dependências: * PyXmlSec * lxml * signxml -* suds -* suds_requests +* suds-jurko +* suds-jurko-requests * reportlab * Jinja2 diff --git a/requirements.txt b/requirements.txt index 9a94f0cf..2683ae6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,14 @@ lxml >= 3.5.0, < 4 -nose -mock coveralls -http://xmlsoft.org/sources/python/libxml2-python-2.6.21.tar.gz -https://github.com/odoo-brazil/pyxmlsec/archive/master.zip Jinja2 signxml -suds >= 0.4 -suds_requests >= 0.3 +suds-jurko >= 0.6 +suds-jurko-requests >= 1.0 defusedxml >= 0.4.1, < 0.6 eight >= 0.3.0, < 0.5 cryptography >= 1.8, < 1.10 pyOpenSSL >= 16.0.0, < 17 certifi >= 2015.11.20.1 reportlab +pytest +pytest-cov diff --git a/setup.py b/setup.py index f74be779..b17205fd 100644 --- a/setup.py +++ b/setup.py @@ -37,13 +37,11 @@ 'Jinja2 >= 2.8', 'signxml >= 2.4.0', 'lxml >= 3.5.0, < 4', - 'suds >= 0.4', - 'suds_requests >= 0.3', + 'suds-jurko >= 0.6', + 'suds-jurko-requests >= 0.3', 'reportlab' ], - test_suite='nose.collector', tests_require=[ - 'nose', - 'mock', + 'pytest', ], ) From 5c5602acf926e0ae7b9c565af7b0a03f0904c974 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Mon, 11 Sep 2017 23:18:49 -0300 Subject: [PATCH 02/31] =?UTF-8?q?Migra=C3=A7=C3=A3o=20para=20python=203=20?= =?UTF-8?q?-=20Corre=C3=A7=C3=A3o=20de=20testes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 3 +- pytrustnfe/Servidores.py | 4 +- pytrustnfe/__init__.py | 8 +- pytrustnfe/certificado.py | 4 +- pytrustnfe/client.py | 4 +- pytrustnfe/nfe/__init__.py | 14 +- pytrustnfe/nfe/assinatura.py | 4 +- pytrustnfe/nfe/comunicacao.py | 1 + pytrustnfe/nfe/danfe.py | 1658 ++++++++--------- pytrustnfe/nfse/assinatura.py | 90 +- pytrustnfe/nfse/betha/__init__.py | 2 +- pytrustnfe/nfse/ginfes/__init__.py | 2 +- pytrustnfe/nfse/paulistana/__init__.py | 9 +- pytrustnfe/nfse/susesu/__init__.py | 2 +- .../test/XMLs/paulistana_canc_errado.xml | 1 - pytrustnfe/test/XMLs/paulistana_canc_ok.xml | 1 - pytrustnfe/test/XMLs/paulistana_resultado.xml | 2 +- pytrustnfe/test/XMLs/paulistana_signature.xml | 16 +- pytrustnfe/test/test_add_qr_code.py | 2 +- pytrustnfe/test/test_assinatura.py | 12 +- pytrustnfe/test/test_certificado.py | 8 +- pytrustnfe/test/test_consulta_cadastro.py | 2 +- pytrustnfe/test/test_danfe.py | 2 +- pytrustnfe/test/test_ginfes.py | 2 +- pytrustnfe/test/test_nfse_paulistana.py | 8 +- pytrustnfe/test/test_utils.py | 16 +- pytrustnfe/test/test_xml.py | 4 +- pytrustnfe/test/test_xml_serializacao.py | 6 +- pytrustnfe/test/xml_com_qrcode.xml | 6 +- pytrustnfe/test/xml_sem_qrcode.xml | 6 +- pytrustnfe/xml/__init__.py | 10 +- pytrustnfe/xml/filters.py | 18 +- requirements.txt | 2 + 33 files changed, 945 insertions(+), 984 deletions(-) diff --git a/.travis.yml b/.travis.yml index 66a9cc87..7377797b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,8 @@ virtual_env: install: - pip install --upgrade pip - pip install -r requirements.txt -script: coverage run --source=pytrustnfe setup.py nosetests +script: + pytest --cov=pytrustnfe before_install: - sudo apt-get update -qq - sudo apt-get install -qq python-dev libffi-dev libxml2-dev libxslt1-dev libssl-dev diff --git a/pytrustnfe/Servidores.py b/pytrustnfe/Servidores.py index e942df9e..239053e7 100644 --- a/pytrustnfe/Servidores.py +++ b/pytrustnfe/Servidores.py @@ -37,8 +37,8 @@ NFCE_AMBIENTE_PRODUCAO = 1 NFCE_AMBIENTE_HOMOLOGACAO = 2 -NFE_MODELO = u'55' -NFCE_MODELO = u'65' +NFE_MODELO = '55' +NFCE_MODELO = '65' SIGLA_ESTADO = { '12': 'AC', diff --git a/pytrustnfe/__init__.py b/pytrustnfe/__init__.py index 252e121a..093c5c3c 100644 --- a/pytrustnfe/__init__.py +++ b/pytrustnfe/__init__.py @@ -12,10 +12,10 @@ def __init__(self, url): def _headers(self, action): return { - u'Content-type': - u'text/xml; charset=utf-8;', - u'Accept': u'application/soap+xml; charset=utf-8', - u'SOAPAction': action + 'Content-type': + 'text/xml; charset=utf-8;', + 'Accept': 'application/soap+xml; charset=utf-8', + 'SOAPAction': action } def post_soap(self, xml_soap, action): diff --git a/pytrustnfe/certificado.py b/pytrustnfe/certificado.py index 4e44ae99..b96df912 100644 --- a/pytrustnfe/certificado.py +++ b/pytrustnfe/certificado.py @@ -14,7 +14,7 @@ def __init__(self, pfx, password): def save_pfx(self): pfx_temp = '/tmp/' + uuid4().hex - arq_temp = open(pfx_temp, 'w') + arq_temp = open(pfx_temp, 'wb') arq_temp.write(self.pfx) arq_temp.close() return pfx_temp @@ -28,7 +28,7 @@ def extract_cert_and_key_from_pfx(pfx, password): # PEM formatted certificate cert = crypto.dump_certificate(crypto.FILETYPE_PEM, pfx.get_certificate()) - return cert, key + return cert.decode(), key.decode() def save_cert_key(cert, key): diff --git a/pytrustnfe/client.py b/pytrustnfe/client.py index bf76c7b0..30047d1e 100644 --- a/pytrustnfe/client.py +++ b/pytrustnfe/client.py @@ -44,8 +44,8 @@ def __init__(self, url, cert_path, key_path): def _headers(self, action): return { - u'Content-type': u'application/soap+xml; charset=utf-8; action="http://www.portalfiscal.inf.br/nfe/wsdl/%s"' % action, - u'Accept': u'application/soap+xml; charset=utf-8', + 'Content-type': 'application/soap+xml; charset=utf-8; action="http://www.portalfiscal.inf.br/nfe/wsdl/%s"' % action, + 'Accept': 'application/soap+xml; charset=utf-8', } def post_soap(self, xml_soap, cabecalho): diff --git a/pytrustnfe/nfe/__init__.py b/pytrustnfe/nfe/__init__.py index f00ee9fd..b2daec2a 100644 --- a/pytrustnfe/nfe/__init__.py +++ b/pytrustnfe/nfe/__init__.py @@ -5,6 +5,7 @@ import os import hashlib +import binascii from lxml import etree from .comunicacao import executar_consulta from .assinatura import Assinatura @@ -80,7 +81,7 @@ def _add_qrCode(xml, **kwargs): infnfesupl = etree.Element('infNFeSupl') qrcode = etree.Element('qrCode') chave_nfe = inf_nfe['Id'][3:] - dh_emissao = inf_nfe['ide']['dhEmi'].encode('hex') + dh_emissao = binascii.hexlify(inf_nfe['ide']['dhEmi'].encode()).decode() versao = '100' ambiente = kwargs['ambiente'] valor_total = inf_nfe['total']['vNF'] @@ -98,9 +99,8 @@ def _add_qrCode(xml, **kwargs): dest.append(cpf) dest_parent.append(dest) icms_total = inf_nfe['total']['vICMS'] - dig_val = xml.find( - ".//{http://www.w3.org/2000/09/xmldsig#}DigestValue")\ - .text.encode('hex') + dig_val = binascii.hexlify(xml.find( + ".//{http://www.w3.org/2000/09/xmldsig#}DigestValue").text.encode()).decode() cid_token = kwargs['NFes'][0]['infNFe']['codigo_seguranca']['cid_token'] csc = kwargs['NFes'][0]['infNFe']['codigo_seguranca']['csc'] @@ -108,7 +108,7 @@ def _add_qrCode(xml, **kwargs): ={5}&vICMS={6}&digVal={7}&cIdToken={8}{9}".\ format(chave_nfe, versao, ambiente, dest_cpf, dh_emissao, valor_total, icms_total, dig_val, cid_token, csc) - c_hash_QR_code = hashlib.sha1(c_hash_QR_code).hexdigest() + c_hash_QR_code = hashlib.sha1(c_hash_QR_code.encode()).hexdigest() QR_code_url = "?chNFe={0}&nVersao={1}&tpAmb={2}&{3}dhEmi={4}&vNF={5}&vICMS\ ={6}&digVal={7}&cIdToken={8}&cHashQRCode={9}".\ @@ -121,7 +121,7 @@ def _add_qrCode(xml, **kwargs): qrcode.text = etree.CDATA(qrcode_text) infnfesupl.append(qrcode) nfe.insert(1, infnfesupl) - return etree.tostring(xml) + return etree.tostring(xml, encoding=str) def _send(certificado, method, sign, **kwargs): @@ -175,7 +175,7 @@ def _send(certificado, method, sign, **kwargs): xml_send = _add_qrCode(xml_send, **kwargs) else: - xml_send = etree.tostring(xmlElem_send) + xml_send = etree.tostring(xmlElem_send, encoding=str) url = localizar_url(method, kwargs['estado'], modelo, kwargs['ambiente']) diff --git a/pytrustnfe/nfe/assinatura.py b/pytrustnfe/nfe/assinatura.py index e14ec5de..34df54a8 100644 --- a/pytrustnfe/nfe/assinatura.py +++ b/pytrustnfe/nfe/assinatura.py @@ -32,7 +32,7 @@ def assina_xml(self, xml_element, reference): ref_uri = ('#%s' % reference) if reference else None signed_root = signer.sign( - xml_element, key=key, cert=cert, + xml_element, key=key.encode(), cert=cert.encode(), reference_uri=ref_uri) if reference: element_signed = signed_root.find(".//*[@Id='%s']" % reference) @@ -42,4 +42,4 @@ def assina_xml(self, xml_element, reference): if element_signed is not None and signature is not None: parent = element_signed.getparent() parent.append(signature) - return etree.tostring(signed_root) + return etree.tostring(signed_root, encoding=str) diff --git a/pytrustnfe/nfe/comunicacao.py b/pytrustnfe/nfe/comunicacao.py index 2e3f4d91..df15ddd5 100644 --- a/pytrustnfe/nfe/comunicacao.py +++ b/pytrustnfe/nfe/comunicacao.py @@ -10,6 +10,7 @@ def _soap_xml(body, cabecalho): + print(type(body)) xml = '' xml += '' xml += '' diff --git a/pytrustnfe/nfe/danfe.py b/pytrustnfe/nfe/danfe.py index 799150c9..d0fedcd0 100644 --- a/pytrustnfe/nfe/danfe.py +++ b/pytrustnfe/nfe/danfe.py @@ -1,829 +1,829 @@ -# -*- coding: utf-8 -*- -# © 2017 Edson Bernardino, ITK Soft -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -# Classe para geração de PDF da DANFE a partir de xml etree.fromstring - - -from cStringIO import StringIO as IO -from textwrap import wrap - -from reportlab.lib import utils -from reportlab.pdfgen import canvas -from reportlab.lib.units import mm, cm -from reportlab.lib.pagesizes import A4 -from reportlab.lib.colors import black, gray -from reportlab.graphics.barcode import code128 -from reportlab.lib.styles import getSampleStyleSheet -from reportlab.lib.enums import TA_CENTER -from reportlab.platypus import Paragraph, Image - - -def chunks(cString, nLen): - for start in range(0, len(cString), nLen): - yield cString[start:start+nLen] - - -def format_cnpj_cpf(value): - if len(value) < 12: # CPF - cValue = '%s.%s.%s-%s' % (value[:-8], value[-8:-5], - value[-5:-2], value[-2:]) - else: - cValue = '%s.%s.%s/%s-%s' % (value[:-12], value[-12:-9], - value[-9:-6], value[-6:-2], value[-2:]) - return cValue - - -def getdateUTC(cDateUTC): - cDt = cDateUTC[0:10].split('-') - cDt.reverse() - return '/'.join(cDt), cDateUTC[11:16] - - -def format_number(cNumber, precision=0, group_sep='.', decimal_sep=','): - if cNumber: - number = float(cNumber) - return ("{:,." + str(precision) + "f}").format(number).\ - replace(",", "X").replace(".", ",").replace("X", ".") - return "" - - -def tagtext(oNode=None, cTag=None): - try: - xpath = ".//{http://www.portalfiscal.inf.br/nfe}%s" % (cTag) - cText = oNode.find(xpath).text - except: - cText = '' - return cText - -REGIME_TRIBUTACAO = { - '1': 'Simples Nacional', - '2': 'Simples Nacional, excesso sublimite de receita bruta', - '3': 'Regime Normal' -} - - -def get_image(path, width=1*cm): - img = utils.ImageReader(path) - iw, ih = img.getSize() - aspect = ih / float(iw) - return Image(path, width=width, height=(width * aspect)) - - -class danfe(object): - def __init__(self, sizepage=A4, list_xml=None, recibo=True, - orientation='portrait', logo=None): - self.width = 210 # 21 x 29,7cm - self.height = 297 - self.nLeft = 10 - self.nRight = 10 - self.nTop = 7 - self.nBottom = 8 - self.nlin = self.nTop - self.logo = logo - self.oFrete = {'0': '0 - Emitente', - '1': '1 - Dest/Remet', - '2': '2 - Terceiros', - '9': '9 - Sem Frete'} - - self.oPDF_IO = IO() - if orientation == 'landscape': - raise NameError('Rotina não implementada') - else: - size = sizepage - - self.canvas = canvas.Canvas(self.oPDF_IO, pagesize=size) - self.canvas.setTitle('DANFE') - self.canvas.setStrokeColor(black) - - for oXML in list_xml: - oXML_cobr = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}cobr") - - self.NrPages = 1 - self.Page = 1 - - # Calculando total linhas usadas para descrições dos itens - # Com bloco fatura, apenas 29 linhas para itens na primeira folha - nNr_Lin_Pg_1 = 34 if oXML_cobr is None else 30 - # [ rec_ini , rec_fim , lines , limit_lines ] - oPaginator = [[0, 0, 0, nNr_Lin_Pg_1]] - el_det = oXML.findall(".//{http://www.portalfiscal.inf.br/nfe}det") - if el_det is not None: - list_desc = [] - list_cod_prod = [] - nPg = 0 - for nId, item in enumerate(el_det): - el_prod = item.find( - ".//{http://www.portalfiscal.inf.br/nfe}prod") - infAdProd = item.find( - ".//{http://www.portalfiscal.inf.br/nfe}infAdProd") - - list_ = wrap(tagtext(oNode=el_prod, cTag='xProd'), 56) - if infAdProd is not None: - list_.extend(wrap(infAdProd.text, 56)) - list_desc.append(list_) - - list_cProd = wrap(tagtext(oNode=el_prod, cTag='cProd'), 14) - list_cod_prod.append(list_cProd) - - # Nr linhas necessárias p/ descrição item - nLin_Itens = len(list_) - - if (oPaginator[nPg][2] + nLin_Itens) >= oPaginator[nPg][3]: - oPaginator.append([0, 0, 0, 77]) - nPg += 1 - oPaginator[nPg][0] = nId - oPaginator[nPg][1] = nId + 1 - oPaginator[nPg][2] = nLin_Itens - else: - # adiciona-se 1 pelo funcionamento de xrange - oPaginator[nPg][1] = nId + 1 - oPaginator[nPg][2] += nLin_Itens - - self.NrPages = len(oPaginator) # Calculando nr. páginas - - if recibo: - self.recibo_entrega(oXML=oXML) - - self.ide_emit(oXML=oXML) - self.destinatario(oXML=oXML) - - if oXML_cobr is not None: - self.faturas(oXML=oXML_cobr) - - self.impostos(oXML=oXML) - self.transportes(oXML=oXML) - self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPaginator[0], - list_desc=list_desc, list_cod_prod=list_cod_prod) - - self.adicionais(oXML=oXML) - - # Gera o restante das páginas do XML - for oPag in oPaginator[1:]: - self.newpage() - self.ide_emit(oXML=oXML) - self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPag, - list_desc=list_desc, nHeight=77, - list_cod_prod=list_cod_prod) - - self.newpage() - - self.canvas.save() - - def ide_emit(self, oXML=None): - elem_infNFe = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}infNFe") - elem_protNFe = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}protNFe") - elem_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") - elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") - - cChave = elem_infNFe.attrib.get('Id')[3:] - barcode128 = code128.Code128(cChave, barHeight=10*mm, barWidth=0.25*mm) - - self.canvas.setLineWidth(.5) - self.rect(self.nLeft, self.nlin+1, self.nLeft+75, 32) - self.rect(self.nLeft+115, self.nlin+1, - self.width-self.nLeft-self.nRight-115, 39) - - self.hline(self.nLeft+85, self.nlin+1, 125) - - self.rect(self.nLeft+116, self.nlin+15, - self.width-self.nLeft-self.nRight-117, 6) - - self.rect(self.nLeft, self.nlin+33, - self.width-self.nLeft-self.nRight, 14) - self.hline(self.nLeft, self.nlin+40, self.width-self.nRight) - self.vline(self.nLeft+60, self.nlin+40, 7) - self.vline(self.nLeft+100, self.nlin+40, 7) - - # Labels - self.canvas.setFont('NimbusSanL-Bold', 12) - self.stringcenter(self.nLeft+98, self.nlin+5, 'DANFE') - self.stringcenter(self.nLeft+109, self.nlin+19.5, - tagtext(oNode=elem_ide, cTag='tpNF')) - self.canvas.setFont('NimbusSanL-Bold', 8) - cNF = tagtext(oNode=elem_ide, cTag='nNF') - cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") - self.stringcenter(self.nLeft+100, self.nlin+25, "Nº %s" % (cNF)) - - self.stringcenter(self.nLeft+100, self.nlin+29, u"SÉRIE %s" % ( - tagtext(oNode=elem_ide, cTag='serie'))) - cPag = "Página %s de %s" % (str(self.Page), str(self.NrPages)) - self.stringcenter(self.nLeft+100, self.nlin+32, cPag) - self.canvas.setFont('NimbusSanL-Regu', 6) - 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+16, '0 - Entrada') - self.string(self.nLeft+86, self.nlin+19, '1 - Saída') - self.rect(self.nLeft+105, self.nlin+15, 8, 6) - - self.stringcenter( - self.nLeft+152, self.nlin+25, - 'Consulta de autenticidade no portal nacional da NF-e') - self.stringcenter( - self.nLeft+152, self.nlin+28, - 'www.nfe.fazenda.gov.br/portal ou no site da SEFAZ Autorizadora') - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+117, self.nlin+16.7, 'CHAVE DE ACESSO') - self.string(self.nLeft+116, self.nlin+2.7, 'CONTROLE DO FISCO') - - self.string(self.nLeft+1, self.nlin+34.7, 'NATUREZA DA OPERAÇÃO') - self.string(self.nLeft+116, self.nlin+34.7, - 'PROTOCOLO DE AUTORIZAÇÃO DE USO') - self.string(self.nLeft+1, self.nlin+41.7, 'INSCRIÇÃO ESTADUAL') - self.string(self.nLeft+61, self.nlin+41.7, - 'INSCRIÇÃO ESTADUAL DO SUBST. TRIB.') - self.string(self.nLeft+101, self.nlin+41.7, 'CNPJ') - - # Conteúdo campos - barcode128.drawOn(self.canvas, (self.nLeft+111.5)*mm, - (self.height-self.nlin-14)*mm) - self.canvas.setFont('NimbusSanL-Bold', 6) - nW_Rect = (self.width-self.nLeft-self.nRight-117) / 2 - self.stringcenter(self.nLeft+116.5+nW_Rect, self.nlin+19.5, - ' '.join(chunks(cChave, 4))) # Chave - self.canvas.setFont('NimbusSanL-Regu', 8) - cDt, cHr = getdateUTC(tagtext(oNode=elem_protNFe, cTag='dhRecbto')) - cProtocolo = tagtext(oNode=elem_protNFe, cTag='nProt') - cDt = cProtocolo + ' - ' + cDt + ' ' + cHr - nW_Rect = (self.width-self.nLeft-self.nRight-110) / 2 - self.stringcenter(self.nLeft+115+nW_Rect, self.nlin+38.7, cDt) - self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+38.7, - tagtext(oNode=elem_ide, cTag='natOp')) - self.string(self.nLeft+1, self.nlin+46, - tagtext(oNode=elem_emit, cTag='IE')) - self.string(self.nLeft+101, self.nlin+46, - format_cnpj_cpf(tagtext(oNode=elem_emit, cTag='CNPJ'))) - - styles = getSampleStyleSheet() - styleN = styles['Normal'] - styleN.fontSize = 10 - styleN.fontName = 'NimbusSanL-Bold' - styleN.alignment = TA_CENTER - - # Razão Social emitente - P = Paragraph(tagtext(oNode=elem_emit, cTag='xNome'), styleN) - w, h = P.wrap(55*mm, 50*mm) - P.drawOn(self.canvas, (self.nLeft+30)*mm, - (self.height-self.nlin-12)*mm) - - if self.logo: - img = get_image(self.logo, width=2*cm) - img.drawOn(self.canvas, (self.nLeft+5)*mm, - (self.height-self.nlin-22)*mm) - - cEnd = tagtext(oNode=elem_emit, cTag='xLgr') + ', ' + tagtext( - oNode=elem_emit, cTag='nro') + ' - ' - cEnd += tagtext(oNode=elem_emit, cTag='xBairro') + '
' + tagtext( - oNode=elem_emit, cTag='xMun') + ' - ' - cEnd += 'Fone: ' + tagtext(oNode=elem_emit, cTag='fone') + '
' - cEnd += tagtext(oNode=elem_emit, cTag='UF') + ' - ' + tagtext( - oNode=elem_emit, cTag='CEP') - - regime = tagtext(oNode=elem_emit, cTag='CRT') - cEnd += u'
Regime Tributário: %s' % (REGIME_TRIBUTACAO[regime]) - - styleN.fontName = 'NimbusSanL-Regu' - styleN.fontSize = 7 - styleN.leading = 10 - P = Paragraph(cEnd, styleN) - w, h = P.wrap(55*mm, 30*mm) - P.drawOn(self.canvas, (self.nLeft+30)*mm, - (self.height-self.nlin-31)*mm) - - # Homologação - if tagtext(oNode=elem_ide, cTag='tpAmb') == '2': - self.canvas.saveState() - self.canvas.rotate(90) - self.canvas.setFont('Times-Bold', 40) - self.canvas.setFillColorRGB(0.57, 0.57, 0.57) - self.string(self.nLeft+65, 449, 'SEM VALOR FISCAL') - self.canvas.restoreState() - - self.nlin += 48 - - def destinatario(self, oXML=None): - elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") - elem_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") - nMr = self.width-self.nRight - - self.nlin += 1 - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'DESTINATÁRIO/REMETENTE') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 20) - self.vline(nMr-25, self.nlin+2, 20) - self.hline(self.nLeft, self.nlin+8.66, self.width-self.nLeft) - self.hline(self.nLeft, self.nlin+15.32, self.width-self.nLeft) - self.vline(nMr-70, self.nlin+2, 6.66) - self.vline(nMr-53, self.nlin+8.66, 6.66) - self.vline(nMr-99, self.nlin+8.66, 6.66) - self.vline(nMr-90, self.nlin+15.32, 6.66) - self.vline(nMr-102, self.nlin+15.32, 6.66) - self.vline(nMr-136, self.nlin+15.32, 6.66) - # Labels/Fields - self.canvas.setFont('NimbusSanL-Bold', 5) - self.string(self.nLeft+1, self.nlin+3.7, 'NOME/RAZÃO SOCIAL') - self.string(nMr-69, self.nlin+3.7, 'CNPJ/CPF') - self.string(nMr-24, self.nlin+3.7, 'DATA DA EMISSÃO') - self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') - self.string(nMr-98, self.nlin+10.3, 'BAIRRO/DISTRITO') - self.string(nMr-52, self.nlin+10.3, 'CEP') - self.string(nMr-24, self.nlin+10.3, 'DATA DE ENTRADA/SAÍDA') - self.string(self.nLeft+1, self.nlin+17.1, 'MUNICÍPIO') - self.string(nMr-135, self.nlin+17.1, 'FONE/FAX') - self.string(nMr-101, self.nlin+17.1, 'UF') - self.string(nMr-89, self.nlin+17.1, 'INSCRIÇÃO ESTADUAL') - self.string(nMr-24, self.nlin+17.1, 'HORA DE ENTRADA/SAÍDA') - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+7.5, - tagtext(oNode=elem_dest, cTag='xNome')) - self.string(nMr-69, self.nlin+7.5, - format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CNPJ'))) - cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhEmi')) - self.string(nMr-24, self.nlin+7.7, cDt + ' ' + cHr) - cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhSaiEnt')) - self.string(nMr-24, self.nlin+14.3, cDt + ' ' + cHr) # Dt saída - cEnd = tagtext(oNode=elem_dest, cTag='xLgr') + ', ' + tagtext( - oNode=elem_dest, cTag='nro') - self.string(self.nLeft+1, self.nlin+14.3, cEnd) - self.string(nMr-98, self.nlin+14.3, - tagtext(oNode=elem_dest, cTag='xBairro')) - self.string(nMr-52, self.nlin+14.3, - tagtext(oNode=elem_dest, cTag='CEP')) - self.string(self.nLeft+1, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='xMun')) - self.string(nMr-135, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='fone')) - self.string(nMr-101, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='UF')) - self.string(nMr-89, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='IE')) - - self.nlin += 24 # Nr linhas ocupadas pelo bloco - - def faturas(self, oXML=None): - - nMr = self.width-self.nRight - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'FATURA') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 13) - self.vline(nMr-47.5, self.nlin+2, 13) - self.vline(nMr-95, self.nlin+2, 13) - self.vline(nMr-142.5, self.nlin+2, 13) - self.hline(nMr-47.5, self.nlin+8.5, self.width-self.nLeft) - # Labels - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(nMr-46.5, self.nlin+3.8, 'CÓDIGO VENDEDOR') - self.string(nMr-46.5, self.nlin+10.2, 'NOME VENDEDOR') - self.string(nMr-93.5, self.nlin+3.8, - 'FATURA VENCIMENTO VALOR') - self.string(nMr-140.5, self.nlin+3.8, - 'FATURA VENCIMENTO VALOR') - self.string(self.nLeft+2, self.nlin+3.8, - 'FATURA VENCIMENTO VALOR') - - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Bold', 6) - nLin = 7 - nPar = 1 - nCol = 0 - nAju = 0 - - line_iter = iter(oXML[1:10]) # Salta elemt 1 e considera os próximos 9 - for oXML_dup in line_iter: - - cDt, cHr = getdateUTC(tagtext(oNode=oXML_dup, cTag='dVenc')) - self.string(self.nLeft+nCol+1, self.nlin+nLin, - tagtext(oNode=oXML_dup, cTag='nDup')) - self.string(self.nLeft+nCol+17, self.nlin+nLin, cDt) - self.stringRight( - self.nLeft+nCol+47, self.nlin+nLin, - format_number(tagtext(oNode=oXML_dup, cTag='vDup'), - precision=2)) - - if nPar == 3: - nLin = 7 - nPar = 1 - nCol += 47 - nAju += 1 - nCol += nAju * (0.3) - else: - nLin += 3.3 - nPar += 1 - - # Campos adicionais XML - Condicionados a existencia de financeiro - elem_infAdic = oXML.getparent().find( - ".//{http://www.portalfiscal.inf.br/nfe}infAdic") - if elem_infAdic is not None: - codvend = elem_infAdic.find( - ".//{http://www.portalfiscal.inf.br/nfe}obsCont\ -[@xCampo='CodVendedor']") - self.string(nMr-46.5, self.nlin+7.7, - tagtext(oNode=codvend, cTag='xTexto')) - vend = elem_infAdic.find(".//{http://www.portalfiscal.inf.br/nfe}\ -obsCont[@xCampo='NomeVendedor']") - self.string(nMr-46.5, self.nlin+14.3, - tagtext(oNode=vend, cTag='xTexto')[:36]) - - self.nlin += 16 # Nr linhas ocupadas pelo bloco - - def impostos(self, oXML=None): - # Impostos - el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") - nMr = self.width-self.nRight - self.nlin += 1 - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'CÁLCULO DO IMPOSTO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 13) - self.hline(self.nLeft, self.nlin+8.5, self.width-self.nLeft) - self.vline(nMr-35, self.nlin+2, 6.5) - self.vline(nMr-65, self.nlin+2, 6.5) - self.vline(nMr-95, self.nlin+2, 6.5) - self.vline(nMr-125, self.nlin+2, 6.5) - self.vline(nMr-155, self.nlin+2, 6.5) - self.vline(nMr-35, self.nlin+8.5, 6.5) - self.vline(nMr-65, self.nlin+8.5, 6.5) - self.vline(nMr-95, self.nlin+8.5, 6.5) - self.vline(nMr-125, self.nlin+8.5, 6.5) - self.vline(nMr-155, self.nlin+8.5, 6.5) - # Labels - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+1, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS') - self.string(nMr-154, self.nlin+3.8, 'VALOR DO ICMS') - self.string(nMr-124, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS ST') - self.string(nMr-94, self.nlin+3.8, 'VALOR DO ICMS ST') - self.string(nMr-64, self.nlin+3.8, 'VALOR APROX TRIBUTOS') - self.string(nMr-34, self.nlin+3.8, 'VALOR TOTAL DOS PRODUTOS') - - self.string(self.nLeft+1, self.nlin+10.2, 'VALOR DO FRETE') - self.string(nMr-154, self.nlin+10.2, 'VALOR DO SEGURO') - self.string(nMr-124, self.nlin+10.2, 'DESCONTO') - self.string(nMr-94, self.nlin+10.2, 'OUTRAS DESP. ACESSÓRIAS') - self.string(nMr-64, self.nlin+10.2, 'VALOR DO IPI') - self.string(nMr-34, self.nlin+10.2, 'VALOR TOTAL DA NOTA') - - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 8) - self.stringRight( - self.nLeft+34, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vBC'), precision=2)) - self.stringRight( - self.nLeft+64, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vICMS'), precision=2)) - self.stringRight( - self.nLeft+94, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vBCST'), precision=2)) - self.stringRight( - nMr-66, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vST'), precision=2)) - self.stringRight( - nMr-36, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vTotTrib'), - precision=2)) - self.stringRight( - nMr-1, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vProd'), precision=2)) - self.stringRight( - self.nLeft+34, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vFrete'), precision=2)) - self.stringRight( - self.nLeft+64, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vSeg'), precision=2)) - self.stringRight( - self.nLeft+94, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vDesc'), precision=2)) - self.stringRight( - self.nLeft+124, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vOutro'), precision=2)) - self.stringRight( - self.nLeft+154, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vIPI'), precision=2)) - self.stringRight( - nMr-1, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vNF'), precision=2)) - - self.nlin += 17 # Nr linhas ocupadas pelo bloco - - def transportes(self, oXML=None): - el_transp = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}transp") - nMr = self.width-self.nRight - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, - 'TRANSPORTADOR/VOLUMES TRANSPORTADOS') - self.canvas.setFont('NimbusSanL-Regu', 5) - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 20) - self.hline(self.nLeft, self.nlin+8.6, self.width-self.nLeft) - self.hline(self.nLeft, self.nlin+15.2, self.width-self.nLeft) - self.vline(nMr-40, self.nlin+2, 13.2) - self.vline(nMr-49, self.nlin+2, 20) - self.vline(nMr-92, self.nlin+2, 6.6) - self.vline(nMr-120, self.nlin+2, 6.6) - self.vline(nMr-75, self.nlin+2, 6.6) - self.vline(nMr-26, self.nlin+15.2, 6.6) - self.vline(nMr-102, self.nlin+8.6, 6.6) - self.vline(nMr-85, self.nlin+15.2, 6.6) - self.vline(nMr-121, self.nlin+15.2, 6.6) - self.vline(nMr-160, self.nlin+15.2, 6.6) - # Labels/Fields - self.string(nMr-39, self.nlin+3.8, 'CNPJ/CPF') - self.string(nMr-74, self.nlin+3.8, 'PLACA DO VEÍCULO') - self.string(nMr-91, self.nlin+3.8, 'CÓDIGO ANTT') - self.string(nMr-119, self.nlin+3.8, 'FRETE POR CONTA') - self.string(self.nLeft+1, self.nlin+3.8, 'RAZÃO SOCIAL') - self.string(nMr-48, self.nlin+3.8, 'UF') - self.string(nMr-39, self.nlin+10.3, 'INSCRIÇÃO ESTADUAL') - self.string(nMr-48, self.nlin+10.3, 'UF') - self.string(nMr-101, self.nlin+10.3, 'MUNICÍPIO') - self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') - self.string(nMr-48, self.nlin+17, 'PESO BRUTO') - self.string(nMr-25, self.nlin+17, 'PESO LÍQUIDO') - self.string(nMr-84, self.nlin+17, 'NUMERAÇÃO') - self.string(nMr-120, self.nlin+17, 'MARCA') - self.string(nMr-159, self.nlin+17, 'ESPÉCIE') - self.string(self.nLeft+1, self.nlin+17, 'QUANTIDADE') - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+7.7, - tagtext(oNode=el_transp, cTag='xNome')[:40]) - self.string(self.nLeft+71, self.nlin+7.7, - self.oFrete[tagtext(oNode=el_transp, cTag='modFrete')]) - self.string(nMr-39, self.nlin+7.7, - format_cnpj_cpf(tagtext(oNode=el_transp, cTag='CNPJ'))) - self.string(self.nLeft+1, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='xEnder')[:45]) - self.string(self.nLeft+89, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='xMun')) - self.string(nMr-48, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='UF')) - self.string(nMr-39, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='IE')) - self.string(self.nLeft+1, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='qVol')) - self.string(self.nLeft+31, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='esp')) - self.string(self.nLeft+70, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='marca')) - self.string(self.nLeft+106, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='nVol')) - self.stringRight( - nMr-27, self.nlin+21.2, - format_number(tagtext(oNode=el_transp, cTag='pesoB'), precision=3)) - self.stringRight( - nMr-1, self.nlin+21.2, - format_number(tagtext(oNode=el_transp, cTag='pesoL'), precision=3)) - - self.nlin += 23 - - def produtos(self, oXML=None, el_det=None, oPaginator=None, - list_desc=None, list_cod_prod=None, nHeight=29): - - nMr = self.width-self.nRight - nStep = 2.5 # Passo entre linhas - nH = 7.5 + (nHeight * nStep) # cabeçalho 7.5 - self.nlin += 1 - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'DADOS DO PRODUTO/SERVIÇO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, nH) - self.hline(self.nLeft, self.nlin+8, self.width-self.nLeft) - - self.canvas.setFont('NimbusSanL-Regu', 5.5) - # Colunas - self.vline(self.nLeft+15, self.nlin+2, nH) - self.stringcenter(self.nLeft+7.5, self.nlin+5.5, 'CÓDIGO') - self.vline(nMr-7, self.nlin+2, nH) - self.stringcenter(nMr-3.5, self.nlin+4.5, 'ALÍQ') - self.stringcenter(nMr-3.5, self.nlin+6.5, 'IPI') - self.vline(nMr-14, self.nlin+2, nH) - self.stringcenter(nMr-10.5, self.nlin+4.5, 'ALÍQ') - self.stringcenter(nMr-10.5, self.nlin+6.5, 'ICMS') - self.vline(nMr-26, self.nlin+2, nH) - self.stringcenter(nMr-20, self.nlin+5.5, 'VLR. IPI') - self.vline(nMr-38, self.nlin+2, nH) - self.stringcenter(nMr-32, self.nlin+5.5, 'VLR. ICMS') - self.vline(nMr-50, self.nlin+2, nH) - self.stringcenter(nMr-44, self.nlin+5.5, 'BC ICMS') - self.vline(nMr-64, self.nlin+2, nH) - self.stringcenter(nMr-57, self.nlin+5.5, 'VLR TOTAL') - self.vline(nMr-77, self.nlin+2, nH) - self.stringcenter(nMr-70.5, self.nlin+5.5, 'VLR UNIT') - self.vline(nMr-90, self.nlin+2, nH) - self.stringcenter(nMr-83.5, self.nlin+5.5, 'QTD') - self.vline(nMr-96, self.nlin+2, nH) - self.stringcenter(nMr-93, self.nlin+5.5, 'UNID') - self.vline(nMr-102, self.nlin+2, nH) - self.stringcenter(nMr-99, self.nlin+5.5, 'CFOP') - self.vline(nMr-108, self.nlin+2, nH) - self.stringcenter(nMr-105, self.nlin+5.5, 'CST') - self.vline(nMr-117, self.nlin+2, nH) - self.stringcenter(nMr-112.5, self.nlin+5.5, 'NCM/SH') - - nWidth_Prod = nMr-135-self.nLeft-11 - nCol_ = self.nLeft+20 + (nWidth_Prod / 2) - self.stringcenter(nCol_, self.nlin+5.5, 'DESCRIÇÃO DO PRODUTO/SERVIÇO') - - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 5) - nLin = self.nlin+10.5 - - for id in xrange(oPaginator[0], oPaginator[1]): - item = el_det[id] - el_prod = item.find(".//{http://www.portalfiscal.inf.br/nfe}prod") - el_imp = item.find( - ".//{http://www.portalfiscal.inf.br/nfe}imposto") - - el_imp_ICMS = el_imp.find( - ".//{http://www.portalfiscal.inf.br/nfe}ICMS") - el_imp_IPI = el_imp.find( - ".//{http://www.portalfiscal.inf.br/nfe}IPI") - - cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \ - tagtext(oNode=el_imp_ICMS, cTag='CST') - vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC') - vICMS = tagtext(oNode=el_imp_ICMS, cTag='vICMS') - pICMS = tagtext(oNode=el_imp_ICMS, cTag='pICMS') - - vIPI = tagtext(oNode=el_imp_IPI, cTag='vIPI') - pIPI = tagtext(oNode=el_imp_IPI, cTag='pIPI') - - self.stringcenter(nMr-112.5, nLin, - tagtext(oNode=el_prod, cTag='NCM')) - self.stringcenter(nMr-105, nLin, cCST) - self.stringcenter(nMr-99, nLin, - tagtext(oNode=el_prod, cTag='CFOP')) - self.stringcenter(nMr-93, nLin, - 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-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)) - - if vIPI: - self.stringRight(nMr-14.5, nLin, - format_number(vIPI, precision=2)) - if pIPI: - self.stringRight(nMr-0.5, nLin, - format_number(pIPI, precision=2)) - - # Código Item - line_cod = nLin - for des in list_cod_prod[id]: - self.string(self.nLeft+0.2, line_cod, des) - line_cod += nStep - - # Descrição Item - line_desc = nLin - for des in list_desc[id]: - self.string(self.nLeft+15.5, line_desc, des) - line_desc += nStep - - nLin = max(line_cod, line_desc) - self.canvas.setStrokeColor(gray) - self.hline(self.nLeft, nLin-2, self.width-self.nLeft) - self.canvas.setStrokeColor(black) - - self.nlin += nH + 3 - - def adicionais(self, oXML=None): - el_infAdic = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}infAdic") - - self.nlin += 2 - self.canvas.setFont('NimbusSanL-Bold', 6) - self.string(self.nLeft+1, self.nlin+1, 'DADOS ADICIONAIS') - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+1, self.nlin+4, 'INFORMAÇÕES COMPLEMENTARES') - self.string((self.width/2)+1, self.nlin+4, 'RESERVADO AO FISCO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 42) - self.vline(self.width/2, self.nlin+2, 42) - # Conteúdo campos - styles = getSampleStyleSheet() - styleN = styles['Normal'] - styleN.fontSize = 6 - styleN.fontName = 'NimbusSanL-Regu' - styleN.leading = 7 - - fisco = tagtext(oNode=el_infAdic, cTag='infAdFisco') - observacoes = tagtext(oNode=el_infAdic, cTag='infCpl') - if fisco: - observacoes = fisco + ' ' + observacoes - P = Paragraph(observacoes, styles['Normal']) - w, h = P.wrap(92*mm, 32*mm) - altura = (self.height-self.nlin-5)*mm - P.drawOn(self.canvas, (self.nLeft+1)*mm, altura - h) - self.nlin += 36 - - def recibo_entrega(self, oXML=None): - el_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") - el_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") - el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") - el_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") - - # self.nlin = self.height-self.nBottom-18 # 17 altura recibo - nW = 40 - nH = 17 - self.canvas.setLineWidth(.5) - self.rect(self.nLeft, self.nlin, - self.width-(self.nLeft+self.nRight), nH) - self.hline(self.nLeft, self.nlin+8.5, self.width-self.nRight-nW) - self.vline(self.width-self.nRight-nW, self.nlin, nH) - self.vline(self.nLeft+nW, self.nlin+8.5, 8.5) - - # Labels - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+1, self.nlin+10.2, 'DATA DE RECEBIMENTO') - self.string(self.nLeft+41, self.nlin+10.2, - 'IDENTIFICAÇÃO E ASSINATURA DO RECEBEDOR') - self.stringcenter(self.width-self.nRight-(nW/2), self.nlin+2, 'NF-e') - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Bold', 8) - cNF = tagtext(oNode=el_ide, cTag='nNF') - cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") - self.string(self.width-self.nRight-nW+2, self.nlin+8, "Nº %s" % (cNF)) - self.string(self.width-self.nRight-nW+2, self.nlin+14, - u"SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie'))) - - cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi')) - cTotal = format_number(tagtext(oNode=el_total, cTag='vNF'), - precision=2) - - cEnd = tagtext(oNode=el_dest, cTag='xNome') + ' - ' - cEnd += tagtext(oNode=el_dest, cTag='xLgr') + ', ' + tagtext( - oNode=el_dest, cTag='nro') + ', ' - cEnd += tagtext(oNode=el_dest, cTag='xBairro') + ', ' + tagtext( - oNode=el_dest, cTag='xMun') + ' - ' - cEnd += tagtext(oNode=el_dest, cTag='UF') - - cString = u""" - RECEBEMOS DE %s OS PRODUTOS/SERVIÇOS CONSTANTES DA NOTA FISCAL INDICADA - ABAIXO. EMISSÃO: %s VALOR TOTAL: %s - DESTINATARIO: %s""" % (tagtext(oNode=el_emit, cTag='xNome'), - cDt, cTotal, cEnd) - - styles = getSampleStyleSheet() - styleN = styles['Normal'] - styleN.fontName = 'NimbusSanL-Regu' - styleN.fontSize = 6 - styleN.leading = 7 - - P = Paragraph(cString, styleN) - w, h = P.wrap(149*mm, 7*mm) - P.drawOn(self.canvas, (self.nLeft+1)*mm, - ((self.height-self.nlin)*mm) - h) - - self.nlin += 20 - self.hline(self.nLeft, self.nlin, self.width-self.nRight) - self.nlin += 2 - - def newpage(self): - self.nlin = self.nTop - self.Page += 1 - self.canvas.showPage() - - def hline(self, x, y, width): - y = self.height - y - self.canvas.line(x*mm, y*mm, width*mm, y*mm) - - def vline(self, x, y, width): - width = self.height - y - width - y = self.height - y - self.canvas.line(x*mm, y*mm, x*mm, width*mm) - - def rect(self, col, lin, nWidth, nHeight, fill=False): - lin = self.height - nHeight - lin - self.canvas.rect(col*mm, lin*mm, nWidth*mm, nHeight*mm, - stroke=True, fill=fill) - - def string(self, x, y, value): - y = self.height - y - self.canvas.drawString(x*mm, y*mm, value) - - def stringRight(self, x, y, value): - y = self.height - y - self.canvas.drawRightString(x*mm, y*mm, value) - - def stringcenter(self, x, y, value): - y = self.height - y - self.canvas.drawCentredString(x*mm, y*mm, value) - - def writeto_pdf(self, fileObj): - pdf_out = self.oPDF_IO.getvalue() - self.oPDF_IO.close() - fileObj.write(pdf_out) +# -*- coding: utf-8 -*- +# © 2017 Edson Bernardino, ITK Soft +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Classe para geração de PDF da DANFE a partir de xml etree.fromstring + + +from io import BytesIO +from textwrap import wrap + +from reportlab.lib import utils +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm, cm +from reportlab.lib.pagesizes import A4 +from reportlab.lib.colors import black, gray +from reportlab.graphics.barcode import code128 +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.enums import TA_CENTER +from reportlab.platypus import Paragraph, Image + + +def chunks(cString, nLen): + for start in range(0, len(cString), nLen): + yield cString[start:start+nLen] + + +def format_cnpj_cpf(value): + if len(value) < 12: # CPF + cValue = '%s.%s.%s-%s' % (value[:-8], value[-8:-5], + value[-5:-2], value[-2:]) + else: + cValue = '%s.%s.%s/%s-%s' % (value[:-12], value[-12:-9], + value[-9:-6], value[-6:-2], value[-2:]) + return cValue + + +def getdateUTC(cDateUTC): + cDt = cDateUTC[0:10].split('-') + cDt.reverse() + return '/'.join(cDt), cDateUTC[11:16] + + +def format_number(cNumber, precision=0, group_sep='.', decimal_sep=','): + if cNumber: + number = float(cNumber) + return ("{:,." + str(precision) + "f}").format(number).\ + replace(",", "X").replace(".", ",").replace("X", ".") + return "" + + +def tagtext(oNode=None, cTag=None): + try: + xpath = ".//{http://www.portalfiscal.inf.br/nfe}%s" % (cTag) + cText = oNode.find(xpath).text + except: + cText = '' + return cText + +REGIME_TRIBUTACAO = { + '1': 'Simples Nacional', + '2': 'Simples Nacional, excesso sublimite de receita bruta', + '3': 'Regime Normal' +} + + +def get_image(path, width=1*cm): + img = utils.ImageReader(path) + iw, ih = img.getSize() + aspect = ih / float(iw) + return Image(path, width=width, height=(width * aspect)) + + +class danfe(object): + def __init__(self, sizepage=A4, list_xml=None, recibo=True, + orientation='portrait', logo=None): + self.width = 210 # 21 x 29,7cm + self.height = 297 + self.nLeft = 10 + self.nRight = 10 + self.nTop = 7 + self.nBottom = 8 + self.nlin = self.nTop + self.logo = logo + self.oFrete = {'0': '0 - Emitente', + '1': '1 - Dest/Remet', + '2': '2 - Terceiros', + '9': '9 - Sem Frete'} + + self.oPDF_IO = BytesIO() + if orientation == 'landscape': + raise NameError('Rotina não implementada') + else: + size = sizepage + + self.canvas = canvas.Canvas(self.oPDF_IO, pagesize=size) + self.canvas.setTitle('DANFE') + self.canvas.setStrokeColor(black) + + for oXML in list_xml: + oXML_cobr = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}cobr") + + self.NrPages = 1 + self.Page = 1 + + # Calculando total linhas usadas para descrições dos itens + # Com bloco fatura, apenas 29 linhas para itens na primeira folha + nNr_Lin_Pg_1 = 34 if oXML_cobr is None else 30 + # [ rec_ini , rec_fim , lines , limit_lines ] + oPaginator = [[0, 0, 0, nNr_Lin_Pg_1]] + el_det = oXML.findall(".//{http://www.portalfiscal.inf.br/nfe}det") + if el_det is not None: + list_desc = [] + list_cod_prod = [] + nPg = 0 + for nId, item in enumerate(el_det): + el_prod = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}prod") + infAdProd = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdProd") + + list_ = wrap(tagtext(oNode=el_prod, cTag='xProd'), 56) + if infAdProd is not None: + list_.extend(wrap(infAdProd.text, 56)) + list_desc.append(list_) + + list_cProd = wrap(tagtext(oNode=el_prod, cTag='cProd'), 14) + list_cod_prod.append(list_cProd) + + # Nr linhas necessárias p/ descrição item + nLin_Itens = len(list_) + + if (oPaginator[nPg][2] + nLin_Itens) >= oPaginator[nPg][3]: + oPaginator.append([0, 0, 0, 77]) + nPg += 1 + oPaginator[nPg][0] = nId + oPaginator[nPg][1] = nId + 1 + oPaginator[nPg][2] = nLin_Itens + else: + # adiciona-se 1 pelo funcionamento de xrange + oPaginator[nPg][1] = nId + 1 + oPaginator[nPg][2] += nLin_Itens + + self.NrPages = len(oPaginator) # Calculando nr. páginas + + if recibo: + self.recibo_entrega(oXML=oXML) + + self.ide_emit(oXML=oXML) + self.destinatario(oXML=oXML) + + if oXML_cobr is not None: + self.faturas(oXML=oXML_cobr) + + self.impostos(oXML=oXML) + self.transportes(oXML=oXML) + self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPaginator[0], + list_desc=list_desc, list_cod_prod=list_cod_prod) + + self.adicionais(oXML=oXML) + + # Gera o restante das páginas do XML + for oPag in oPaginator[1:]: + self.newpage() + self.ide_emit(oXML=oXML) + self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPag, + list_desc=list_desc, nHeight=77, + list_cod_prod=list_cod_prod) + + self.newpage() + + self.canvas.save() + + def ide_emit(self, oXML=None): + elem_infNFe = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}infNFe") + elem_protNFe = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}protNFe") + elem_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") + elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + + cChave = elem_infNFe.attrib.get('Id')[3:] + barcode128 = code128.Code128(cChave, barHeight=10*mm, barWidth=0.25*mm) + + self.canvas.setLineWidth(.5) + self.rect(self.nLeft, self.nlin+1, self.nLeft+75, 32) + self.rect(self.nLeft+115, self.nlin+1, + self.width-self.nLeft-self.nRight-115, 39) + + self.hline(self.nLeft+85, self.nlin+1, 125) + + self.rect(self.nLeft+116, self.nlin+15, + self.width-self.nLeft-self.nRight-117, 6) + + self.rect(self.nLeft, self.nlin+33, + self.width-self.nLeft-self.nRight, 14) + self.hline(self.nLeft, self.nlin+40, self.width-self.nRight) + self.vline(self.nLeft+60, self.nlin+40, 7) + self.vline(self.nLeft+100, self.nlin+40, 7) + + # Labels + self.canvas.setFont('NimbusSanL-Bold', 12) + self.stringcenter(self.nLeft+98, self.nlin+5, 'DANFE') + self.stringcenter(self.nLeft+109, self.nlin+19.5, + tagtext(oNode=elem_ide, cTag='tpNF')) + self.canvas.setFont('NimbusSanL-Bold', 8) + cNF = tagtext(oNode=elem_ide, cTag='nNF') + cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") + self.stringcenter(self.nLeft+100, self.nlin+25, "Nº %s" % (cNF)) + + self.stringcenter(self.nLeft+100, self.nlin+29, "SÉRIE %s" % ( + tagtext(oNode=elem_ide, cTag='serie'))) + cPag = "Página %s de %s" % (str(self.Page), str(self.NrPages)) + self.stringcenter(self.nLeft+100, self.nlin+32, cPag) + self.canvas.setFont('NimbusSanL-Regu', 6) + 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+16, '0 - Entrada') + self.string(self.nLeft+86, self.nlin+19, '1 - Saída') + self.rect(self.nLeft+105, self.nlin+15, 8, 6) + + self.stringcenter( + self.nLeft+152, self.nlin+25, + 'Consulta de autenticidade no portal nacional da NF-e') + self.stringcenter( + self.nLeft+152, self.nlin+28, + 'www.nfe.fazenda.gov.br/portal ou no site da SEFAZ Autorizadora') + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+117, self.nlin+16.7, 'CHAVE DE ACESSO') + self.string(self.nLeft+116, self.nlin+2.7, 'CONTROLE DO FISCO') + + self.string(self.nLeft+1, self.nlin+34.7, 'NATUREZA DA OPERAÇÃO') + self.string(self.nLeft+116, self.nlin+34.7, + 'PROTOCOLO DE AUTORIZAÇÃO DE USO') + self.string(self.nLeft+1, self.nlin+41.7, 'INSCRIÇÃO ESTADUAL') + self.string(self.nLeft+61, self.nlin+41.7, + 'INSCRIÇÃO ESTADUAL DO SUBST. TRIB.') + self.string(self.nLeft+101, self.nlin+41.7, 'CNPJ') + + # Conteúdo campos + barcode128.drawOn(self.canvas, (self.nLeft+111.5)*mm, + (self.height-self.nlin-14)*mm) + self.canvas.setFont('NimbusSanL-Bold', 6) + nW_Rect = (self.width-self.nLeft-self.nRight-117) / 2 + self.stringcenter(self.nLeft+116.5+nW_Rect, self.nlin+19.5, + ' '.join(chunks(cChave, 4))) # Chave + self.canvas.setFont('NimbusSanL-Regu', 8) + cDt, cHr = getdateUTC(tagtext(oNode=elem_protNFe, cTag='dhRecbto')) + cProtocolo = tagtext(oNode=elem_protNFe, cTag='nProt') + cDt = cProtocolo + ' - ' + cDt + ' ' + cHr + nW_Rect = (self.width-self.nLeft-self.nRight-110) / 2 + self.stringcenter(self.nLeft+115+nW_Rect, self.nlin+38.7, cDt) + self.canvas.setFont('NimbusSanL-Regu', 8) + self.string(self.nLeft+1, self.nlin+38.7, + tagtext(oNode=elem_ide, cTag='natOp')) + self.string(self.nLeft+1, self.nlin+46, + tagtext(oNode=elem_emit, cTag='IE')) + self.string(self.nLeft+101, self.nlin+46, + format_cnpj_cpf(tagtext(oNode=elem_emit, cTag='CNPJ'))) + + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleN.fontSize = 10 + styleN.fontName = 'NimbusSanL-Bold' + styleN.alignment = TA_CENTER + + # Razão Social emitente + P = Paragraph(tagtext(oNode=elem_emit, cTag='xNome'), styleN) + w, h = P.wrap(55*mm, 50*mm) + P.drawOn(self.canvas, (self.nLeft+30)*mm, + (self.height-self.nlin-12)*mm) + + if self.logo: + img = get_image(self.logo, width=2*cm) + img.drawOn(self.canvas, (self.nLeft+5)*mm, + (self.height-self.nlin-22)*mm) + + cEnd = tagtext(oNode=elem_emit, cTag='xLgr') + ', ' + tagtext( + oNode=elem_emit, cTag='nro') + ' - ' + cEnd += tagtext(oNode=elem_emit, cTag='xBairro') + '
' + tagtext( + oNode=elem_emit, cTag='xMun') + ' - ' + cEnd += 'Fone: ' + tagtext(oNode=elem_emit, cTag='fone') + '
' + cEnd += tagtext(oNode=elem_emit, cTag='UF') + ' - ' + tagtext( + oNode=elem_emit, cTag='CEP') + + regime = tagtext(oNode=elem_emit, cTag='CRT') + cEnd += '
Regime Tributário: %s' % (REGIME_TRIBUTACAO[regime]) + + styleN.fontName = 'NimbusSanL-Regu' + styleN.fontSize = 7 + styleN.leading = 10 + P = Paragraph(cEnd, styleN) + w, h = P.wrap(55*mm, 30*mm) + P.drawOn(self.canvas, (self.nLeft+30)*mm, + (self.height-self.nlin-31)*mm) + + # Homologação + if tagtext(oNode=elem_ide, cTag='tpAmb') == '2': + self.canvas.saveState() + self.canvas.rotate(90) + self.canvas.setFont('Times-Bold', 40) + self.canvas.setFillColorRGB(0.57, 0.57, 0.57) + self.string(self.nLeft+65, 449, 'SEM VALOR FISCAL') + self.canvas.restoreState() + + self.nlin += 48 + + def destinatario(self, oXML=None): + elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + elem_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") + nMr = self.width-self.nRight + + self.nlin += 1 + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'DESTINATÁRIO/REMETENTE') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 20) + self.vline(nMr-25, self.nlin+2, 20) + self.hline(self.nLeft, self.nlin+8.66, self.width-self.nLeft) + self.hline(self.nLeft, self.nlin+15.32, self.width-self.nLeft) + self.vline(nMr-70, self.nlin+2, 6.66) + self.vline(nMr-53, self.nlin+8.66, 6.66) + self.vline(nMr-99, self.nlin+8.66, 6.66) + self.vline(nMr-90, self.nlin+15.32, 6.66) + self.vline(nMr-102, self.nlin+15.32, 6.66) + self.vline(nMr-136, self.nlin+15.32, 6.66) + # Labels/Fields + self.canvas.setFont('NimbusSanL-Bold', 5) + self.string(self.nLeft+1, self.nlin+3.7, 'NOME/RAZÃO SOCIAL') + self.string(nMr-69, self.nlin+3.7, 'CNPJ/CPF') + self.string(nMr-24, self.nlin+3.7, 'DATA DA EMISSÃO') + self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') + self.string(nMr-98, self.nlin+10.3, 'BAIRRO/DISTRITO') + self.string(nMr-52, self.nlin+10.3, 'CEP') + self.string(nMr-24, self.nlin+10.3, 'DATA DE ENTRADA/SAÍDA') + self.string(self.nLeft+1, self.nlin+17.1, 'MUNICÍPIO') + self.string(nMr-135, self.nlin+17.1, 'FONE/FAX') + self.string(nMr-101, self.nlin+17.1, 'UF') + self.string(nMr-89, self.nlin+17.1, 'INSCRIÇÃO ESTADUAL') + self.string(nMr-24, self.nlin+17.1, 'HORA DE ENTRADA/SAÍDA') + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 8) + self.string(self.nLeft+1, self.nlin+7.5, + tagtext(oNode=elem_dest, cTag='xNome')) + self.string(nMr-69, self.nlin+7.5, + format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CNPJ'))) + cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhEmi')) + self.string(nMr-24, self.nlin+7.7, cDt + ' ' + cHr) + cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhSaiEnt')) + self.string(nMr-24, self.nlin+14.3, cDt + ' ' + cHr) # Dt saída + cEnd = tagtext(oNode=elem_dest, cTag='xLgr') + ', ' + tagtext( + oNode=elem_dest, cTag='nro') + self.string(self.nLeft+1, self.nlin+14.3, cEnd) + self.string(nMr-98, self.nlin+14.3, + tagtext(oNode=elem_dest, cTag='xBairro')) + self.string(nMr-52, self.nlin+14.3, + tagtext(oNode=elem_dest, cTag='CEP')) + self.string(self.nLeft+1, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='xMun')) + self.string(nMr-135, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='fone')) + self.string(nMr-101, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='UF')) + self.string(nMr-89, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='IE')) + + self.nlin += 24 # Nr linhas ocupadas pelo bloco + + def faturas(self, oXML=None): + + nMr = self.width-self.nRight + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'FATURA') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 13) + self.vline(nMr-47.5, self.nlin+2, 13) + self.vline(nMr-95, self.nlin+2, 13) + self.vline(nMr-142.5, self.nlin+2, 13) + self.hline(nMr-47.5, self.nlin+8.5, self.width-self.nLeft) + # Labels + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(nMr-46.5, self.nlin+3.8, 'CÓDIGO VENDEDOR') + self.string(nMr-46.5, self.nlin+10.2, 'NOME VENDEDOR') + self.string(nMr-93.5, self.nlin+3.8, + 'FATURA VENCIMENTO VALOR') + self.string(nMr-140.5, self.nlin+3.8, + 'FATURA VENCIMENTO VALOR') + self.string(self.nLeft+2, self.nlin+3.8, + 'FATURA VENCIMENTO VALOR') + + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Bold', 6) + nLin = 7 + nPar = 1 + nCol = 0 + nAju = 0 + + line_iter = iter(oXML[1:10]) # Salta elemt 1 e considera os próximos 9 + for oXML_dup in line_iter: + + cDt, cHr = getdateUTC(tagtext(oNode=oXML_dup, cTag='dVenc')) + self.string(self.nLeft+nCol+1, self.nlin+nLin, + tagtext(oNode=oXML_dup, cTag='nDup')) + self.string(self.nLeft+nCol+17, self.nlin+nLin, cDt) + self.stringRight( + self.nLeft+nCol+47, self.nlin+nLin, + format_number(tagtext(oNode=oXML_dup, cTag='vDup'), + precision=2)) + + if nPar == 3: + nLin = 7 + nPar = 1 + nCol += 47 + nAju += 1 + nCol += nAju * (0.3) + else: + nLin += 3.3 + nPar += 1 + + # Campos adicionais XML - Condicionados a existencia de financeiro + elem_infAdic = oXML.getparent().find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdic") + if elem_infAdic is not None: + codvend = elem_infAdic.find( + ".//{http://www.portalfiscal.inf.br/nfe}obsCont\ +[@xCampo='CodVendedor']") + self.string(nMr-46.5, self.nlin+7.7, + tagtext(oNode=codvend, cTag='xTexto')) + vend = elem_infAdic.find(".//{http://www.portalfiscal.inf.br/nfe}\ +obsCont[@xCampo='NomeVendedor']") + self.string(nMr-46.5, self.nlin+14.3, + tagtext(oNode=vend, cTag='xTexto')[:36]) + + self.nlin += 16 # Nr linhas ocupadas pelo bloco + + def impostos(self, oXML=None): + # Impostos + el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") + nMr = self.width-self.nRight + self.nlin += 1 + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'CÁLCULO DO IMPOSTO') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 13) + self.hline(self.nLeft, self.nlin+8.5, self.width-self.nLeft) + self.vline(nMr-35, self.nlin+2, 6.5) + self.vline(nMr-65, self.nlin+2, 6.5) + self.vline(nMr-95, self.nlin+2, 6.5) + self.vline(nMr-125, self.nlin+2, 6.5) + self.vline(nMr-155, self.nlin+2, 6.5) + self.vline(nMr-35, self.nlin+8.5, 6.5) + self.vline(nMr-65, self.nlin+8.5, 6.5) + self.vline(nMr-95, self.nlin+8.5, 6.5) + self.vline(nMr-125, self.nlin+8.5, 6.5) + self.vline(nMr-155, self.nlin+8.5, 6.5) + # Labels + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+1, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS') + self.string(nMr-154, self.nlin+3.8, 'VALOR DO ICMS') + self.string(nMr-124, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS ST') + self.string(nMr-94, self.nlin+3.8, 'VALOR DO ICMS ST') + self.string(nMr-64, self.nlin+3.8, 'VALOR APROX TRIBUTOS') + self.string(nMr-34, self.nlin+3.8, 'VALOR TOTAL DOS PRODUTOS') + + self.string(self.nLeft+1, self.nlin+10.2, 'VALOR DO FRETE') + self.string(nMr-154, self.nlin+10.2, 'VALOR DO SEGURO') + self.string(nMr-124, self.nlin+10.2, 'DESCONTO') + self.string(nMr-94, self.nlin+10.2, 'OUTRAS DESP. ACESSÓRIAS') + self.string(nMr-64, self.nlin+10.2, 'VALOR DO IPI') + self.string(nMr-34, self.nlin+10.2, 'VALOR TOTAL DA NOTA') + + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 8) + self.stringRight( + self.nLeft+34, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vBC'), precision=2)) + self.stringRight( + self.nLeft+64, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vICMS'), precision=2)) + self.stringRight( + self.nLeft+94, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vBCST'), precision=2)) + self.stringRight( + nMr-66, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vST'), precision=2)) + self.stringRight( + nMr-36, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vTotTrib'), + precision=2)) + self.stringRight( + nMr-1, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vProd'), precision=2)) + self.stringRight( + self.nLeft+34, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vFrete'), precision=2)) + self.stringRight( + self.nLeft+64, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vSeg'), precision=2)) + self.stringRight( + self.nLeft+94, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vDesc'), precision=2)) + self.stringRight( + self.nLeft+124, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vOutro'), precision=2)) + self.stringRight( + self.nLeft+154, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vIPI'), precision=2)) + self.stringRight( + nMr-1, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vNF'), precision=2)) + + self.nlin += 17 # Nr linhas ocupadas pelo bloco + + def transportes(self, oXML=None): + el_transp = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}transp") + nMr = self.width-self.nRight + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, + 'TRANSPORTADOR/VOLUMES TRANSPORTADOS') + self.canvas.setFont('NimbusSanL-Regu', 5) + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 20) + self.hline(self.nLeft, self.nlin+8.6, self.width-self.nLeft) + self.hline(self.nLeft, self.nlin+15.2, self.width-self.nLeft) + self.vline(nMr-40, self.nlin+2, 13.2) + self.vline(nMr-49, self.nlin+2, 20) + self.vline(nMr-92, self.nlin+2, 6.6) + self.vline(nMr-120, self.nlin+2, 6.6) + self.vline(nMr-75, self.nlin+2, 6.6) + self.vline(nMr-26, self.nlin+15.2, 6.6) + self.vline(nMr-102, self.nlin+8.6, 6.6) + self.vline(nMr-85, self.nlin+15.2, 6.6) + self.vline(nMr-121, self.nlin+15.2, 6.6) + self.vline(nMr-160, self.nlin+15.2, 6.6) + # Labels/Fields + self.string(nMr-39, self.nlin+3.8, 'CNPJ/CPF') + self.string(nMr-74, self.nlin+3.8, 'PLACA DO VEÍCULO') + self.string(nMr-91, self.nlin+3.8, 'CÓDIGO ANTT') + self.string(nMr-119, self.nlin+3.8, 'FRETE POR CONTA') + self.string(self.nLeft+1, self.nlin+3.8, 'RAZÃO SOCIAL') + self.string(nMr-48, self.nlin+3.8, 'UF') + self.string(nMr-39, self.nlin+10.3, 'INSCRIÇÃO ESTADUAL') + self.string(nMr-48, self.nlin+10.3, 'UF') + self.string(nMr-101, self.nlin+10.3, 'MUNICÍPIO') + self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') + self.string(nMr-48, self.nlin+17, 'PESO BRUTO') + self.string(nMr-25, self.nlin+17, 'PESO LÍQUIDO') + self.string(nMr-84, self.nlin+17, 'NUMERAÇÃO') + self.string(nMr-120, self.nlin+17, 'MARCA') + self.string(nMr-159, self.nlin+17, 'ESPÉCIE') + self.string(self.nLeft+1, self.nlin+17, 'QUANTIDADE') + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 8) + self.string(self.nLeft+1, self.nlin+7.7, + tagtext(oNode=el_transp, cTag='xNome')[:40]) + self.string(self.nLeft+71, self.nlin+7.7, + self.oFrete[tagtext(oNode=el_transp, cTag='modFrete')]) + self.string(nMr-39, self.nlin+7.7, + format_cnpj_cpf(tagtext(oNode=el_transp, cTag='CNPJ'))) + self.string(self.nLeft+1, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='xEnder')[:45]) + self.string(self.nLeft+89, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='xMun')) + self.string(nMr-48, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='UF')) + self.string(nMr-39, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='IE')) + self.string(self.nLeft+1, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='qVol')) + self.string(self.nLeft+31, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='esp')) + self.string(self.nLeft+70, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='marca')) + self.string(self.nLeft+106, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='nVol')) + self.stringRight( + nMr-27, self.nlin+21.2, + format_number(tagtext(oNode=el_transp, cTag='pesoB'), precision=3)) + self.stringRight( + nMr-1, self.nlin+21.2, + format_number(tagtext(oNode=el_transp, cTag='pesoL'), precision=3)) + + self.nlin += 23 + + def produtos(self, oXML=None, el_det=None, oPaginator=None, + list_desc=None, list_cod_prod=None, nHeight=29): + + nMr = self.width-self.nRight + nStep = 2.5 # Passo entre linhas + nH = 7.5 + (nHeight * nStep) # cabeçalho 7.5 + self.nlin += 1 + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'DADOS DO PRODUTO/SERVIÇO') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, nH) + self.hline(self.nLeft, self.nlin+8, self.width-self.nLeft) + + self.canvas.setFont('NimbusSanL-Regu', 5.5) + # Colunas + self.vline(self.nLeft+15, self.nlin+2, nH) + self.stringcenter(self.nLeft+7.5, self.nlin+5.5, 'CÓDIGO') + self.vline(nMr-7, self.nlin+2, nH) + self.stringcenter(nMr-3.5, self.nlin+4.5, 'ALÍQ') + self.stringcenter(nMr-3.5, self.nlin+6.5, 'IPI') + self.vline(nMr-14, self.nlin+2, nH) + self.stringcenter(nMr-10.5, self.nlin+4.5, 'ALÍQ') + self.stringcenter(nMr-10.5, self.nlin+6.5, 'ICMS') + self.vline(nMr-26, self.nlin+2, nH) + self.stringcenter(nMr-20, self.nlin+5.5, 'VLR. IPI') + self.vline(nMr-38, self.nlin+2, nH) + self.stringcenter(nMr-32, self.nlin+5.5, 'VLR. ICMS') + self.vline(nMr-50, self.nlin+2, nH) + self.stringcenter(nMr-44, self.nlin+5.5, 'BC ICMS') + self.vline(nMr-64, self.nlin+2, nH) + self.stringcenter(nMr-57, self.nlin+5.5, 'VLR TOTAL') + self.vline(nMr-77, self.nlin+2, nH) + self.stringcenter(nMr-70.5, self.nlin+5.5, 'VLR UNIT') + self.vline(nMr-90, self.nlin+2, nH) + self.stringcenter(nMr-83.5, self.nlin+5.5, 'QTD') + self.vline(nMr-96, self.nlin+2, nH) + self.stringcenter(nMr-93, self.nlin+5.5, 'UNID') + self.vline(nMr-102, self.nlin+2, nH) + self.stringcenter(nMr-99, self.nlin+5.5, 'CFOP') + self.vline(nMr-108, self.nlin+2, nH) + self.stringcenter(nMr-105, self.nlin+5.5, 'CST') + self.vline(nMr-117, self.nlin+2, nH) + self.stringcenter(nMr-112.5, self.nlin+5.5, 'NCM/SH') + + nWidth_Prod = nMr-135-self.nLeft-11 + nCol_ = self.nLeft+20 + (nWidth_Prod / 2) + self.stringcenter(nCol_, self.nlin+5.5, 'DESCRIÇÃO DO PRODUTO/SERVIÇO') + + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 5) + nLin = self.nlin+10.5 + + for id in range(oPaginator[0], oPaginator[1]): + item = el_det[id] + el_prod = item.find(".//{http://www.portalfiscal.inf.br/nfe}prod") + el_imp = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}imposto") + + el_imp_ICMS = el_imp.find( + ".//{http://www.portalfiscal.inf.br/nfe}ICMS") + el_imp_IPI = el_imp.find( + ".//{http://www.portalfiscal.inf.br/nfe}IPI") + + cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \ + tagtext(oNode=el_imp_ICMS, cTag='CST') + vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC') + vICMS = tagtext(oNode=el_imp_ICMS, cTag='vICMS') + pICMS = tagtext(oNode=el_imp_ICMS, cTag='pICMS') + + vIPI = tagtext(oNode=el_imp_IPI, cTag='vIPI') + pIPI = tagtext(oNode=el_imp_IPI, cTag='pIPI') + + self.stringcenter(nMr-112.5, nLin, + tagtext(oNode=el_prod, cTag='NCM')) + self.stringcenter(nMr-105, nLin, cCST) + self.stringcenter(nMr-99, nLin, + tagtext(oNode=el_prod, cTag='CFOP')) + self.stringcenter(nMr-93, nLin, + 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-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)) + + if vIPI: + self.stringRight(nMr-14.5, nLin, + format_number(vIPI, precision=2)) + if pIPI: + self.stringRight(nMr-0.5, nLin, + format_number(pIPI, precision=2)) + + # Código Item + line_cod = nLin + for des in list_cod_prod[id]: + self.string(self.nLeft+0.2, line_cod, des) + line_cod += nStep + + # Descrição Item + line_desc = nLin + for des in list_desc[id]: + self.string(self.nLeft+15.5, line_desc, des) + line_desc += nStep + + nLin = max(line_cod, line_desc) + self.canvas.setStrokeColor(gray) + self.hline(self.nLeft, nLin-2, self.width-self.nLeft) + self.canvas.setStrokeColor(black) + + self.nlin += nH + 3 + + def adicionais(self, oXML=None): + el_infAdic = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdic") + + self.nlin += 2 + self.canvas.setFont('NimbusSanL-Bold', 6) + self.string(self.nLeft+1, self.nlin+1, 'DADOS ADICIONAIS') + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+1, self.nlin+4, 'INFORMAÇÕES COMPLEMENTARES') + self.string((self.width/2)+1, self.nlin+4, 'RESERVADO AO FISCO') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 42) + self.vline(self.width/2, self.nlin+2, 42) + # Conteúdo campos + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleN.fontSize = 6 + styleN.fontName = 'NimbusSanL-Regu' + styleN.leading = 7 + + fisco = tagtext(oNode=el_infAdic, cTag='infAdFisco') + observacoes = tagtext(oNode=el_infAdic, cTag='infCpl') + if fisco: + observacoes = fisco + ' ' + observacoes + P = Paragraph(observacoes, styles['Normal']) + w, h = P.wrap(92*mm, 32*mm) + altura = (self.height-self.nlin-5)*mm + P.drawOn(self.canvas, (self.nLeft+1)*mm, altura - h) + self.nlin += 36 + + def recibo_entrega(self, oXML=None): + el_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + el_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") + el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") + el_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") + + # self.nlin = self.height-self.nBottom-18 # 17 altura recibo + nW = 40 + nH = 17 + self.canvas.setLineWidth(.5) + self.rect(self.nLeft, self.nlin, + self.width-(self.nLeft+self.nRight), nH) + self.hline(self.nLeft, self.nlin+8.5, self.width-self.nRight-nW) + self.vline(self.width-self.nRight-nW, self.nlin, nH) + self.vline(self.nLeft+nW, self.nlin+8.5, 8.5) + + # Labels + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+1, self.nlin+10.2, 'DATA DE RECEBIMENTO') + self.string(self.nLeft+41, self.nlin+10.2, + 'IDENTIFICAÇÃO E ASSINATURA DO RECEBEDOR') + self.stringcenter(self.width-self.nRight-(nW/2), self.nlin+2, 'NF-e') + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Bold', 8) + cNF = tagtext(oNode=el_ide, cTag='nNF') + cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") + self.string(self.width-self.nRight-nW+2, self.nlin+8, "Nº %s" % (cNF)) + self.string(self.width-self.nRight-nW+2, self.nlin+14, + "SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie'))) + + cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi')) + cTotal = format_number(tagtext(oNode=el_total, cTag='vNF'), + precision=2) + + cEnd = tagtext(oNode=el_dest, cTag='xNome') + ' - ' + cEnd += tagtext(oNode=el_dest, cTag='xLgr') + ', ' + tagtext( + oNode=el_dest, cTag='nro') + ', ' + cEnd += tagtext(oNode=el_dest, cTag='xBairro') + ', ' + tagtext( + oNode=el_dest, cTag='xMun') + ' - ' + cEnd += tagtext(oNode=el_dest, cTag='UF') + + cString = """ + RECEBEMOS DE %s OS PRODUTOS/SERVIÇOS CONSTANTES DA NOTA FISCAL INDICADA + ABAIXO. EMISSÃO: %s VALOR TOTAL: %s + DESTINATARIO: %s""" % (tagtext(oNode=el_emit, cTag='xNome'), + cDt, cTotal, cEnd) + + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleN.fontName = 'NimbusSanL-Regu' + styleN.fontSize = 6 + styleN.leading = 7 + + P = Paragraph(cString, styleN) + w, h = P.wrap(149*mm, 7*mm) + P.drawOn(self.canvas, (self.nLeft+1)*mm, + ((self.height-self.nlin)*mm) - h) + + self.nlin += 20 + self.hline(self.nLeft, self.nlin, self.width-self.nRight) + self.nlin += 2 + + def newpage(self): + self.nlin = self.nTop + self.Page += 1 + self.canvas.showPage() + + def hline(self, x, y, width): + y = self.height - y + self.canvas.line(x*mm, y*mm, width*mm, y*mm) + + def vline(self, x, y, width): + width = self.height - y - width + y = self.height - y + self.canvas.line(x*mm, y*mm, x*mm, width*mm) + + def rect(self, col, lin, nWidth, nHeight, fill=False): + lin = self.height - nHeight - lin + self.canvas.rect(col*mm, lin*mm, nWidth*mm, nHeight*mm, + stroke=True, fill=fill) + + def string(self, x, y, value): + y = self.height - y + self.canvas.drawString(x*mm, y*mm, value) + + def stringRight(self, x, y, value): + y = self.height - y + self.canvas.drawRightString(x*mm, y*mm, value) + + def stringcenter(self, x, y, value): + y = self.height - y + self.canvas.drawCentredString(x*mm, y*mm, value) + + def writeto_pdf(self, fileObj): + pdf_out = self.oPDF_IO.getvalue() + self.oPDF_IO.close() + fileObj.write(pdf_out) diff --git a/pytrustnfe/nfse/assinatura.py b/pytrustnfe/nfse/assinatura.py index b2a8d283..0c65ceec 100644 --- a/pytrustnfe/nfse/assinatura.py +++ b/pytrustnfe/nfse/assinatura.py @@ -2,86 +2,52 @@ # © 2016 Danimar Ribeiro, Trustcode # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from lxml import etree import xmlsec -import libxml2 import os.path +consts = xmlsec.constants + NAMESPACE_SIG = 'http://www.w3.org/2000/09/xmldsig#' class Assinatura(object): - def __init__(self, arquivo, senha): - self.arquivo = arquivo - self.senha = senha + def __init__(self, cert_pem, private_key, password): + self.cert_pem = cert_pem + self.private_key = private_key + self.password = password def _checar_certificado(self): - if not os.path.isfile(self.arquivo): + if not os.path.isfile(self.private_key): raise Exception('Caminho do certificado não existe.') - def _inicializar_cripto(self): - libxml2.initParser() - libxml2.substituteEntitiesDefault(1) - - xmlsec.init() - xmlsec.cryptoAppInit(None) - xmlsec.cryptoInit() - - def _finalizar_cripto(self): - xmlsec.cryptoShutdown() - xmlsec.cryptoAppShutdown() - xmlsec.shutdown() - - libxml2.cleanupParser() - def assina_xml(self, xml, reference): self._checar_certificado() - self._inicializar_cripto() - try: - doc_xml = libxml2.parseMemory( - xml, len(xml)) - - signNode = xmlsec.TmplSignature(doc_xml, - xmlsec.transformInclC14NId(), - xmlsec.transformRsaSha1Id(), None) - - doc_xml.getRootElement().addChild(signNode) - refNode = signNode.addReference(xmlsec.transformSha1Id(), - None, reference, None) + template = etree.fromstring(xml) - refNode.addTransform(xmlsec.transformEnvelopedId()) - refNode.addTransform(xmlsec.transformInclC14NId()) - keyInfoNode = signNode.ensureKeyInfo() - keyInfoNode.addX509Data() + key = xmlsec.Key.from_file( + self.private_key, format=xmlsec.constants.KeyDataFormatPem, + password=self.password) - dsig_ctx = xmlsec.DSigCtx() - chave = xmlsec.cryptoAppKeyLoad(filename=str(self.arquivo), - format=xmlsec.KeyDataFormatPkcs12, - pwd=str(self.senha), - pwdCallback=None, - pwdCallbackCtx=None) + signature_node = xmlsec.template.create( + template, c14n_method=consts.TransformInclC14N, + sign_method=consts.TransformRsaSha1) + template.append(signature_node) + ref = xmlsec.template.add_reference( + signature_node, consts.TransformSha1, uri='') - dsig_ctx.signKey = chave - dsig_ctx.sign(signNode) + xmlsec.template.add_transform(ref, consts.TransformEnveloped) + xmlsec.template.add_transform(ref, consts.TransformInclC14N) - status = dsig_ctx.status - dsig_ctx.destroy() + ki = xmlsec.template.ensure_key_info(signature_node) + xmlsec.template.add_x509_data(ki) - if status != xmlsec.DSigStatusSucceeded: - raise RuntimeError( - 'Erro ao realizar a assinatura do arquivo; status: "' + - str(status) + - '"') + ctx = xmlsec.SignatureContext() + ctx.key = key - xpath = doc_xml.xpathNewContext() - xpath.xpathRegisterNs('sig', NAMESPACE_SIG) - certificados = xpath.xpathEval( - '//sig:X509Data/sig:X509Certificate') - for i in range(len(certificados) - 1): - certificados[i].unlinkNode() - certificados[i].freeNode() + ctx.key.load_cert_from_file( + self.cert_pem, consts.KeyDataFormatPem) - xml = doc_xml.serialize() - return xml - finally: - doc_xml.freeDoc() + ctx.sign(signature_node) + return etree.tostring(template, encoding=str) diff --git a/pytrustnfe/nfse/betha/__init__.py b/pytrustnfe/nfse/betha/__init__.py index f2517fa3..67ae42d5 100644 --- a/pytrustnfe/nfse/betha/__init__.py +++ b/pytrustnfe/nfse/betha/__init__.py @@ -50,7 +50,7 @@ def _send(certificado, method, **kwargs): try: response = getattr(client.service, method)(1, xml_send) - except suds.WebFault, e: + except suds.WebFault as e: return { 'sent_xml': xml_send, 'received_xml': e.fault.faultstring, diff --git a/pytrustnfe/nfse/ginfes/__init__.py b/pytrustnfe/nfse/ginfes/__init__.py index f2655dca..e662f2f5 100644 --- a/pytrustnfe/nfse/ginfes/__init__.py +++ b/pytrustnfe/nfse/ginfes/__init__.py @@ -39,7 +39,7 @@ def _send(certificado, method, **kwargs): xml_send = kwargs['xml'] header = '3' #noqa response = getattr(client.service, method)(header, xml_send) - except suds.WebFault, e: + except suds.WebFault as e: return { 'sent_xml': xml_send, 'received_xml': e.fault.faultstring, diff --git a/pytrustnfe/nfse/paulistana/__init__.py b/pytrustnfe/nfse/paulistana/__init__.py index 48818e86..ed0611b5 100644 --- a/pytrustnfe/nfse/paulistana/__init__.py +++ b/pytrustnfe/nfse/paulistana/__init__.py @@ -18,10 +18,10 @@ def sign_tag(certificado, **kwargs): if 'nfse' in kwargs: for item in kwargs['nfse']['lista_rps']: signed = crypto.sign(key, item['assinatura'], 'SHA1') - item['assinatura'] = b64encode(signed) + item['assinatura'] = b64encode(signed).decode() if 'cancelamento' in kwargs: signed = crypto.sign(key, kwargs['cancelamento']['assinatura'], 'SHA1') - kwargs['cancelamento']['assinatura'] = b64encode(signed) + kwargs['cancelamento']['assinatura'] = b64encode(signed).decode() def _send(certificado, method, **kwargs): @@ -42,13 +42,12 @@ def _send(certificado, method, **kwargs): cert, key = save_cert_key(cert, key) client = get_authenticated_client(base_url, cert, key) - pfx_path = certificado.save_pfx() - signer = Assinatura(pfx_path, certificado.password) + signer = Assinatura(cert, key, certificado.password) xml_send = signer.assina_xml(xml_send, '') try: response = getattr(client.service, method)(1, xml_send) - except suds.WebFault, e: + except suds.WebFault as e: return { 'sent_xml': xml_send, 'received_xml': e.fault.faultstring, diff --git a/pytrustnfe/nfse/susesu/__init__.py b/pytrustnfe/nfse/susesu/__init__.py index 2295bb2f..557a168f 100644 --- a/pytrustnfe/nfse/susesu/__init__.py +++ b/pytrustnfe/nfse/susesu/__init__.py @@ -29,7 +29,7 @@ def _send(method, **kwargs): 'sent_xml': xml_send, 'received_xml': e.fault.faultstring, } - result = unicode(result) + result = str(result) result = unicodedata.normalize('NFKD', result).encode('ascii', 'ignore') return { 'sent_xml': xml_send, diff --git a/pytrustnfe/test/XMLs/paulistana_canc_errado.xml b/pytrustnfe/test/XMLs/paulistana_canc_errado.xml index fc1aeaef..4fd39b49 100644 --- a/pytrustnfe/test/XMLs/paulistana_canc_errado.xml +++ b/pytrustnfe/test/XMLs/paulistana_canc_errado.xml @@ -1,4 +1,3 @@ - true265436451212213329001632016-08-29T10:52:15101.3552382446APR9MJR5128216 +true265436451212213329001632016-08-29T10:52:15101.3552382446APR9MJR5128216 diff --git a/pytrustnfe/test/XMLs/paulistana_signature.xml b/pytrustnfe/test/XMLs/paulistana_signature.xml index 013053ec..c4f8cbc0 100644 --- a/pytrustnfe/test/XMLs/paulistana_signature.xml +++ b/pytrustnfe/test/XMLs/paulistana_signature.xml @@ -1,8 +1,4 @@ - -12345678901234false2016-08-292016-08-291E4fpHYkQa7Naxn6IKGb7NwwZu5tPk/KXJ9hCwtZgq0xvKS450aQqqBL+7Iv46lTgqrSMu7+gLrl+LC1qs/8aT2mbHE8uaVFSbzwZ+sF/BkcT6nsFHLMswEiTAEs95Jb7hN1cC91xqQGRH4buw0TzxHKmhuLJ22WwtG/scxyKtjM=12345611RPS2016-08-29NT0.000.000.000.000.00074985.00false - - - 123456Trustcode1Vinicius de Moraes, 4242CorregoFloripaSC88037240Venda de servico +12345678901234false2016-08-292016-08-291E4fpHYkQa7Naxn6IKGb7NwwZu5tPk/KXJ9hCwtZgq0xvKS450aQqqBL+7Iv46lTgqrSMu7+gLrl+LC1qs/8aT2mbHE8uaVFSbzwZ+sF/BkcT6nsFHLMswEiTAEs95Jb7hN1cC91xqQGRH4buw0TzxHKmhuLJ22WwtG/scxyKtjM=12345611RPS2016-08-29NT0.000.000.000.000.00074985.00false123456Trustcode1Vinicius de Moraes, 4242CorregoFloripaSC88037240Venda de servico @@ -12,12 +8,12 @@ -ivaOwkcrt0pfuMYsAdfyLaUAcIk= +ePJnD6hyDvlJo08PFX8h2TXk0ZM= -FjIHdfPavSEyaWYhAT0z0shPLuTsqBKyy78PUEZ8PUhTZ+iSV0MOvAIRq9MPPVK9 -jjXOw1TE903uSK8aJon52RNKPd68ORVJ3bKFSjTqQLxFRR9tiiAQFrWDETf7FF89 -EhG6dy6TGcgVbOyn0Jqm8MkqrE1XrJ44orN1X+Jt+7U= +GbaQaTEtxuKdRRaadginWPFH5K65ywqEikkwChWO3xX5Kglq8RPm4+LjnpJmuTcE +9I2BVon3GJFh+c/6RKzJPose6FXog2xnCpTOgwA/rks/gKsUAaRlXCPsLcKMKaOj +3eH21RHEyrxBAbdpEUdlEgQWaWzmGq009EiQ544sD6c= MIICMTCCAZqgAwIBAgIQfYOsIEVuAJ1FwwcTrY0t1DANBgkqhkiG9w0BAQUFADBX @@ -34,4 +30,4 @@ QtgAhuZM9rxpOJuNKc+pM29EixpAiZZiRMCSWEItNyEVdUIi+YnKBcAHd88TwO86 d126MWQ2O8cu5W1VoDp7hYBYKOnLbYi11/StO+0rzK+oPYAvIw== - + \ No newline at end of file diff --git a/pytrustnfe/test/test_add_qr_code.py b/pytrustnfe/test/test_add_qr_code.py index 17965c9c..da9e3811 100644 --- a/pytrustnfe/test/test_add_qr_code.py +++ b/pytrustnfe/test/test_add_qr_code.py @@ -12,7 +12,7 @@ def setUp(self): self.xml_sem_qrcode = open('pytrustnfe/test/xml_sem_qrcode.xml', 'r') self.xml_com_qrcode = open('pytrustnfe/test/xml_com_qrcode.xml', 'r') dhEmi = '2016-11-09T16:03:25-00:00' - chave_nfe = u'NFe35161121332917000163650010000000011448875034' + chave_nfe = 'NFe35161121332917000163650010000000011448875034' ambiente = 2 valor_total = '324.00' icms_total = '61.56' diff --git a/pytrustnfe/test/test_assinatura.py b/pytrustnfe/test/test_assinatura.py index 380e1fd3..f13b8517 100644 --- a/pytrustnfe/test/test_assinatura.py +++ b/pytrustnfe/test/test_assinatura.py @@ -11,16 +11,14 @@ from pytrustnfe.nfe.assinatura import Assinatura -XML_ASSINAR = '' \ - '' \ +XML_ASSINAR = '' \ ' '\ ' Hello, World!' \ ' ' \ '' -XML_ERRADO = '' \ - '' \ +XML_ERRADO = '' \ ' ' \ ' Hello, World!' \ ' ' \ @@ -32,21 +30,21 @@ class test_assinatura(unittest.TestCase): caminho = os.path.dirname(__file__) def test_assinar_xml_senha_invalida(self): - pfx = open(os.path.join(self.caminho, 'teste.pfx')).read() + pfx = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() signer = Assinatura(pfx, '123') self.assertRaises(Exception, signer.assina_xml, signer, etree.fromstring(XML_ASSINAR), 'NFe43150602261542000143550010000000761792265342') def test_assinar_xml_invalido(self): - pfx = open(os.path.join(self.caminho, 'teste.pfx')).read() + pfx = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() signer = Assinatura(pfx, '123456') self.assertRaises(Exception, signer.assina_xml, signer, etree.fromstring(XML_ERRADO), 'NFe43150602261542000143550010000000761792265342') def test_assinar_xml_valido(self): - pfx = open(os.path.join(self.caminho, 'teste.pfx')).read() + pfx = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() signer = Assinatura(pfx, '123456') xml = signer.assina_xml( etree.fromstring(XML_ASSINAR), diff --git a/pytrustnfe/test/test_certificado.py b/pytrustnfe/test/test_certificado.py index 2e7f248c..e05a8f07 100644 --- a/pytrustnfe/test/test_certificado.py +++ b/pytrustnfe/test/test_certificado.py @@ -49,21 +49,21 @@ class test_assinatura(unittest.TestCase): caminho = os.path.dirname(__file__) def test_preparar_pfx(self): - dir_pfx = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + dir_pfx = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() cert, key = extract_cert_and_key_from_pfx(dir_pfx, '123456') self.assertEqual(key, CHAVE, 'Chave gerada inválida') self.assertEqual(cert, CERTIFICADO, 'Certificado inválido') def test_save_pfx(self): - pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() pfx = Certificado(pfx_source, '123') path = pfx.save_pfx() - saved = open(path, 'r').read() + saved = open(path, 'rb').read() self.assertEqual(pfx_source, saved, 'Arquivo pfx salvo não bate com arquivo lido') def test_save_cert_and_key(self): - dir_pfx = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + dir_pfx = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() cert, key = extract_cert_and_key_from_pfx(dir_pfx, '123456') cert_path, key_path = save_cert_key(cert, key) cert_saved = open(cert_path, 'r').read() diff --git a/pytrustnfe/test/test_consulta_cadastro.py b/pytrustnfe/test/test_consulta_cadastro.py index 9c63f03b..a284b567 100644 --- a/pytrustnfe/test/test_consulta_cadastro.py +++ b/pytrustnfe/test/test_consulta_cadastro.py @@ -12,7 +12,7 @@ class test_consulta_cadastro(unittest.TestCase): caminho = os.path.dirname(__file__) def test_conta_de_cadastro(self): - pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() pfx = Certificado(pfx_source, '123456') obj = {'cnpj': '12345678901234', 'estado': '42'} diff --git a/pytrustnfe/test/test_danfe.py b/pytrustnfe/test/test_danfe.py index e7385c73..a8548587 100644 --- a/pytrustnfe/test/test_danfe.py +++ b/pytrustnfe/test/test_danfe.py @@ -22,5 +22,5 @@ def test_can_generate_danfe(self): # Para testar localmente o Danfe # with open('/home/danimar/danfe.pdf', 'w') as oFile: - with tempfile.TemporaryFile(mode='w') as oFile: + with tempfile.TemporaryFile(mode='wb') as oFile: oDanfe.writeto_pdf(oFile) diff --git a/pytrustnfe/test/test_ginfes.py b/pytrustnfe/test/test_ginfes.py index d2895f22..6d23e5ea 100644 --- a/pytrustnfe/test/test_ginfes.py +++ b/pytrustnfe/test/test_ginfes.py @@ -13,7 +13,7 @@ class test_nfse_ginfes(unittest.TestCase): @unittest.skip def test_consulta_situacao_lote(self): - pfx_source = open('/home/danimar/Downloads/machado.pfx', 'r').read() + pfx_source = open('/home/danimar/Downloads/machado.pfx', 'rb').read() pfx = Certificado(pfx_source, '123456789') dados = {'ambiente': 'homologacao'} diff --git a/pytrustnfe/test/test_nfse_paulistana.py b/pytrustnfe/test/test_nfse_paulistana.py index 0ed4ded7..06d42005 100644 --- a/pytrustnfe/test/test_nfse_paulistana.py +++ b/pytrustnfe/test/test_nfse_paulistana.py @@ -54,7 +54,7 @@ def _get_nfse(self): return nfse def test_envio_nfse(self): - pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() pfx = Certificado(pfx_source, '123456') nfse = self._get_nfse() @@ -77,7 +77,7 @@ def test_envio_nfse(self): retorno['object'].ChaveNFeRPS.ChaveRPS.NumeroRPS, 6) def test_nfse_signature(self): - pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() pfx = Certificado(pfx_source, '123456') nfse = self._get_nfse() @@ -103,7 +103,7 @@ def _get_cancelamento(self): } def test_cancelamento_nfse_ok(self): - pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() pfx = Certificado(pfx_source, '123456') cancelamento = self._get_cancelamento() @@ -122,7 +122,7 @@ def test_cancelamento_nfse_ok(self): self.assertEqual(retorno['object'].Cabecalho.Sucesso, True) def test_cancelamento_nfse_com_erro(self): - pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'r').read() + pfx_source = open(os.path.join(self.caminho, 'teste.pfx'), 'rb').read() pfx = Certificado(pfx_source, '123456') cancelamento = self._get_cancelamento() diff --git a/pytrustnfe/test/test_utils.py b/pytrustnfe/test/test_utils.py index 64cf1faa..247327d8 100644 --- a/pytrustnfe/test/test_utils.py +++ b/pytrustnfe/test/test_utils.py @@ -60,7 +60,7 @@ def test_chave_nfe(self): chave.validar() chave.cnpj = '1234567891011' self.assertEqual('CNPJ necessário para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') with self.assertRaises(AssertionError) as cm: @@ -68,7 +68,7 @@ def test_chave_nfe(self): chave.validar() chave.estado = '42' self.assertEqual('Estado necessário para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') with self.assertRaises(AssertionError) as cm: @@ -76,7 +76,7 @@ def test_chave_nfe(self): chave.validar() chave.emissao = '0' self.assertEqual('Emissão necessário para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') with self.assertRaises(AssertionError) as cm: @@ -84,7 +84,7 @@ def test_chave_nfe(self): chave.validar() chave.modelo = '55' self.assertEqual('Modelo necessário para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') with self.assertRaises(AssertionError) as cm: @@ -92,7 +92,7 @@ def test_chave_nfe(self): chave.validar() chave.serie = '012' self.assertEqual('Série necessária para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') with self.assertRaises(AssertionError) as cm: @@ -100,7 +100,7 @@ def test_chave_nfe(self): chave.validar() chave.numero = '000000780' self.assertEqual('Número necessário para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') with self.assertRaises(AssertionError) as cm: @@ -108,12 +108,12 @@ def test_chave_nfe(self): chave.validar() chave.tipo = '42' self.assertEqual('Tipo necessário para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') with self.assertRaises(AssertionError) as cm: chave.codigo = '' chave.validar() self.assertEqual('Código necessário para criar chave NF-e', - cm.exception.message, + str(cm.exception), 'Validação da chave nf-e incorreta') diff --git a/pytrustnfe/test/test_xml.py b/pytrustnfe/test/test_xml.py index ad5162fc..45b70b70 100644 --- a/pytrustnfe/test/test_xml.py +++ b/pytrustnfe/test/test_xml.py @@ -26,5 +26,5 @@ def test_xmlfilters(self): self.assertEqual('2016-09-17', format_date(dt.date())) self.assertEqual('2016-09-17T12:12:12', format_datetime(dt)) - word = strip_line_feed(u"olá\ncomo vai\r senhor ") - self.assertEqual(word, u"olá como vai senhor") + word = strip_line_feed("olá\ncomo vai\r senhor ") + self.assertEqual(word, "olá como vai senhor") diff --git a/pytrustnfe/test/test_xml_serializacao.py b/pytrustnfe/test/test_xml_serializacao.py index 64d0a2a3..82f3dbce 100644 --- a/pytrustnfe/test/test_xml_serializacao.py +++ b/pytrustnfe/test/test_xml_serializacao.py @@ -15,13 +15,13 @@ def test_serializacao_default(self): tag2='ola', tag3='comovai') result = open(os.path.join(path, 'jinja_result.xml'), 'r').read() - self.assertEqual(xml + '\n', result) + self.assertEqual(xml + "\n", result) def test_serializacao_remove_empty(self): path = os.path.join(os.path.dirname(__file__), 'XMLs') xmlElem = render_xml(path, 'jinja_template.xml', True, tag1='oi', tag2='ola', tag3='comovai') - xml = etree.tostring(xmlElem) + xml = etree.tostring(xmlElem, encoding=str) result = open(os.path.join(path, 'jinja_remove_empty.xml'), 'r').read() self.assertEqual(xml + '\n', result) @@ -29,7 +29,7 @@ def test_sanitize_response(self): path = os.path.join(os.path.dirname(__file__), 'XMLs') xml_to_clear = open(os.path.join(path, 'jinja_result.xml'), 'r').read() xml, obj = sanitize_response(xml_to_clear) - + print(type(xml)) self.assertEqual(xml, xml_to_clear) self.assertEqual(obj.tpAmb, 'oi') self.assertEqual(obj.CNPJ, 'ola') diff --git a/pytrustnfe/test/xml_com_qrcode.xml b/pytrustnfe/test/xml_com_qrcode.xml index 5731c5f9..22cf1efe 100644 --- a/pytrustnfe/test/xml_com_qrcode.xml +++ b/pytrustnfe/test/xml_com_qrcode.xml @@ -30,11 +30,11 @@ LEL AMBIENTAL LTDA - EPP Zell Ambiental - Rua Padre João + Rua Padre João 444 - Penha de França + Penha de França 3550308 - São Paulo + São Paulo SP 03637000 1058 diff --git a/pytrustnfe/test/xml_sem_qrcode.xml b/pytrustnfe/test/xml_sem_qrcode.xml index 74b8c786..3352a221 100644 --- a/pytrustnfe/test/xml_sem_qrcode.xml +++ b/pytrustnfe/test/xml_sem_qrcode.xml @@ -31,11 +31,11 @@ LEL AMBIENTAL LTDA - EPP Zell Ambiental - Rua Padre João + Rua Padre João 444 - Penha de França + Penha de França 3550308 - São Paulo + São Paulo SP 03637000 1058 diff --git a/pytrustnfe/xml/__init__.py b/pytrustnfe/xml/__init__.py index d3eeeeab..3bc6321d 100644 --- a/pytrustnfe/xml/__init__.py +++ b/pytrustnfe/xml/__init__.py @@ -39,14 +39,14 @@ def render_xml(path, template_name, remove_empty, **nfe): if recursively_empty(elem): parent.remove(elem) return root - return etree.tostring(root) + for element in root.iter("*"): # remove espaços em branco + if element.text is not None and not element.text.strip(): + element.text = None + return etree.tostring(root, encoding=str) def sanitize_response(response): - response = unicode(response) - response = unicodedata.normalize('NFKD', response).encode('ascii', - 'ignore') - + print(response) tree = etree.fromstring(response) # Remove namespaces inuteis na resposta for elem in tree.getiterator(): diff --git a/pytrustnfe/xml/filters.py b/pytrustnfe/xml/filters.py index a9ed6890..afd19ece 100644 --- a/pytrustnfe/xml/filters.py +++ b/pytrustnfe/xml/filters.py @@ -13,24 +13,24 @@ def normalize_str(string): Remove special characters and strip spaces """ if string: - if not isinstance(string, unicode): - string = unicode(string, 'utf-8', 'replace') + if not isinstance(string, str): + string = str(string, 'utf-8', 'replace') string = string.encode('utf-8') return normalize( - 'NFKD', string.decode('utf-8')).encode('ASCII', 'ignore') + 'NFKD', string.decode('utf-8')).encode('ASCII', 'ignore').decode() return '' def strip_line_feed(string): if string: - if not isinstance(string, unicode): - string = unicode(string, 'utf-8', 'replace') + if not isinstance(string, str): + string = str(string, 'utf-8', 'replace') remap = { - ord(u'\t'): u' ', - ord(u'\n'): u' ', - ord(u'\f'): u' ', - ord(u'\r'): None, # Delete + ord('\t'): ' ', + ord('\n'): ' ', + ord('\f'): ' ', + ord('\r'): None, # Delete } return string.translate(remap).strip() return string diff --git a/requirements.txt b/requirements.txt index 2683ae6d..886103ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ lxml >= 3.5.0, < 4 coveralls Jinja2 signxml +urllib3 >= 1.22 suds-jurko >= 0.6 suds-jurko-requests >= 1.0 defusedxml >= 0.4.1, < 0.6 @@ -9,6 +10,7 @@ eight >= 0.3.0, < 0.5 cryptography >= 1.8, < 1.10 pyOpenSSL >= 16.0.0, < 17 certifi >= 2015.11.20.1 +xmlsec >= 1.3.3 reportlab pytest pytest-cov From 0c89b3e957cbceb8eb5c862d04c0d2e92d075631 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Mon, 11 Sep 2017 23:38:06 -0300 Subject: [PATCH 03/31] =?UTF-8?q?Modifica=C3=A7=C3=A3o=20na=20estrutura=20?= =?UTF-8?q?do=20projeto=20-=20Movendo=20testes=20para=20fora=20do=20pacote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {pytrustnfe/test => tests}/XMLs/NFe00000857.xml | 0 .../test => tests}/XMLs/jinja_remove_empty.xml | 0 {pytrustnfe/test => tests}/XMLs/jinja_result.xml | 0 {pytrustnfe/test => tests}/XMLs/jinja_template.xml | 0 {pytrustnfe/test => tests}/XMLs/paulistana_canc.xml | 0 .../test => tests}/XMLs/paulistana_canc_errado.xml | 0 .../test => tests}/XMLs/paulistana_canc_ok.xml | 0 .../test => tests}/XMLs/paulistana_resultado.xml | 0 .../test => tests}/XMLs/paulistana_signature.xml | 0 {pytrustnfe/test => tests}/XMLs/recibo_envio_1.xml | 0 {pytrustnfe/test => tests}/XMLs/recibo_envio_2.xml | 0 .../XMLs/recibo_protocolo_sucesso_1.xml | 0 .../XMLs/recibo_protocolo_sucesso_2.xml | 0 {pytrustnfe/test => tests}/__init__.py | 0 {pytrustnfe/test => tests}/test_add_qr_code.py | 4 ++-- {pytrustnfe/test => tests}/test_assinatura.py | 0 {pytrustnfe/test => tests}/test_certificado.py | 0 {pytrustnfe/test => tests}/test_comunicacao.py | 0 .../test => tests}/test_consulta_cadastro.py | 0 {pytrustnfe/test => tests}/test_danfe.py | 0 {pytrustnfe/test => tests}/test_ginfes.py | 0 {pytrustnfe/test => tests}/test_nfse_paulistana.py | 0 {pytrustnfe/test => tests}/test_servidores.py | 0 {pytrustnfe/test => tests}/test_utils.py | 0 {pytrustnfe/test => tests}/test_xml.py | 0 {pytrustnfe/test => tests}/test_xml_serializacao.py | 0 {pytrustnfe/test => tests}/teste.pfx | Bin {pytrustnfe/test => tests}/xml_assinado.xml | 0 {pytrustnfe/test => tests}/xml_com_qrcode.xml | 0 {pytrustnfe/test => tests}/xml_sem_qrcode.xml | 0 {pytrustnfe/test => tests}/xml_valido_assinado.xml | 0 31 files changed, 2 insertions(+), 2 deletions(-) rename {pytrustnfe/test => tests}/XMLs/NFe00000857.xml (100%) rename {pytrustnfe/test => tests}/XMLs/jinja_remove_empty.xml (100%) rename {pytrustnfe/test => tests}/XMLs/jinja_result.xml (100%) rename {pytrustnfe/test => tests}/XMLs/jinja_template.xml (100%) rename {pytrustnfe/test => tests}/XMLs/paulistana_canc.xml (100%) rename {pytrustnfe/test => tests}/XMLs/paulistana_canc_errado.xml (100%) rename {pytrustnfe/test => tests}/XMLs/paulistana_canc_ok.xml (100%) rename {pytrustnfe/test => tests}/XMLs/paulistana_resultado.xml (100%) rename {pytrustnfe/test => tests}/XMLs/paulistana_signature.xml (100%) rename {pytrustnfe/test => tests}/XMLs/recibo_envio_1.xml (100%) rename {pytrustnfe/test => tests}/XMLs/recibo_envio_2.xml (100%) rename {pytrustnfe/test => tests}/XMLs/recibo_protocolo_sucesso_1.xml (100%) rename {pytrustnfe/test => tests}/XMLs/recibo_protocolo_sucesso_2.xml (100%) rename {pytrustnfe/test => tests}/__init__.py (100%) rename {pytrustnfe/test => tests}/test_add_qr_code.py (87%) rename {pytrustnfe/test => tests}/test_assinatura.py (100%) rename {pytrustnfe/test => tests}/test_certificado.py (100%) rename {pytrustnfe/test => tests}/test_comunicacao.py (100%) rename {pytrustnfe/test => tests}/test_consulta_cadastro.py (100%) rename {pytrustnfe/test => tests}/test_danfe.py (100%) rename {pytrustnfe/test => tests}/test_ginfes.py (100%) rename {pytrustnfe/test => tests}/test_nfse_paulistana.py (100%) rename {pytrustnfe/test => tests}/test_servidores.py (100%) rename {pytrustnfe/test => tests}/test_utils.py (100%) rename {pytrustnfe/test => tests}/test_xml.py (100%) rename {pytrustnfe/test => tests}/test_xml_serializacao.py (100%) rename {pytrustnfe/test => tests}/teste.pfx (100%) rename {pytrustnfe/test => tests}/xml_assinado.xml (100%) rename {pytrustnfe/test => tests}/xml_com_qrcode.xml (100%) rename {pytrustnfe/test => tests}/xml_sem_qrcode.xml (100%) rename {pytrustnfe/test => tests}/xml_valido_assinado.xml (100%) diff --git a/pytrustnfe/test/XMLs/NFe00000857.xml b/tests/XMLs/NFe00000857.xml similarity index 100% rename from pytrustnfe/test/XMLs/NFe00000857.xml rename to tests/XMLs/NFe00000857.xml diff --git a/pytrustnfe/test/XMLs/jinja_remove_empty.xml b/tests/XMLs/jinja_remove_empty.xml similarity index 100% rename from pytrustnfe/test/XMLs/jinja_remove_empty.xml rename to tests/XMLs/jinja_remove_empty.xml diff --git a/pytrustnfe/test/XMLs/jinja_result.xml b/tests/XMLs/jinja_result.xml similarity index 100% rename from pytrustnfe/test/XMLs/jinja_result.xml rename to tests/XMLs/jinja_result.xml diff --git a/pytrustnfe/test/XMLs/jinja_template.xml b/tests/XMLs/jinja_template.xml similarity index 100% rename from pytrustnfe/test/XMLs/jinja_template.xml rename to tests/XMLs/jinja_template.xml diff --git a/pytrustnfe/test/XMLs/paulistana_canc.xml b/tests/XMLs/paulistana_canc.xml similarity index 100% rename from pytrustnfe/test/XMLs/paulistana_canc.xml rename to tests/XMLs/paulistana_canc.xml diff --git a/pytrustnfe/test/XMLs/paulistana_canc_errado.xml b/tests/XMLs/paulistana_canc_errado.xml similarity index 100% rename from pytrustnfe/test/XMLs/paulistana_canc_errado.xml rename to tests/XMLs/paulistana_canc_errado.xml diff --git a/pytrustnfe/test/XMLs/paulistana_canc_ok.xml b/tests/XMLs/paulistana_canc_ok.xml similarity index 100% rename from pytrustnfe/test/XMLs/paulistana_canc_ok.xml rename to tests/XMLs/paulistana_canc_ok.xml diff --git a/pytrustnfe/test/XMLs/paulistana_resultado.xml b/tests/XMLs/paulistana_resultado.xml similarity index 100% rename from pytrustnfe/test/XMLs/paulistana_resultado.xml rename to tests/XMLs/paulistana_resultado.xml diff --git a/pytrustnfe/test/XMLs/paulistana_signature.xml b/tests/XMLs/paulistana_signature.xml similarity index 100% rename from pytrustnfe/test/XMLs/paulistana_signature.xml rename to tests/XMLs/paulistana_signature.xml diff --git a/pytrustnfe/test/XMLs/recibo_envio_1.xml b/tests/XMLs/recibo_envio_1.xml similarity index 100% rename from pytrustnfe/test/XMLs/recibo_envio_1.xml rename to tests/XMLs/recibo_envio_1.xml diff --git a/pytrustnfe/test/XMLs/recibo_envio_2.xml b/tests/XMLs/recibo_envio_2.xml similarity index 100% rename from pytrustnfe/test/XMLs/recibo_envio_2.xml rename to tests/XMLs/recibo_envio_2.xml diff --git a/pytrustnfe/test/XMLs/recibo_protocolo_sucesso_1.xml b/tests/XMLs/recibo_protocolo_sucesso_1.xml similarity index 100% rename from pytrustnfe/test/XMLs/recibo_protocolo_sucesso_1.xml rename to tests/XMLs/recibo_protocolo_sucesso_1.xml diff --git a/pytrustnfe/test/XMLs/recibo_protocolo_sucesso_2.xml b/tests/XMLs/recibo_protocolo_sucesso_2.xml similarity index 100% rename from pytrustnfe/test/XMLs/recibo_protocolo_sucesso_2.xml rename to tests/XMLs/recibo_protocolo_sucesso_2.xml diff --git a/pytrustnfe/test/__init__.py b/tests/__init__.py similarity index 100% rename from pytrustnfe/test/__init__.py rename to tests/__init__.py diff --git a/pytrustnfe/test/test_add_qr_code.py b/tests/test_add_qr_code.py similarity index 87% rename from pytrustnfe/test/test_add_qr_code.py rename to tests/test_add_qr_code.py index da9e3811..77a24f4d 100644 --- a/pytrustnfe/test/test_add_qr_code.py +++ b/tests/test_add_qr_code.py @@ -9,8 +9,8 @@ class TestAddQRCode(unittest.TestCase): def setUp(self): - self.xml_sem_qrcode = open('pytrustnfe/test/xml_sem_qrcode.xml', 'r') - self.xml_com_qrcode = open('pytrustnfe/test/xml_com_qrcode.xml', 'r') + self.xml_sem_qrcode = open('tests/xml_sem_qrcode.xml', 'r') + self.xml_com_qrcode = open('tests/xml_com_qrcode.xml', 'r') dhEmi = '2016-11-09T16:03:25-00:00' chave_nfe = 'NFe35161121332917000163650010000000011448875034' ambiente = 2 diff --git a/pytrustnfe/test/test_assinatura.py b/tests/test_assinatura.py similarity index 100% rename from pytrustnfe/test/test_assinatura.py rename to tests/test_assinatura.py diff --git a/pytrustnfe/test/test_certificado.py b/tests/test_certificado.py similarity index 100% rename from pytrustnfe/test/test_certificado.py rename to tests/test_certificado.py diff --git a/pytrustnfe/test/test_comunicacao.py b/tests/test_comunicacao.py similarity index 100% rename from pytrustnfe/test/test_comunicacao.py rename to tests/test_comunicacao.py diff --git a/pytrustnfe/test/test_consulta_cadastro.py b/tests/test_consulta_cadastro.py similarity index 100% rename from pytrustnfe/test/test_consulta_cadastro.py rename to tests/test_consulta_cadastro.py diff --git a/pytrustnfe/test/test_danfe.py b/tests/test_danfe.py similarity index 100% rename from pytrustnfe/test/test_danfe.py rename to tests/test_danfe.py diff --git a/pytrustnfe/test/test_ginfes.py b/tests/test_ginfes.py similarity index 100% rename from pytrustnfe/test/test_ginfes.py rename to tests/test_ginfes.py diff --git a/pytrustnfe/test/test_nfse_paulistana.py b/tests/test_nfse_paulistana.py similarity index 100% rename from pytrustnfe/test/test_nfse_paulistana.py rename to tests/test_nfse_paulistana.py diff --git a/pytrustnfe/test/test_servidores.py b/tests/test_servidores.py similarity index 100% rename from pytrustnfe/test/test_servidores.py rename to tests/test_servidores.py diff --git a/pytrustnfe/test/test_utils.py b/tests/test_utils.py similarity index 100% rename from pytrustnfe/test/test_utils.py rename to tests/test_utils.py diff --git a/pytrustnfe/test/test_xml.py b/tests/test_xml.py similarity index 100% rename from pytrustnfe/test/test_xml.py rename to tests/test_xml.py diff --git a/pytrustnfe/test/test_xml_serializacao.py b/tests/test_xml_serializacao.py similarity index 100% rename from pytrustnfe/test/test_xml_serializacao.py rename to tests/test_xml_serializacao.py diff --git a/pytrustnfe/test/teste.pfx b/tests/teste.pfx similarity index 100% rename from pytrustnfe/test/teste.pfx rename to tests/teste.pfx diff --git a/pytrustnfe/test/xml_assinado.xml b/tests/xml_assinado.xml similarity index 100% rename from pytrustnfe/test/xml_assinado.xml rename to tests/xml_assinado.xml diff --git a/pytrustnfe/test/xml_com_qrcode.xml b/tests/xml_com_qrcode.xml similarity index 100% rename from pytrustnfe/test/xml_com_qrcode.xml rename to tests/xml_com_qrcode.xml diff --git a/pytrustnfe/test/xml_sem_qrcode.xml b/tests/xml_sem_qrcode.xml similarity index 100% rename from pytrustnfe/test/xml_sem_qrcode.xml rename to tests/xml_sem_qrcode.xml diff --git a/pytrustnfe/test/xml_valido_assinado.xml b/tests/xml_valido_assinado.xml similarity index 100% rename from pytrustnfe/test/xml_valido_assinado.xml rename to tests/xml_valido_assinado.xml From fabe0e05614e6597ec4bfe910e3e608e282b15ec Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Mon, 11 Sep 2017 23:56:54 -0300 Subject: [PATCH 04/31] =?UTF-8?q?Preparando=20pypi=20package=20para=20vers?= =?UTF-8?q?=C3=A3o=20python=20>=3D=203.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- setup.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7377797b..73a0c702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,5 +21,5 @@ deploy: password: secure: wV+DH+WVji4qyCRXxugOsu8/u9MgUN9YggIBozh2Si1z6OlONZVr/oCaJDW8UD+Qg0EF87RbHuEmmlpAZVERAZv5uGsxjSO/NyvAsr99sOlTy9TSLi6TLp4aPnOCgjBTFDWkwkNyDTGYGNfendS7KO2jaHUsr/eDZcpTz42lOfDgpmccz822wwI6Uu1hNC61qlskPkKVzFhHT61/XAgmjHvw1wAMWVmv9/E6J8VAlZoI9/v3K0RTRisB/+0+sSvY86crYyuW/zIEhQJnMu/gfFWDSxNdY+0S3VyFgERn5S7IYlpBPUUlukX5aPXy+OQD2ygeu7w9f6aOSaJZsoyhe4pPXDjA9XNyfiazuZrz51fzhricMvdsMPAcukK/sJzGICAFgOutAjy+nGBkNqA2genKL8gMtJGUrPW5Yq5MGMC7FEgEQi5SgEj+01FgSY5mHlR3qo9bEgXWcxhNL/uZ3C1ElnGNLbyn5hjWzCnMEe70JwfWNQxGgtNm73vrrsZJ7M5wGjrEKVAvTERQegRQm2ObX7YsPmTY+tF15Hxs8GiZ0T/MzpxGe6yAkIutKI0CxpoUMXBnrmcMbn74GT8KWQjS724AA3K5ePO5ogLECxIq3huyB9USeeXmYBhUtcLpKSSH7gA/8vT/tvXK0+/YNTKzIIrOjuZ9IOVrwq2PyUY= on: - branch: master + branch: master3 distributions: "bdist_wheel" diff --git a/setup.py b/setup.py index b17205fd..e5a3b5d9 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,25 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.1.39" +VERSION = "0.1.0" setup( - name="PyTrustNFe", + name="PyTrustNFe3", version=VERSION, author="Danimar Ribeiro", author_email='danimaribeiro@gmail.com', keywords=['nfe', 'mdf-e'], classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Environment :: Plugins', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU Lesser General Public License v2 or \ later (LGPLv2+)', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.4', 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=find_packages(exclude=['*test*']), From 7b31a2f78fb18c563ed75a5fe4493c684fde5207 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Tue, 12 Sep 2017 19:42:42 -0300 Subject: [PATCH 05/31] Removendo print - Ajustando encodings --- pytrustnfe/nfe/__init__.py | 2 +- pytrustnfe/nfe/comunicacao.py | 3 +-- pytrustnfe/xml/__init__.py | 1 - tests/test_xml_serializacao.py | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pytrustnfe/nfe/__init__.py b/pytrustnfe/nfe/__init__.py index b2daec2a..9854047c 100644 --- a/pytrustnfe/nfe/__init__.py +++ b/pytrustnfe/nfe/__init__.py @@ -189,7 +189,7 @@ def _send(certificado, method, sign, **kwargs): send_raw=send_raw) return { 'sent_xml': xml_send, - 'received_xml': response, + 'received_xml': response.decode(), 'object': obj } diff --git a/pytrustnfe/nfe/comunicacao.py b/pytrustnfe/nfe/comunicacao.py index df15ddd5..08a7a6a7 100644 --- a/pytrustnfe/nfe/comunicacao.py +++ b/pytrustnfe/nfe/comunicacao.py @@ -10,7 +10,6 @@ def _soap_xml(body, cabecalho): - print(type(body)) xml = '' xml += '' xml += '' @@ -32,4 +31,4 @@ def executar_consulta(certificado, url, cabecalho, xmlEnviar, send_raw=False): xml = '' + xmlEnviar.rstrip('\n') xml_enviar = xml xml_retorno = client.post_soap(xml_enviar, cabecalho) - return sanitize_response(xml_retorno) + return sanitize_response(xml_retorno.encode()) diff --git a/pytrustnfe/xml/__init__.py b/pytrustnfe/xml/__init__.py index 3bc6321d..bef5a539 100644 --- a/pytrustnfe/xml/__init__.py +++ b/pytrustnfe/xml/__init__.py @@ -46,7 +46,6 @@ def render_xml(path, template_name, remove_empty, **nfe): def sanitize_response(response): - print(response) tree = etree.fromstring(response) # Remove namespaces inuteis na resposta for elem in tree.getiterator(): diff --git a/tests/test_xml_serializacao.py b/tests/test_xml_serializacao.py index 82f3dbce..3f06e14f 100644 --- a/tests/test_xml_serializacao.py +++ b/tests/test_xml_serializacao.py @@ -29,7 +29,6 @@ def test_sanitize_response(self): path = os.path.join(os.path.dirname(__file__), 'XMLs') xml_to_clear = open(os.path.join(path, 'jinja_result.xml'), 'r').read() xml, obj = sanitize_response(xml_to_clear) - print(type(xml)) self.assertEqual(xml, xml_to_clear) self.assertEqual(obj.tpAmb, 'oi') self.assertEqual(obj.CNPJ, 'ola') From c44cd90103d9d12fb0ca789494f213bf3af33f69 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Fri, 15 Sep 2017 11:44:02 -0300 Subject: [PATCH 06/31] Fixing build for windows --- pytrustnfe/certificado.py | 9 ++++----- requirements.txt | 2 +- setup.py | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pytrustnfe/certificado.py b/pytrustnfe/certificado.py index b96df912..d24732c2 100644 --- a/pytrustnfe/certificado.py +++ b/pytrustnfe/certificado.py @@ -2,8 +2,7 @@ # © 2016 Danimar Ribeiro, Trustcode # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from uuid import uuid4 +import tempfile from OpenSSL import crypto @@ -13,7 +12,7 @@ def __init__(self, pfx, password): self.password = password def save_pfx(self): - pfx_temp = '/tmp/' + uuid4().hex + pfx_temp = tempfile.mkstemp()[1] arq_temp = open(pfx_temp, 'wb') arq_temp.write(self.pfx) arq_temp.close() @@ -32,8 +31,8 @@ def extract_cert_and_key_from_pfx(pfx, password): def save_cert_key(cert, key): - cert_temp = '/tmp/' + uuid4().hex - key_temp = '/tmp/' + uuid4().hex + cert_temp = tempfile.mkstemp()[1] + key_temp = tempfile.mkstemp()[1] arq_temp = open(cert_temp, 'w') arq_temp.write(cert) diff --git a/requirements.txt b/requirements.txt index 886103ba..c42cf9de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ Jinja2 signxml urllib3 >= 1.22 suds-jurko >= 0.6 -suds-jurko-requests >= 1.0 +suds-jurko-requests >= 1.1 defusedxml >= 0.4.1, < 0.6 eight >= 0.3.0, < 0.5 cryptography >= 1.8, < 1.10 diff --git a/setup.py b/setup.py index e5a3b5d9..84740cf7 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.1.0" +VERSION = "0.1.1" setup( name="PyTrustNFe3", @@ -41,7 +41,7 @@ 'signxml >= 2.4.0', 'lxml >= 3.5.0, < 4', 'suds-jurko >= 0.6', - 'suds-jurko-requests >= 0.3', + 'suds-jurko-requests >= 1.1', 'reportlab' ], tests_require=[ From aa4fd2bb42ede64bd14ada552ad363d9176d1501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Luna?= Date: Fri, 15 Sep 2017 14:49:48 -0300 Subject: [PATCH 07/31] Inclui fonte NimbusSanL. --- pytrustnfe/nfe/danfe.py | 7 +++++++ pytrustnfe/nfe/fonts/NimbusSanL Bold.ttf | Bin 0 -> 49840 bytes pytrustnfe/nfe/fonts/NimbusSanL Regular.ttf | Bin 0 -> 48304 bytes 3 files changed, 7 insertions(+) create mode 100644 pytrustnfe/nfe/fonts/NimbusSanL Bold.ttf create mode 100644 pytrustnfe/nfe/fonts/NimbusSanL Regular.ttf diff --git a/pytrustnfe/nfe/danfe.py b/pytrustnfe/nfe/danfe.py index d0fedcd0..a67d95d0 100644 --- a/pytrustnfe/nfe/danfe.py +++ b/pytrustnfe/nfe/danfe.py @@ -16,6 +16,9 @@ from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.enums import TA_CENTER from reportlab.platypus import Paragraph, Image +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + def chunks(cString, nLen): @@ -72,6 +75,10 @@ def get_image(path, width=1*cm): class danfe(object): def __init__(self, sizepage=A4, list_xml=None, recibo=True, orientation='portrait', logo=None): + pdfmetrics.registerFont( + TTFont('NimbusSanL-Regu', '../fonts/NimbusSanL Regular.ttf')) + pdfmetrics.registerFont( + TTFont('NimbusSanL-Bold', '../fonts/NimbusSanL Bold.ttf')) self.width = 210 # 21 x 29,7cm self.height = 297 self.nLeft = 10 diff --git a/pytrustnfe/nfe/fonts/NimbusSanL Bold.ttf b/pytrustnfe/nfe/fonts/NimbusSanL Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0e29c13dc61aa3a7c61292074897c3e7c2908ed6 GIT binary patch literal 49840 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≥+OH~lvgByGdi~fj8P1Uf)e8eX0;V*>tJM?c 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 R>RP_t#N*I1#V4G`e*vyMr4j%D literal 0 HcmV?d00001 diff --git a/pytrustnfe/nfe/fonts/NimbusSanL Regular.ttf b/pytrustnfe/nfe/fonts/NimbusSanL Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ac601af810251cc4d1e6dc587642b1a9877abb1d GIT binary patch literal 48304 zcmb@v2VhiH+CF~Hy;IVsXVNp1NiqrPWzs_?36MfaLWj^>=)Lz|lrEx(NVOor&LV4J zL2=b}6%ko=?Vs+dyLNY7OLFsj-g9S?z`Eu8{{KKGlRJ}p&Uw#!-txR<7-x)yvUN;m z%}s-|a_1Zx9*8UN;Jb0s>N7VeFQN@p{2DTnJmHv*Gto@q6O5*^8EKerIe2WBWpIkJ_12 z7R>q3^rr_HyL|{__9e6CPMT2iQRGU-9{fFHQu6Ex%jN|p@=c6A{5Sl)W6p%xQ|8`R zSIyWX7*7hCH+SKpUavY$j6LRHEO5@e1ykl_tzP>YV^3iYEEaR##`iES(@B@*dR$@Z zPpKbk<$uDeIM?7CU-Vkfuc3p6JD8U_zLsmwx~9E0+wZ*l-uoYX$TSjm zkPQOOw9L%buuiV$q1?pdc@D4QgZTu$jIZIhNY6_zO7F@S99Bn^BhHcFNOoj9N*pJg z3C`pMDM6QDNH8bZ5~32~64DZC6UHY@N&4N_QfDXb?O;c_j)&l$aXgz>@+Wg7JtMpBIT`n4ot>Q@G1mEy&hI+E?))R39qSy6$Lh}M zo!~76Jk|d82S?PM{9yA3x8Zxp`!n8O{@!ix9e-~-WAELE??vy;e{bA-S?^{4@hf$o zS+n{VDp%l9qyA1m)#p5jhjFw2UEM#s-_dpLC;W}XPOwdEGkcod#y(|R*e;`|H?x_Bs2KJ;8orZ?K

gT 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 WnP$E^@vwNFvYGi Date: Fri, 15 Sep 2017 20:49:30 -0300 Subject: [PATCH 08/31] Updating setup.py to include font files --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 84740cf7..83609d9a 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ packages=find_packages(exclude=['*test*']), package_data={'pytrustnfe': [ 'nfe/templates/*xml', + 'nfe/fonts/*ttf', 'nfse/paulistana/templates/*xml', 'nfse/ginfes/templates/*xml', 'nfse/simpliss/templates/*xml', From b05bd193542c8c58e6c236d6ba16d2dac72c1d1d Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Fri, 15 Sep 2017 20:56:42 -0300 Subject: [PATCH 09/31] Build para windows --- .appveyor.yml | 32 ++++++++++++++++++++++++++++++++ pytrustnfe/nfe/danfe.py | 11 +++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..886484b8 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,32 @@ +version: 1.3.{build} + +environment: + matrix: + - python: 35 + - python: 35-x64 + - python: 36 + - python: 36-x64 + +install: + - SET PATH=C:\\Python%PYTHON%;c:\\Python%PYTHON%\\scripts;%PATH% + - python -m pip.__main__ install -U pip wheel setuptools + - pip install -r requirements.txt + +build: off +build_script: + # configure version + - ps: >- + If ($env:APPVEYOR_REPO_TAG -Eq "true" ) { + $version = "$env:APPVEYOR_REPO_TAG_NAME" + } Else { + $version = "$env:APPVEYOR_BUILD_VERSION.dev0" + } + $version | Set-Content version.txt + - python setup.py build bdist_wheel + - ps: Get-ChildItem dist\*.whl | % { pip install $_.FullName } + +test: off +test_script: + - pip list + - py.test -v tests + - ps: Get-ChildItem dist\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/pytrustnfe/nfe/danfe.py b/pytrustnfe/nfe/danfe.py index a67d95d0..22338774 100644 --- a/pytrustnfe/nfe/danfe.py +++ b/pytrustnfe/nfe/danfe.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # Classe para geração de PDF da DANFE a partir de xml etree.fromstring - +import os from io import BytesIO from textwrap import wrap @@ -20,7 +20,6 @@ from reportlab.pdfbase.ttfonts import TTFont - def chunks(cString, nLen): for start in range(0, len(cString), nLen): yield cString[start:start+nLen] @@ -75,10 +74,14 @@ def get_image(path, width=1*cm): class danfe(object): def __init__(self, sizepage=A4, list_xml=None, recibo=True, orientation='portrait', logo=None): + + path = os.path.join(os.path.dirname(__file__), 'fonts') pdfmetrics.registerFont( - TTFont('NimbusSanL-Regu', '../fonts/NimbusSanL Regular.ttf')) + TTFont('NimbusSanL-Regu', + os.path.join(path, 'NimbusSanL Regular.ttf'))) pdfmetrics.registerFont( - TTFont('NimbusSanL-Bold', '../fonts/NimbusSanL Bold.ttf')) + TTFont('NimbusSanL-Bold', + os.path.join(path, 'NimbusSanL Bold.ttf'))) self.width = 210 # 21 x 29,7cm self.height = 297 self.nLeft = 10 From 0b47eba7b5f39c7ed61a1f87d22309b4aa9dd624 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Thu, 5 Oct 2017 21:46:01 -0300 Subject: [PATCH 10/31] =?UTF-8?q?Refactor=20-=20Permitir=20criar=20o=20xml?= =?UTF-8?q?=20de=20envio=20separadamente=20Ajuste=20na=20fun=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20valida=C3=A7=C3=A3o=20do=20xml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfe/__init__.py | 106 +++++++++++++++--- pytrustnfe/xml/schemas/enviNFe_v3.10.xsd | 0 pytrustnfe/xml/schemas/leiauteNFe_v3.10.xsd | 0 pytrustnfe/xml/schemas/nfe_v3.10.xsd | 0 pytrustnfe/xml/schemas/tiposBasico_v3.10.xsd | 2 +- .../xml/schemas/xmldsig-core-schema_v1.01.xsd | 0 pytrustnfe/xml/validate.py | 31 +---- 7 files changed, 97 insertions(+), 42 deletions(-) mode change 100644 => 100755 pytrustnfe/xml/schemas/enviNFe_v3.10.xsd mode change 100644 => 100755 pytrustnfe/xml/schemas/leiauteNFe_v3.10.xsd mode change 100644 => 100755 pytrustnfe/xml/schemas/nfe_v3.10.xsd mode change 100644 => 100755 pytrustnfe/xml/schemas/tiposBasico_v3.10.xsd mode change 100644 => 100755 pytrustnfe/xml/schemas/xmldsig-core-schema_v1.01.xsd diff --git a/pytrustnfe/nfe/__init__.py b/pytrustnfe/nfe/__init__.py index 9854047c..f2ac1560 100644 --- a/pytrustnfe/nfe/__init__.py +++ b/pytrustnfe/nfe/__init__.py @@ -124,9 +124,10 @@ def _add_qrCode(xml, **kwargs): return etree.tostring(xml, encoding=str) -def _send(certificado, method, sign, **kwargs): +def _render(certificado, method, sign, **kwargs): path = os.path.join(os.path.dirname(__file__), 'templates') xmlElem_send = render_xml(path, '%s.xml' % method, True, **kwargs) + modelo = xmlElem_send.find(".//{http://www.portalfiscal.inf.br/nfe}mod") modelo = modelo.text if modelo is not None else '55' if modelo == '65': @@ -176,8 +177,12 @@ def _send(certificado, method, sign, **kwargs): else: xml_send = etree.tostring(xmlElem_send, encoding=str) + return xml_send + - url = localizar_url(method, kwargs['estado'], modelo, +def _send(certificado, method, **kwargs): + xml_send = kwargs["xml"] + url = localizar_url(method, kwargs['estado'], '55', kwargs['ambiente']) cabecalho = _build_header(method, **kwargs) @@ -194,50 +199,123 @@ def _send(certificado, method, sign, **kwargs): } -def autorizar_nfe(certificado, **kwargs): # Assinar +def xml_autorizar_nfe(certificado, **kwargs): _generate_nfe_id(**kwargs) - return _send(certificado, 'NfeAutorizacao', True, **kwargs) + return _render(certificado, 'NfeAutorizacao', True, **kwargs) + + +def autorizar_nfe(certificado, **kwargs): # Assinar + if "xml" not in kwargs: + kwargs['xml'] = xml_autorizar_nfe(certificado, **kwargs) + return _send(certificado, 'NfeAutorizacao', **kwargs) + + +def xml_retorno_autorizar_nfe(certificado, **kwargs): + return _render(certificado, 'NfeRetAutorizacao', False, **kwargs) def retorno_autorizar_nfe(certificado, **kwargs): - return _send(certificado, 'NfeRetAutorizacao', False, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_retorno_autorizar_nfe(certificado, **kwargs) + return _send(certificado, 'NfeRetAutorizacao', **kwargs) + + +def xml_recepcao_evento_cancelamento(certificado, **kwargs): # Assinar + return _render(certificado, 'RecepcaoEventoCancelamento', True, **kwargs) def recepcao_evento_cancelamento(certificado, **kwargs): # Assinar - return _send(certificado, 'RecepcaoEventoCancelamento', True, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_recepcao_evento_cancelamento(certificado, **kwargs) + return _send(certificado, 'RecepcaoEventoCancelamento', **kwargs) + + +def xml_inutilizar_nfe(certificado, **kwargs): + return _render(certificado, 'NfeInutilizacao', True, **kwargs) + + +def inutilizar_nfe(certificado, **kwargs): + if "xml" not in kwargs: + kwargs['xml'] = xml_inutilizar_nfe(certificado, **kwargs) + return _send(certificado, 'NfeInutilizacao', **kwargs) -def inutilizar_nfe(certificado, **kwargs): # Assinar - return _send(certificado, 'NfeInutilizacao', True, **kwargs) +def xml_consultar_protocolo_nfe(certificado, **kwargs): + return _render(certificado, 'NfeConsultaProtocolo', True, **kwargs) def consultar_protocolo_nfe(certificado, **kwargs): - return _send(certificado, 'NfeConsultaProtocolo', True, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_consultar_protocolo_nfe(certificado, **kwargs) + return _send(certificado, 'NfeConsultaProtocolo', **kwargs) + + +def xml_nfe_status_servico(certificado, **kwargs): + return _render(certificado, 'NfeStatusServico', False, **kwargs) def nfe_status_servico(certificado, **kwargs): - return _send(certificado, 'NfeStatusServico', False, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_nfe_status_servico(certificado, **kwargs) + return _send(certificado, 'NfeStatusServico', **kwargs) + + +def xml_consulta_cadastro(certificado, **kwargs): + return _render(certificado, 'NfeConsultaCadastro', False, **kwargs) def consulta_cadastro(certificado, **kwargs): - return _send(certificado, 'NfeConsultaCadastro', False, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_consulta_cadastro(certificado, **kwargs) + return _send(certificado, 'NfeConsultaCadastro', **kwargs) + + +def xml_recepcao_evento_carta_correcao(certificado, **kwargs): # Assinar + return _render(certificado, 'RecepcaoEventoCarta', True, **kwargs) def recepcao_evento_carta_correcao(certificado, **kwargs): # Assinar + if "xml" not in kwargs: + kwargs['xml'] = xml_recepcao_evento_carta_correcao( + certificado, **kwargs) return _send(certificado, 'RecepcaoEventoCarta', True, **kwargs) +def xml_recepcao_evento_manifesto(certificado, **kwargs): # Assinar + return _render(certificado, 'RecepcaoEventoManifesto', True, **kwargs) + + def recepcao_evento_manifesto(certificado, **kwargs): # Assinar + if "xml" not in kwargs: + kwargs['xml'] = xml_recepcao_evento_manifesto(certificado, **kwargs) return _send(certificado, 'RecepcaoEventoManifesto', True, **kwargs) +def xml_recepcao_evento_epec(certificado, **kwargs): # Assinar + return _render(certificado, 'RecepcaoEventoEPEC', True, **kwargs) + + def recepcao_evento_epec(certificado, **kwargs): # Assinar - return _send(certificado, 'RecepcaoEventoEPEC', True, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_recepcao_evento_epec(certificado, **kwargs) + return _send(certificado, 'RecepcaoEventoEPEC', **kwargs) + + +def xml_consulta_distribuicao_nfe(certificado, **kwargs): # Assinar + return _render(certificado, 'NFeDistribuicaoDFe', False, **kwargs) def consulta_distribuicao_nfe(certificado, **kwargs): - return _send(certificado, 'NFeDistribuicaoDFe', False, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_consulta_distribuicao_nfe(certificado, **kwargs) + return _send(certificado, 'NFeDistribuicaoDFe', **kwargs) + + +def xml_download_nfe(certificado, **kwargs): # Assinar + return _render(certificado, 'NFeDistribuicaoDFe', False, **kwargs) def download_nfe(certificado, **kwargs): - return _send(certificado, 'NFeDistribuicaoDFe', False, **kwargs) + if "xml" not in kwargs: + kwargs['xml'] = xml_download_nfe(certificado, **kwargs) + return _send(certificado, 'NFeDistribuicaoDFe', **kwargs) diff --git a/pytrustnfe/xml/schemas/enviNFe_v3.10.xsd b/pytrustnfe/xml/schemas/enviNFe_v3.10.xsd old mode 100644 new mode 100755 diff --git a/pytrustnfe/xml/schemas/leiauteNFe_v3.10.xsd b/pytrustnfe/xml/schemas/leiauteNFe_v3.10.xsd old mode 100644 new mode 100755 diff --git a/pytrustnfe/xml/schemas/nfe_v3.10.xsd b/pytrustnfe/xml/schemas/nfe_v3.10.xsd old mode 100644 new mode 100755 diff --git a/pytrustnfe/xml/schemas/tiposBasico_v3.10.xsd b/pytrustnfe/xml/schemas/tiposBasico_v3.10.xsd old mode 100644 new mode 100755 index 70c9a4b8..1dfe7d06 --- a/pytrustnfe/xml/schemas/tiposBasico_v3.10.xsd +++ b/pytrustnfe/xml/schemas/tiposBasico_v3.10.xsd @@ -494,7 +494,7 @@ - + diff --git a/pytrustnfe/xml/schemas/xmldsig-core-schema_v1.01.xsd b/pytrustnfe/xml/schemas/xmldsig-core-schema_v1.01.xsd old mode 100644 new mode 100755 diff --git a/pytrustnfe/xml/validate.py b/pytrustnfe/xml/validate.py index 4314d2c6..3306b575 100644 --- a/pytrustnfe/xml/validate.py +++ b/pytrustnfe/xml/validate.py @@ -3,39 +3,16 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import os -import re from lxml import etree PATH = os.path.dirname(os.path.abspath(__file__)) -SCHEMA = os.path.join(PATH, 'schemas/nfe_v3.10.xsd') +SCHEMA = os.path.join(PATH, 'schemas/enviNFe_v3.10.xsd') -def pop_encoding(xml): - xml = xml.split('\n') - if re.match(r'<\?xml version=', xml[0]): - xml.pop(0) - return '\n'.join(xml) - - -def valida_nfe(nfe): - xml = pop_encoding(nfe).encode('utf-8') - nfe = etree.fromstring(xml) +def valida_nfe(xml_nfe): + nfe = etree.fromstring(xml_nfe) esquema = etree.XMLSchema(etree.parse(SCHEMA)) esquema.validate(nfe) erros = [x.message for x in esquema.error_log] - error_msg = '{field} inválido: {valor}.' - unexpected = '{unexpected} não é esperado. O valor esperado é {expected}' - namespace = '{http://www.portalfiscal.inf.br/nfe}' - mensagens = [] - for erro in erros: - campo = re.findall(r"'([^']*)'", erro)[0] - nome = campo[campo.find('}') + 1: ] - valor = nfe.find('.//' + campo).text - if 'Expected is' in erro: - expected_name = re.findall('\(.*?\)', erro) - valor = unexpected.format(unexpected=nome, expected=expected_name) - mensagem = error_msg.format(field=campo.replace(namespace, ''), - valor=valor) - mensagens.append(mensagem) - return "\n".join(mensagens) + return "\n".join(erros) From 9b21368d251607f813a63bda52c4178c971932b1 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Mon, 9 Oct 2017 17:01:36 -0300 Subject: [PATCH 11/31] =?UTF-8?q?Nova=20vers=C3=A3o,=20atualiza=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++------ setup.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index eeadd158..98ddd7b1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # PyTrustNFe Biblioteca Python que tem por objetivo enviar NFe, NFCe e NFSe no Brasil -[![Coverage Status](https://coveralls.io/repos/danimaribeiro/PyTrustNFe/badge.svg?branch=master)](https://coveralls.io/r/danimaribeiro/PyTrustNFe?branch=master) -[![Code Health](https://landscape.io/github/danimaribeiro/PyTrustNFe/master/landscape.svg?style=flat)](https://landscape.io/github/danimaribeiro/PyTrustNFe/master) -[![Build Status](https://travis-ci.org/danimaribeiro/PyTrustNFe.svg?branch=master)](https://travis-ci.org/danimaribeiro/PyTrustNFe) -[![PyPI version](https://badge.fury.io/py/PyTrustNFe.svg)](https://badge.fury.io/py/PyTrustNFe) +[![Coverage Status](https://coveralls.io/repos/danimaribeiro/PyTrustNFe/badge.svg?branch=master3)](https://coveralls.io/r/danimaribeiro/PyTrustNFe?branch=master3) +[![Code Health](https://landscape.io/github/danimaribeiro/PyTrustNFe/master3/landscape.svg?style=flat)](https://landscape.io/github/danimaribeiro/PyTrustNFe/master3) +[![Build Status](https://travis-ci.org/danimaribeiro/PyTrustNFe.svg?branch=master3)](https://travis-ci.org/danimaribeiro/PyTrustNFe) +[![PyPI version](https://badge.fury.io/py/PyTrustNFe3.svg)](https://badge.fury.io/py/PyTrustNFe3) Dependências: * PyXmlSec @@ -19,7 +19,7 @@ NFSe - Cidades atendidas -------------- * [Ariss](cidades/ariss.md) - 4 cidades atendidas * [Simpliss](cidades/simpliss.md) - 18 cidade atendidas - +* [GINFES](cidades/ginfes.md) - 79 cidades atendidas Roadmap -------------- @@ -31,7 +31,6 @@ Compatibilidade [python 2 e 3](https://github.com/danimaribeiro/PyTrustNFe/pull/ Implementar novos provedores de NFSe * [Betha](cidades/betha.md) - 81 cidades atendidas WIP -* [GINFES](cidades/ginfes.md) - 79 cidades atendidas * [WebISS](cidades/webiss.md) - 51 cidades atendidas * [ISSIntel](cidades/issintel.md) - 32 cidades atendidas * [ISSNET](cidades/issnet.md) - 32 cidades atendidas diff --git a/setup.py b/setup.py index 83609d9a..09fc4827 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.1.1" +VERSION = "0.9.0" setup( name="PyTrustNFe3", From 582742ecca75c8e3d097c02bf288552eca5199d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Luna?= Date: Fri, 3 Nov 2017 12:14:40 -0200 Subject: [PATCH 12/31] =?UTF-8?q?[FIX]=20Corrige=20consulta=20de=20distrib?= =?UTF-8?q?u=C3=AD=C3=A7=C3=A3o=20de=20NF-e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/client.py | 12 +++++++++--- pytrustnfe/nfe/comunicacao.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pytrustnfe/client.py b/pytrustnfe/client.py index 30047d1e..03adbd2e 100644 --- a/pytrustnfe/client.py +++ b/pytrustnfe/client.py @@ -42,14 +42,20 @@ def __init__(self, url, cert_path, key_path): self.cert_path = cert_path self.key_path = key_path - def _headers(self, action): + def _headers(self, action, send_raw): + if send_raw: + return { + 'Content-type': 'text/xml; charset=utf-8; action="http://www.portalfiscal.inf.br/nfe/wsdl/%s"' % action, + 'Accept': 'application/soap+xml; charset=utf-8', + } + return { 'Content-type': 'application/soap+xml; charset=utf-8; action="http://www.portalfiscal.inf.br/nfe/wsdl/%s"' % action, 'Accept': 'application/soap+xml; charset=utf-8', } - def post_soap(self, xml_soap, cabecalho): - header = self._headers(cabecalho.soap_action) + def post_soap(self, xml_soap, cabecalho, send_raw): + header = self._headers(cabecalho.soap_action, send_raw) urllib3.disable_warnings(category=InsecureRequestWarning) res = requests.post(self.url, data=xml_soap, cert=(self.cert_path, self.key_path), diff --git a/pytrustnfe/nfe/comunicacao.py b/pytrustnfe/nfe/comunicacao.py index 08a7a6a7..df6ba53d 100644 --- a/pytrustnfe/nfe/comunicacao.py +++ b/pytrustnfe/nfe/comunicacao.py @@ -30,5 +30,5 @@ def executar_consulta(certificado, url, cabecalho, xmlEnviar, send_raw=False): if send_raw: xml = '' + xmlEnviar.rstrip('\n') xml_enviar = xml - xml_retorno = client.post_soap(xml_enviar, cabecalho) + xml_retorno = client.post_soap(xml_enviar, cabecalho, send_raw) return sanitize_response(xml_retorno.encode()) From 914912ec7c1cfa152597d4f22a0a1a12e059a09e Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Fri, 3 Nov 2017 13:33:48 -0200 Subject: [PATCH 13/31] =?UTF-8?q?Nova=20vers=C3=A3o=20para=20publicar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 09fc4827..89374c1d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.9.0" +VERSION = "0.9.1" setup( name="PyTrustNFe3", From fa3f44b0de0e3835704502f0724dd0766e0b1723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Luna?= Date: Fri, 3 Nov 2017 18:01:10 -0200 Subject: [PATCH 14/31] [FIX]Corrige envios de eventos --- pytrustnfe/nfe/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytrustnfe/nfe/__init__.py b/pytrustnfe/nfe/__init__.py index f2ac1560..62b6932f 100644 --- a/pytrustnfe/nfe/__init__.py +++ b/pytrustnfe/nfe/__init__.py @@ -278,17 +278,17 @@ def recepcao_evento_carta_correcao(certificado, **kwargs): # Assinar if "xml" not in kwargs: kwargs['xml'] = xml_recepcao_evento_carta_correcao( certificado, **kwargs) - return _send(certificado, 'RecepcaoEventoCarta', True, **kwargs) + return _send(certificado, 'RecepcaoEventoCarta', **kwargs) def xml_recepcao_evento_manifesto(certificado, **kwargs): # Assinar - return _render(certificado, 'RecepcaoEventoManifesto', True, **kwargs) + return _render(certificado, 'RecepcaoEventoManifesto', **kwargs) def recepcao_evento_manifesto(certificado, **kwargs): # Assinar if "xml" not in kwargs: kwargs['xml'] = xml_recepcao_evento_manifesto(certificado, **kwargs) - return _send(certificado, 'RecepcaoEventoManifesto', True, **kwargs) + return _send(certificado, 'RecepcaoEventoManifesto', **kwargs) def xml_recepcao_evento_epec(certificado, **kwargs): # Assinar From 9219ec424310c0d39750fdf319847166228bf53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Luna?= Date: Wed, 8 Nov 2017 17:43:05 -0200 Subject: [PATCH 15/31] [FIX] Corrige erro 225 na NF-e, devido a caracteres especiais. --- pytrustnfe/xml/__init__.py | 16 ++++++++++++++-- setup.py | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pytrustnfe/xml/__init__.py b/pytrustnfe/xml/__init__.py index bef5a539..99cd5804 100644 --- a/pytrustnfe/xml/__init__.py +++ b/pytrustnfe/xml/__init__.py @@ -2,7 +2,6 @@ # © 2016 Danimar Ribeiro, Trustcode # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import unicodedata from lxml import etree from lxml import objectify @@ -17,6 +16,7 @@ def recursively_empty(e): def render_xml(path, template_name, remove_empty, **nfe): + nfe = recursively_normalize(nfe) env = Environment( loader=FileSystemLoader(path), extensions=['jinja2.ext.with_']) @@ -53,6 +53,18 @@ def sanitize_response(response): continue i = elem.tag.find('}') if i >= 0: - elem.tag = elem.tag[i+1:] + elem.tag = elem.tag[i + 1:] objectify.deannotate(tree, cleanup_namespaces=True) return response, objectify.fromstring(etree.tostring(tree)) + + +def recursively_normalize(vals): + for item in vals: + if type(vals[item]) is str: + vals[item] = filters.normalize_str(vals[item]) + 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 diff --git a/setup.py b/setup.py index 89374c1d..a1add7ca 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.9.1" +VERSION = "0.9.2" setup( name="PyTrustNFe3", From 7cff7bb5f1cbea4b871f3559e87a5c8bf7e2f72a Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Thu, 14 Dec 2017 11:17:28 -0200 Subject: [PATCH 16/31] =?UTF-8?q?[WIP]=20Implementa=C3=A7=C3=A3o=20da=20nf?= =?UTF-8?q?se=20de=20florian=C3=B3polis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfse/floripa/__init__.py | 105 ++++++++++++++++++ .../nfse/floripa/templates/cancelar_nota.xml | 7 ++ .../nfse/floripa/templates/processar_nota.xml | 40 +++++++ pytrustnfe/nfse/ginfes/__init__.py | 1 - requirements.txt | 4 +- 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 pytrustnfe/nfse/floripa/__init__.py create mode 100644 pytrustnfe/nfse/floripa/templates/cancelar_nota.xml create mode 100644 pytrustnfe/nfse/floripa/templates/processar_nota.xml diff --git a/pytrustnfe/nfse/floripa/__init__.py b/pytrustnfe/nfse/floripa/__init__.py new file mode 100644 index 00000000..9c13bcb4 --- /dev/null +++ b/pytrustnfe/nfse/floripa/__init__.py @@ -0,0 +1,105 @@ +# -*- 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 + + +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.text + + +def _send(certificado, method, **kwargs): + if kwargs['ambiente'] == 'producao': + url = 'https://nfps-e.pmf.sc.gov.br/api/v1/processamento/notas/processa' + else: + url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/processamento/notas/processa' + + xml_send = '' + kwargs['xml'] + + base = dict( + ambiente='homologacao', client_id="trustcode-tecnologia-client", + secret_id="", username="", + password="" + ) + token = _get_oauth_token(**kwargs) + + kwargs.update({"numero": 1, 'access_token': token["access_token"]}) + + headers = {"Accept": "application/xml", + "Authorization": "Bearer %s" % kwargs['access_token']} + r = requests.post(url, headers=headers, data=xml_send) + print(r.status_code) + if r.status_code != 200: + raise Exception(r.text) + + print(r.text) + response, obj = sanitize_response(r.text) + return { + 'sent_xml': xml_send, + 'received_xml': response, + 'object': obj + } + + +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 consultar_nota(certificado, **kwargs): + url = "https://nfps-e-hml.pmf.sc.gov.br/api/v1/consultas/notas/numero/%s" % (kwargs["numero"]) + url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/consultas/notas/prestador/24158233000185?pagina=1' + + 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 diff --git a/pytrustnfe/nfse/floripa/templates/cancelar_nota.xml b/pytrustnfe/nfse/floripa/templates/cancelar_nota.xml new file mode 100644 index 00000000..324d7798 --- /dev/null +++ b/pytrustnfe/nfse/floripa/templates/cancelar_nota.xml @@ -0,0 +1,7 @@ + + + {{ cancelamento.motivo }} + {{ cancelamento.aedf }} + {{ cancelamento.numero }} + {{ cancelamento.codigo_verificacao }} + diff --git a/pytrustnfe/nfse/floripa/templates/processar_nota.xml b/pytrustnfe/nfse/floripa/templates/processar_nota.xml new file mode 100644 index 00000000..7a31ae30 --- /dev/null +++ b/pytrustnfe/nfse/floripa/templates/processar_nota.xml @@ -0,0 +1,40 @@ + + + {{ rps.tomador.bairro }} + {{ rps.base_calculo }} + 0.0 + {{ rps.cfps }} + {{ rps.tomador.cidade }} + {{ rps.tomador.cep }} + {{ rps.tomador.complemento }} + {{ rps.observacoes }} + {{ rps.data_emissao }} + {{ rps.tomador.email }} + {{ rps.numero }} + {{ rps.tomador.cnpj_cpf }} + {{ rps.tomador.inscricao_municipal }} + + {% for item in rps.itens_servico -%} + + {{ item.aliquota }} + {{ item.cst_servico }} + {{ item.descricao }} + {{ item.cnae }} + {{ item.quantidade }} + {{ item.valor_total }} + {{ item.valor_unitario }} + + {% endfor %} + + {{ rps.tomador.logradouro }} + + {{ rps.aedf }} + {{ rps.tomador.numero }} + 1058 + {{ rps.tomador.razao_social }} + {{ rps.tomador.telefone }} + {{ rps.tomador.uf }} + {{rps.valor_iss }} + 0.0 + {{ rps.valor_liquido_nfse }} + diff --git a/pytrustnfe/nfse/ginfes/__init__.py b/pytrustnfe/nfse/ginfes/__init__.py index e662f2f5..80e969b3 100644 --- a/pytrustnfe/nfse/ginfes/__init__.py +++ b/pytrustnfe/nfse/ginfes/__init__.py @@ -4,7 +4,6 @@ import os import suds -from lxml import etree from pytrustnfe.xml import render_xml, sanitize_response from pytrustnfe.client import get_authenticated_client from pytrustnfe.certificado import extract_cert_and_key_from_pfx, save_cert_key diff --git a/requirements.txt b/requirements.txt index c42cf9de..37ad7fd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,8 +7,8 @@ suds-jurko >= 0.6 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 +cryptography >= 1.8, < 2.1 +pyOpenSSL >= 16.0.0, < 18 certifi >= 2015.11.20.1 xmlsec >= 1.3.3 reportlab From 2138bd3ee2e663b501f7a47eecca9a9d386e0c4d Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 14 Dec 2017 14:49:49 -0200 Subject: [PATCH 17/31] adicionado string.strip no recursive_normalize --- pytrustnfe/xml/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytrustnfe/xml/__init__.py b/pytrustnfe/xml/__init__.py index 99cd5804..0ed0ee52 100644 --- a/pytrustnfe/xml/__init__.py +++ b/pytrustnfe/xml/__init__.py @@ -61,6 +61,7 @@ def sanitize_response(response): def recursively_normalize(vals): for item in vals: if type(vals[item]) is str: + vals[item] = vals[item].strip() vals[item] = filters.normalize_str(vals[item]) elif type(vals[item]) is dict: recursively_normalize(vals[item]) From 5a85580ba4c11e1b6cf05ba1a20799f2c8c20768 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Fri, 15 Dec 2017 16:30:49 -0200 Subject: [PATCH 18/31] [WIP] - NFSE Floripa --- pytrustnfe/nfse/floripa/__init__.py | 12 +++++++++++- pytrustnfe/nfse/floripa/templates/processar_nota.xml | 4 ++-- setup.py | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pytrustnfe/nfse/floripa/__init__.py b/pytrustnfe/nfse/floripa/__init__.py index 9c13bcb4..7dceb102 100644 --- a/pytrustnfe/nfse/floripa/__init__.py +++ b/pytrustnfe/nfse/floripa/__init__.py @@ -54,7 +54,7 @@ def _send(certificado, method, **kwargs): else: url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/processamento/notas/processa' - xml_send = '' + kwargs['xml'] + xml_send = kwargs['xml'] base = dict( ambiente='homologacao', client_id="trustcode-tecnologia-client", @@ -91,6 +91,16 @@ def 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): url = "https://nfps-e-hml.pmf.sc.gov.br/api/v1/consultas/notas/numero/%s" % (kwargs["numero"]) url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/consultas/notas/prestador/24158233000185?pagina=1' diff --git a/pytrustnfe/nfse/floripa/templates/processar_nota.xml b/pytrustnfe/nfse/floripa/templates/processar_nota.xml index 7a31ae30..08a7a2e6 100644 --- a/pytrustnfe/nfse/floripa/templates/processar_nota.xml +++ b/pytrustnfe/nfse/floripa/templates/processar_nota.xml @@ -34,7 +34,7 @@ {{ rps.tomador.razao_social }} {{ rps.tomador.telefone }} {{ rps.tomador.uf }} - {{rps.valor_iss }} + {{rps.valor_issqn }} 0.0 - {{ rps.valor_liquido_nfse }} + {{ rps.valor_total }} diff --git a/setup.py b/setup.py index a1add7ca..2ebafd05 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ 'nfse/simpliss/templates/*xml', 'nfse/betha/templates/*xml', 'nfse/susesu/templates/*xml', + 'nfse/floripa/templates/*xml', 'xml/schemas/*xsd', ]}, url='https://github.com/danimaribeiro/PyTrustNFe', From c2e2d1ed467e831b7223a0cabe4a3daf7a757ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Luna?= Date: Thu, 14 Dec 2017 16:15:03 -0200 Subject: [PATCH 19/31] [WIP] Implementa NFS-e Campinas --- pytrustnfe/nfse/campinas/__init__.py | 80 ++++++++++ .../campinas/templates/ConsultaSeqRps.xsd | 30 ++++ .../nfse/campinas/templates/ConsultarLote.xml | 10 ++ .../nfse/campinas/templates/ConsultarLote.xsd | 46 ++++++ .../nfse/campinas/templates/cancelar.xml | 18 +++ .../nfse/campinas/templates/cancelar.xsd | 55 +++++++ .../campinas/templates/consulta_notas.xml | 11 ++ .../campinas/templates/consulta_notas.xsd | 88 +++++++++++ .../campinas/templates/consultarNFSeRps.xml | 19 +++ .../campinas/templates/consultarNFSeRps.xsd | 55 +++++++ pytrustnfe/nfse/campinas/templates/enviar.xml | 108 +++++++++++++ pytrustnfe/nfse/campinas/templates/enviar.xsd | 149 ++++++++++++++++++ .../nfse/campinas/templates/soap_header.xml | 12 ++ 13 files changed, 681 insertions(+) create mode 100644 pytrustnfe/nfse/campinas/__init__.py create mode 100644 pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd create mode 100644 pytrustnfe/nfse/campinas/templates/ConsultarLote.xml create mode 100644 pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd create mode 100644 pytrustnfe/nfse/campinas/templates/cancelar.xml create mode 100644 pytrustnfe/nfse/campinas/templates/cancelar.xsd create mode 100644 pytrustnfe/nfse/campinas/templates/consulta_notas.xml create mode 100644 pytrustnfe/nfse/campinas/templates/consulta_notas.xsd create mode 100644 pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml create mode 100644 pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd create mode 100644 pytrustnfe/nfse/campinas/templates/enviar.xml create mode 100644 pytrustnfe/nfse/campinas/templates/enviar.xsd create mode 100644 pytrustnfe/nfse/campinas/templates/soap_header.xml diff --git a/pytrustnfe/nfse/campinas/__init__.py b/pytrustnfe/nfse/campinas/__init__.py new file mode 100644 index 00000000..ff5ef029 --- /dev/null +++ b/pytrustnfe/nfse/campinas/__init__.py @@ -0,0 +1,80 @@ +# -*- encoding: utf-8 -*- +# © 2017 Fábio Luna, Trustcode +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import os +import suds +from lxml import etree +from pytrustnfe.xml import render_xml, sanitize_response +from pytrustnfe.nfse.assinatura import Assinatura +from pytrustnfe import HttpClient + + +def _render_xml(certificado, method, **kwargs): + path = os.path.join(os.path.dirname(__file__), 'templates') + xml_send = render_xml(path, '%s.xml' % method, True, **kwargs) + xml_send = etree.tostring(xml_send) + + return xml_send + + +def _validate(method, xml): + path = os.path.join(os.path.dirname(__file__), 'templates') + schema = os.path.join(path, '%s.xsd' % method) + + nfe = etree.fromstring(xml) + esquema = etree.XMLSchema(etree.parse(schema)) + esquema.validate(nfe) + erros = [x.message for x in esquema.error_log] + return erros + + +def _send(certificado, method, **kwargs): + url = 'http://issdigital.campinas.sp.gov.br/WsNFe2/LoteRps.jws?wsdl' # noqa + + path = os.path.join(os.path.dirname(__file__), 'templates') + + if method == "testeEnviar": + xml_send = render_xml(path, 'testeEnviar', **kwargs) + else: + xml_send = render_xml(path, '%s.xml' % method, False) + client = HttpClient(url) + + pfx_path = certificado.save_pfx() + signer = Assinatura(pfx_path, certificado.password) + xml_signed = signer.assina_xml(xml_send, '') + + try: + response = getattr(client.service, method)(xml_signed) + response, obj = sanitize_response(response) + except suds.WebFault as e: + return { + 'sent_xml': xml_send, + 'received_xml': e.fault.faultstring, + 'object': None + } + + return { + 'sent_xml': xml_send, + 'received_xml': response, + 'object': obj + } + + +def enviar(certificado, **kwargs): + if kwargs['ambiente'] == 'producao': + return _send(certificado, 'enviar', **kwargs) + else: + return _send(certificado, 'testeEnviar', **kwargs) + + +def cancelar(certificado, ** kwargs): + return _send(certificado, 'cancelar', **kwargs) + + +def consulta_lote(certificado, **kwargs): + return _send(certificado, 'ConsultarLote', **kwargs) + + +def consultar_lote_rps(certificado, **kwarg): + return _send(certificado, 'consultarNFSeRps', **kwarg) diff --git a/pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd b/pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd new file mode 100644 index 00000000..94491a76 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pytrustnfe/nfse/campinas/templates/ConsultarLote.xml b/pytrustnfe/nfse/campinas/templates/ConsultarLote.xml new file mode 100644 index 00000000..24afc5d5 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/ConsultarLote.xml @@ -0,0 +1,10 @@ + + + {{ consulta.cidade }} + {{ consulta.cpf_cnpj }} + 1 + {{ consulta.lote }} + + \ No newline at end of file diff --git a/pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd b/pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd new file mode 100644 index 00000000..068756f6 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd @@ -0,0 +1,46 @@ + + + + + + Schema utilizado para REQUISIçÂO de Consulta de Lote de RPS. + Este Schema XML é utilizado pelos Prestadores de serviços para consultarem Lote de RPS emitidos por eles. + + + + + + Cabeçalho do pedido. + + + + + + Informe o Codigo da Cidade. + + + + + Informe o CPF/CNPJ do Remetente autorizado a transmitir a mensagem XML. + + + + + Informe a Versão do Schema XML utilizado. + + + + + Informe o Número do Lote a ser consultado. + + + + + + + + + diff --git a/pytrustnfe/nfse/campinas/templates/cancelar.xml b/pytrustnfe/nfse/campinas/templates/cancelar.xml new file mode 100644 index 00000000..d72086b2 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/cancelar.xml @@ -0,0 +1,18 @@ + + + {{ cancelamento.cidade }} + {{ cancelamento.cpf_cnpj }} + true + 1 + + + + {{ cancelamento.inscricao_municipal }} + {{ cancelamento.nota_id }} + {{ cancelamento.assinatura }} + {{ cancelamento.motivo }} + + + diff --git a/pytrustnfe/nfse/campinas/templates/cancelar.xsd b/pytrustnfe/nfse/campinas/templates/cancelar.xsd new file mode 100644 index 00000000..09f1938a --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/cancelar.xsd @@ -0,0 +1,55 @@ + + + + + + + + Schema utilizado para Cancelamento de NFSe. + Este Schema XML é utilizado pelos Prestadores de serviços cancelarem NFSe emitidas por eles. + + + + + + Cabeçalho do pedido. + + + + + + Informe o Codigo da Cidade. + + + + + Informe o CPF/CNPJ do Remetente autorizado a transmitir a mensagem XML. + + + + + Informe se as NF-e a serem canceladas farão parte de uma mesma transação. True - As NF-e só serão canceladas se não ocorrer nenhum evento de erro durante o processamento de todo o lote; False - As NF-e aptas a serem canceladas serão canceladas, mesmo que ocorram eventos de erro durante processamento do cancelamento de outras NF-e deste lote. + + + + + Informe a Versão do Schema XML utilizado. + + + + + + + + Detalhe do pedido de cancelamento de NFSe. Cada detalhe deverá conter a Chave de uma NFSe e sua respectiva assinatura de cancelamento. + + + + + + + diff --git a/pytrustnfe/nfse/campinas/templates/consulta_notas.xml b/pytrustnfe/nfse/campinas/templates/consulta_notas.xml new file mode 100644 index 00000000..4a666d0b --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/consulta_notas.xml @@ -0,0 +1,11 @@ + + +{{ consulta.cidade }} +{{ consulta.cpf_cnpj }} +{{ consulta.inscricao_municipal }} +{{ consulta.data_inicio }} +{{ consulta.data_final }} +{{ consulta.nota_inicial }} +1 + + \ No newline at end of file diff --git a/pytrustnfe/nfse/campinas/templates/consulta_notas.xsd b/pytrustnfe/nfse/campinas/templates/consulta_notas.xsd new file mode 100644 index 00000000..5784eb81 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/consulta_notas.xsd @@ -0,0 +1,88 @@ + + + + + + + Schema utilizado para REQUISIÇAO de consultas + de notas que foram enviadas por lote de RPS. + Este Schema XML é utilizado pelos prestadores + de serviços para consultas de notas que foram enviadas por lote de + RPS. + + + + + + Cabeçalho do pedido. + + + + + + Informe o Codigo da Cidade. + + + + + + Informe o CPF/CNPJ do Remetente + autorizado a transmitir a mensagem XML. + + + + + Informe a Inscrição Municipal do + Prestador + + + + + Informe a data de início do período + transmitido (AAAA-MM-DD). + + + + + Informe a data final do período + transmitido (AAAA-MM-DD). + + + + + Numero da nota inicial da consulta. Ou + seja a consulta ira retornar as notas no periodo, onde o + numero da nota seja maior ou igual a esse numero. O retorno + não pode ultrapassar 500Kb. Caso não tenha o numero da nota, + passar o valor Zero, será retornado as notas geradas no + periodo até o limite de 500kb. + + + + + Informe a Versão. + + + + + + + + + + + + + diff --git a/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml b/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml new file mode 100644 index 00000000..65355599 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml @@ -0,0 +1,19 @@ + + + {{ consulta.cidade }} + {{ consulta.cpf_cnpj }} + true + 1 + + + + + {{ consulta.inscricao_municipal }} + {{ consulta.rps_id }} + {{ consulta.serie_prestacao }} + + + + diff --git a/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd b/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd new file mode 100644 index 00000000..c0e83bfc --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd @@ -0,0 +1,55 @@ + + + + + + + + Schema utilizado para Consulta de NFSe. + Este Schema XML é utilizado pelos Prestadores de serviços consultarem NFSe emitidas por eles. + + + + + + Cabeçalho do pedido. + + + + + + Informe o Codigo da Cidade. + + + + + Informe o CPF/CNPJ do Remetente autorizado a transmitir a mensagem XML. + + + + + Informe se as NF-e a serem consultadas farão parte de uma mesma transação. Informe sempre True. + + + + + Informe a Versão do Schema XML utilizado. + + + + + + + + Detalhe do pedido de consulta de NFSe. Cada detalhe deverá conter a Chave de uma NFSe e sua respectiva assinatura de consulta. + + + + + + + diff --git a/pytrustnfe/nfse/campinas/templates/enviar.xml b/pytrustnfe/nfse/campinas/templates/enviar.xml new file mode 100644 index 00000000..7e4b1784 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/enviar.xml @@ -0,0 +1,108 @@ + + + {{ nfse.cidade }} + {{ nfse.cpf_cnpj }} + {{ nfse.remetente }} + {{ nfse.transacao }} + {{ nfse.data_inicio|format_date }} + {{ nfse.data_fim|format_date }} + {{ nfse.total_rps }} + {{ nfse.total_servicos }} + {{ nfse.total_deducoes }} + 1 + WS + + + {% for rps in nfse.lista_rps -%} + + {{ rps.assinatura }} + {{ rps.prestador.inscricao_municipal }} + + {{ rps.prestador.razao_social }} + RPS + {{ rps.serie }} + {{ rps.numero }} + {{ rps.data_emissao|format_datetime }} + + {{ rps.situacao }} + + 0 + 0 + 1900-01-01 + {{ rps.serie_prestacao }} + {{ rps.tomador.inscricao_municipal }} + {{ rps.tomador.cpf_cnpj }} + {{ rps.tomador.razao_social }} + + {{ rps.tomador.tipo_logradouro }} + + {{ rps.tomador.logradouro }} + {{ rps.tomador.numero }} + + {{ rps.tomador.tipo_bairro }} + {{ rps.tomador.bairro }} + {{ rps.tomador.cidade }} + {{ rps.tomador.cidade_descricao }} + + {{ rps.tomador.cep }} + {{ rps.tomador.email }} + {{ rps.codigo_atividade }} + {{ rps.aliquota_atividade }} + {{ rps.tipo_recolhimento }} + {{ rps.municipio_prestacao }} + + {{ rps.municipio_descricao_prestacao }} + + {{ rps.operacao }} + {{ rps.tributacao }} + {{ rps.valor_pis }} + {{ rps.valor_cofins }} + {{ rps.valor_inss }} + {{ rps.valor_ir }} + {{ rps.valor_csll }} + {{ rps.aliquota_pis }} + {{ rps.aliquota_cofins }} + {{ rps.aliquota_inss }} + {{ rps.aliquota_ir }} + {{ rps.aliquota_csll }} + {{ rps.descricao }} + {{ rps.prestador.ddd }} + {{ rps.prestador.telefone }} + {{ rps.tomador.ddd }} + {{ rps.tomador.telefone }} + {{ rps.motivo_cancelamento }} + {% if rps.deducoes|count > 0 %} + + {% for deducao in rps.deducoes -%} + + {{ deducao.por }} + {{ deducao.tipo }} + {{ deducao.cnpj_referencia }} + {{ deducao.nf_referencia }} + {{ deducao.valor_referencia }} + {{ deducao.percentual_deduzir }} + {{ deducao.valor_deduzir }} + + {% endfor %} + + {% endif %} + {% if rps.deducoes|count == 0 %} + + {% endif %} + + {% for item in rps.itens -%} + + {{ item.descricao }} + {{ item.quantidade }} + {{ item.valor_unitario }} + {{ item.valor_total }} + S + + {% endfor %} + + + {% endfor %} + + diff --git a/pytrustnfe/nfse/campinas/templates/enviar.xsd b/pytrustnfe/nfse/campinas/templates/enviar.xsd new file mode 100644 index 00000000..fedce414 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/enviar.xsd @@ -0,0 +1,149 @@ + + + + + + Schema utilizado para envio de lote de RPS. + Este Schema XML é utilizado pelos prestadores + de serviços para substituição em lote de RPS por NFS-e. + + + + + + + Cabeçalho do Lote. + + + + + + Informe o Codigo da Cidade no Padrão SIAFI. + + + + + + + CNPJ do contribuinte ou CPF do Responsável Legal autorizado a entregar o lote. + + + + + + + Informe o Nome do Contribuinte ou do Responsável Legal + + + + + + + Informe se os RPS a serem + substituídos por + NF-e farão + parte de uma mesma transação. + True - Os RPS só serão + substituídos por NF-e se não + ocorrer nenhum evento de erro + durante o processamento de todo + o lote; False - Os RPS válidos + serão substituídos por NF-e, + mesmo que ocorram eventos de + erro durante processamento de + outros RPS deste lote. Por definição + estão sendo aceitos apenas lotes com RPS válidos, + o lote é + recusado caso haja algum RPS inválido. + + + + + + + Informe a data de início do + período + transmitido + (AAAA-MM-DD). + + + + + + + Informe a data final do período + transmitido + (AAAA-MM-DD). + + + + + + + Informe o total de RPS contidos + na mensagem + XML. OBS: O xml não pode ultrapassar o tamanho maximo de 500kb. + + + + + + + Informe o valor total dos + serviços prestados + dos RPS + contidos na mensagem XML. + + + + + + + Informe o valor total das + deduções dos RPS + contidos na + mensagem XML. + + + + + + + Informe a Versão do Schema XML + utilizado. + + + + + + Informe o Método de Envio + + + + + Versão da DLL de envio de lote. Não é necessário informar esse campo caso não utilize a DLL. + + + + + + + + + + Informe os RPS a serem substituidos por + NF-e. + + + + + + + diff --git a/pytrustnfe/nfse/campinas/templates/soap_header.xml b/pytrustnfe/nfse/campinas/templates/soap_header.xml new file mode 100644 index 00000000..e9d1dd21 --- /dev/null +++ b/pytrustnfe/nfse/campinas/templates/soap_header.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file From 22ac348b8b722a1b500488477f48119b0b6edb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Luna?= Date: Wed, 20 Dec 2017 18:00:03 -0200 Subject: [PATCH 20/31] Implementa NFse de Campinas --- pytrustnfe/nfse/campinas/__init__.py | 58 ++++--- .../campinas/templates/ConsultaSeqRps.xsd | 30 ---- .../nfse/campinas/templates/ConsultarLote.xsd | 46 ------ .../nfse/campinas/templates/cancelar.xsd | 55 ------- .../campinas/templates/consulta_notas.xsd | 88 ----------- .../{ConsultarLote.xml => consultarLote.xml} | 0 .../campinas/templates/consultarNFSeRps.xml | 19 --- .../campinas/templates/consultarNFSeRps.xsd | 55 ------- pytrustnfe/nfse/campinas/templates/enviar.xsd | 149 ------------------ setup.py | 3 +- 10 files changed, 29 insertions(+), 474 deletions(-) delete mode 100644 pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd delete mode 100644 pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd delete mode 100644 pytrustnfe/nfse/campinas/templates/cancelar.xsd delete mode 100644 pytrustnfe/nfse/campinas/templates/consulta_notas.xsd rename pytrustnfe/nfse/campinas/templates/{ConsultarLote.xml => consultarLote.xml} (100%) delete mode 100644 pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml delete mode 100644 pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd delete mode 100644 pytrustnfe/nfse/campinas/templates/enviar.xsd diff --git a/pytrustnfe/nfse/campinas/__init__.py b/pytrustnfe/nfse/campinas/__init__.py index ff5ef029..e5536af2 100644 --- a/pytrustnfe/nfse/campinas/__init__.py +++ b/pytrustnfe/nfse/campinas/__init__.py @@ -6,27 +6,22 @@ import suds from lxml import etree 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 -from pytrustnfe import HttpClient +from pytrustnfe.client import get_client -def _render_xml(certificado, method, **kwargs): +def _render(certificado, method, **kwargs): path = os.path.join(os.path.dirname(__file__), 'templates') - xml_send = render_xml(path, '%s.xml' % method, True, **kwargs) - xml_send = etree.tostring(xml_send) - - return xml_send - + if method == "testeEnviar": + xml_send = render_xml(path, 'enviar.xml', True, **kwargs) + else: + xml_send = render_xml(path, '%s.xml' % method, False, **kwargs) -def _validate(method, xml): - path = os.path.join(os.path.dirname(__file__), 'templates') - schema = os.path.join(path, '%s.xsd' % method) + if type(xml_send) != str: + xml_send = etree.tostring(xml_send) - nfe = etree.fromstring(xml) - esquema = etree.XMLSchema(etree.parse(schema)) - esquema.validate(nfe) - erros = [x.message for x in esquema.error_log] - return erros + return xml_send def _send(certificado, method, **kwargs): @@ -34,19 +29,19 @@ def _send(certificado, method, **kwargs): path = os.path.join(os.path.dirname(__file__), 'templates') - if method == "testeEnviar": - xml_send = render_xml(path, 'testeEnviar', **kwargs) - else: - xml_send = render_xml(path, '%s.xml' % method, False) - client = HttpClient(url) + xml_send = _render(path, method, **kwargs) + client = get_client(url) - pfx_path = certificado.save_pfx() - signer = Assinatura(pfx_path, certificado.password) - xml_signed = signer.assina_xml(xml_send, '') + if certificado: + 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, '') try: - response = getattr(client.service, method)(xml_signed) - response, obj = sanitize_response(response) + response = getattr(client.service, method)(xml_send) + response, obj = sanitize_response(response.encode()) except suds.WebFault as e: return { 'sent_xml': xml_send, @@ -62,18 +57,19 @@ def _send(certificado, method, **kwargs): def enviar(certificado, **kwargs): - if kwargs['ambiente'] == 'producao': - return _send(certificado, 'enviar', **kwargs) - else: - return _send(certificado, 'testeEnviar', **kwargs) + return _send(certificado, 'enviar', **kwargs) + + +def teste_enviar(certificado, **kwargs): + return _send(certificado, 'testeEnviar', **kwargs) def cancelar(certificado, ** kwargs): return _send(certificado, 'cancelar', **kwargs) -def consulta_lote(certificado, **kwargs): - return _send(certificado, 'ConsultarLote', **kwargs) +def consulta_lote(**kwargs): + return _send(False, 'consultarLote', **kwargs) def consultar_lote_rps(certificado, **kwarg): diff --git a/pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd b/pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd deleted file mode 100644 index 94491a76..00000000 --- a/pytrustnfe/nfse/campinas/templates/ConsultaSeqRps.xsd +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd b/pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd deleted file mode 100644 index 068756f6..00000000 --- a/pytrustnfe/nfse/campinas/templates/ConsultarLote.xsd +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Schema utilizado para REQUISIçÂO de Consulta de Lote de RPS. - Este Schema XML é utilizado pelos Prestadores de serviços para consultarem Lote de RPS emitidos por eles. - - - - - - Cabeçalho do pedido. - - - - - - Informe o Codigo da Cidade. - - - - - Informe o CPF/CNPJ do Remetente autorizado a transmitir a mensagem XML. - - - - - Informe a Versão do Schema XML utilizado. - - - - - Informe o Número do Lote a ser consultado. - - - - - - - - - diff --git a/pytrustnfe/nfse/campinas/templates/cancelar.xsd b/pytrustnfe/nfse/campinas/templates/cancelar.xsd deleted file mode 100644 index 09f1938a..00000000 --- a/pytrustnfe/nfse/campinas/templates/cancelar.xsd +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - Schema utilizado para Cancelamento de NFSe. - Este Schema XML é utilizado pelos Prestadores de serviços cancelarem NFSe emitidas por eles. - - - - - - Cabeçalho do pedido. - - - - - - Informe o Codigo da Cidade. - - - - - Informe o CPF/CNPJ do Remetente autorizado a transmitir a mensagem XML. - - - - - Informe se as NF-e a serem canceladas farão parte de uma mesma transação. True - As NF-e só serão canceladas se não ocorrer nenhum evento de erro durante o processamento de todo o lote; False - As NF-e aptas a serem canceladas serão canceladas, mesmo que ocorram eventos de erro durante processamento do cancelamento de outras NF-e deste lote. - - - - - Informe a Versão do Schema XML utilizado. - - - - - - - - Detalhe do pedido de cancelamento de NFSe. Cada detalhe deverá conter a Chave de uma NFSe e sua respectiva assinatura de cancelamento. - - - - - - - diff --git a/pytrustnfe/nfse/campinas/templates/consulta_notas.xsd b/pytrustnfe/nfse/campinas/templates/consulta_notas.xsd deleted file mode 100644 index 5784eb81..00000000 --- a/pytrustnfe/nfse/campinas/templates/consulta_notas.xsd +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - Schema utilizado para REQUISIÇAO de consultas - de notas que foram enviadas por lote de RPS. - Este Schema XML é utilizado pelos prestadores - de serviços para consultas de notas que foram enviadas por lote de - RPS. - - - - - - Cabeçalho do pedido. - - - - - - Informe o Codigo da Cidade. - - - - - - Informe o CPF/CNPJ do Remetente - autorizado a transmitir a mensagem XML. - - - - - Informe a Inscrição Municipal do - Prestador - - - - - Informe a data de início do período - transmitido (AAAA-MM-DD). - - - - - Informe a data final do período - transmitido (AAAA-MM-DD). - - - - - Numero da nota inicial da consulta. Ou - seja a consulta ira retornar as notas no periodo, onde o - numero da nota seja maior ou igual a esse numero. O retorno - não pode ultrapassar 500Kb. Caso não tenha o numero da nota, - passar o valor Zero, será retornado as notas geradas no - periodo até o limite de 500kb. - - - - - Informe a Versão. - - - - - - - - - - - - - diff --git a/pytrustnfe/nfse/campinas/templates/ConsultarLote.xml b/pytrustnfe/nfse/campinas/templates/consultarLote.xml similarity index 100% rename from pytrustnfe/nfse/campinas/templates/ConsultarLote.xml rename to pytrustnfe/nfse/campinas/templates/consultarLote.xml diff --git a/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml b/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml deleted file mode 100644 index 65355599..00000000 --- a/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - {{ consulta.cidade }} - {{ consulta.cpf_cnpj }} - true - 1 - - - - - {{ consulta.inscricao_municipal }} - {{ consulta.rps_id }} - {{ consulta.serie_prestacao }} - - - - diff --git a/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd b/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd deleted file mode 100644 index c0e83bfc..00000000 --- a/pytrustnfe/nfse/campinas/templates/consultarNFSeRps.xsd +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - Schema utilizado para Consulta de NFSe. - Este Schema XML é utilizado pelos Prestadores de serviços consultarem NFSe emitidas por eles. - - - - - - Cabeçalho do pedido. - - - - - - Informe o Codigo da Cidade. - - - - - Informe o CPF/CNPJ do Remetente autorizado a transmitir a mensagem XML. - - - - - Informe se as NF-e a serem consultadas farão parte de uma mesma transação. Informe sempre True. - - - - - Informe a Versão do Schema XML utilizado. - - - - - - - - Detalhe do pedido de consulta de NFSe. Cada detalhe deverá conter a Chave de uma NFSe e sua respectiva assinatura de consulta. - - - - - - - diff --git a/pytrustnfe/nfse/campinas/templates/enviar.xsd b/pytrustnfe/nfse/campinas/templates/enviar.xsd deleted file mode 100644 index fedce414..00000000 --- a/pytrustnfe/nfse/campinas/templates/enviar.xsd +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - Schema utilizado para envio de lote de RPS. - Este Schema XML é utilizado pelos prestadores - de serviços para substituição em lote de RPS por NFS-e. - - - - - - - Cabeçalho do Lote. - - - - - - Informe o Codigo da Cidade no Padrão SIAFI. - - - - - - - CNPJ do contribuinte ou CPF do Responsável Legal autorizado a entregar o lote. - - - - - - - Informe o Nome do Contribuinte ou do Responsável Legal - - - - - - - Informe se os RPS a serem - substituídos por - NF-e farão - parte de uma mesma transação. - True - Os RPS só serão - substituídos por NF-e se não - ocorrer nenhum evento de erro - durante o processamento de todo - o lote; False - Os RPS válidos - serão substituídos por NF-e, - mesmo que ocorram eventos de - erro durante processamento de - outros RPS deste lote. Por definição - estão sendo aceitos apenas lotes com RPS válidos, - o lote é - recusado caso haja algum RPS inválido. - - - - - - - Informe a data de início do - período - transmitido - (AAAA-MM-DD). - - - - - - - Informe a data final do período - transmitido - (AAAA-MM-DD). - - - - - - - Informe o total de RPS contidos - na mensagem - XML. OBS: O xml não pode ultrapassar o tamanho maximo de 500kb. - - - - - - - Informe o valor total dos - serviços prestados - dos RPS - contidos na mensagem XML. - - - - - - - Informe o valor total das - deduções dos RPS - contidos na - mensagem XML. - - - - - - - Informe a Versão do Schema XML - utilizado. - - - - - - Informe o Método de Envio - - - - - Versão da DLL de envio de lote. Não é necessário informar esse campo caso não utilize a DLL. - - - - - - - - - - Informe os RPS a serem substituidos por - NF-e. - - - - - - - diff --git a/setup.py b/setup.py index a1add7ca..ff137cf6 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.9.2" +VERSION = "0.9.3" setup( name="PyTrustNFe3", @@ -27,6 +27,7 @@ 'nfe/templates/*xml', 'nfe/fonts/*ttf', 'nfse/paulistana/templates/*xml', + 'nfse/campinas/templates/*xml', 'nfse/ginfes/templates/*xml', 'nfse/simpliss/templates/*xml', 'nfse/betha/templates/*xml', From 0a21cfaad8740022e2794ce10eb38c161d446923 Mon Sep 17 00:00:00 2001 From: carcaroff Date: Fri, 22 Dec 2017 15:35:45 -0200 Subject: [PATCH 21/31] [FIX]Campo CPF --- pytrustnfe/nfe/danfe.py | 1669 +++++++++++++++++++-------------------- setup.py | 2 +- 2 files changed, 831 insertions(+), 840 deletions(-) diff --git a/pytrustnfe/nfe/danfe.py b/pytrustnfe/nfe/danfe.py index 22338774..bfe96926 100644 --- a/pytrustnfe/nfe/danfe.py +++ b/pytrustnfe/nfe/danfe.py @@ -1,839 +1,830 @@ -# -*- coding: utf-8 -*- -# © 2017 Edson Bernardino, ITK Soft -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -# Classe para geração de PDF da DANFE a partir de xml etree.fromstring - -import os -from io import BytesIO -from textwrap import wrap - -from reportlab.lib import utils -from reportlab.pdfgen import canvas -from reportlab.lib.units import mm, cm -from reportlab.lib.pagesizes import A4 -from reportlab.lib.colors import black, gray -from reportlab.graphics.barcode import code128 -from reportlab.lib.styles import getSampleStyleSheet -from reportlab.lib.enums import TA_CENTER -from reportlab.platypus import Paragraph, Image -from reportlab.pdfbase import pdfmetrics -from reportlab.pdfbase.ttfonts import TTFont - - -def chunks(cString, nLen): - for start in range(0, len(cString), nLen): - yield cString[start:start+nLen] - - -def format_cnpj_cpf(value): - if len(value) < 12: # CPF - cValue = '%s.%s.%s-%s' % (value[:-8], value[-8:-5], - value[-5:-2], value[-2:]) - else: - cValue = '%s.%s.%s/%s-%s' % (value[:-12], value[-12:-9], - value[-9:-6], value[-6:-2], value[-2:]) - return cValue - - -def getdateUTC(cDateUTC): - cDt = cDateUTC[0:10].split('-') - cDt.reverse() - return '/'.join(cDt), cDateUTC[11:16] - - -def format_number(cNumber, precision=0, group_sep='.', decimal_sep=','): - if cNumber: - number = float(cNumber) - return ("{:,." + str(precision) + "f}").format(number).\ - replace(",", "X").replace(".", ",").replace("X", ".") - return "" - - -def tagtext(oNode=None, cTag=None): - try: - xpath = ".//{http://www.portalfiscal.inf.br/nfe}%s" % (cTag) - cText = oNode.find(xpath).text - except: - cText = '' - return cText - -REGIME_TRIBUTACAO = { - '1': 'Simples Nacional', - '2': 'Simples Nacional, excesso sublimite de receita bruta', - '3': 'Regime Normal' -} - - -def get_image(path, width=1*cm): - img = utils.ImageReader(path) - iw, ih = img.getSize() - aspect = ih / float(iw) - return Image(path, width=width, height=(width * aspect)) - - -class danfe(object): - def __init__(self, sizepage=A4, list_xml=None, recibo=True, - orientation='portrait', logo=None): - - path = os.path.join(os.path.dirname(__file__), 'fonts') - pdfmetrics.registerFont( - TTFont('NimbusSanL-Regu', - os.path.join(path, 'NimbusSanL Regular.ttf'))) - pdfmetrics.registerFont( - TTFont('NimbusSanL-Bold', - os.path.join(path, 'NimbusSanL Bold.ttf'))) - self.width = 210 # 21 x 29,7cm - self.height = 297 - self.nLeft = 10 - self.nRight = 10 - self.nTop = 7 - self.nBottom = 8 - self.nlin = self.nTop - self.logo = logo - self.oFrete = {'0': '0 - Emitente', - '1': '1 - Dest/Remet', - '2': '2 - Terceiros', - '9': '9 - Sem Frete'} - - self.oPDF_IO = BytesIO() - if orientation == 'landscape': - raise NameError('Rotina não implementada') - else: - size = sizepage - - self.canvas = canvas.Canvas(self.oPDF_IO, pagesize=size) - self.canvas.setTitle('DANFE') - self.canvas.setStrokeColor(black) - - for oXML in list_xml: - oXML_cobr = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}cobr") - - self.NrPages = 1 - self.Page = 1 - - # Calculando total linhas usadas para descrições dos itens - # Com bloco fatura, apenas 29 linhas para itens na primeira folha - nNr_Lin_Pg_1 = 34 if oXML_cobr is None else 30 - # [ rec_ini , rec_fim , lines , limit_lines ] - oPaginator = [[0, 0, 0, nNr_Lin_Pg_1]] - el_det = oXML.findall(".//{http://www.portalfiscal.inf.br/nfe}det") - if el_det is not None: - list_desc = [] - list_cod_prod = [] - nPg = 0 - for nId, item in enumerate(el_det): - el_prod = item.find( - ".//{http://www.portalfiscal.inf.br/nfe}prod") - infAdProd = item.find( - ".//{http://www.portalfiscal.inf.br/nfe}infAdProd") - - list_ = wrap(tagtext(oNode=el_prod, cTag='xProd'), 56) - if infAdProd is not None: - list_.extend(wrap(infAdProd.text, 56)) - list_desc.append(list_) - - list_cProd = wrap(tagtext(oNode=el_prod, cTag='cProd'), 14) - list_cod_prod.append(list_cProd) - - # Nr linhas necessárias p/ descrição item - nLin_Itens = len(list_) - - if (oPaginator[nPg][2] + nLin_Itens) >= oPaginator[nPg][3]: - oPaginator.append([0, 0, 0, 77]) - nPg += 1 - oPaginator[nPg][0] = nId - oPaginator[nPg][1] = nId + 1 - oPaginator[nPg][2] = nLin_Itens - else: - # adiciona-se 1 pelo funcionamento de xrange - oPaginator[nPg][1] = nId + 1 - oPaginator[nPg][2] += nLin_Itens - - self.NrPages = len(oPaginator) # Calculando nr. páginas - - if recibo: - self.recibo_entrega(oXML=oXML) - - self.ide_emit(oXML=oXML) - self.destinatario(oXML=oXML) - - if oXML_cobr is not None: - self.faturas(oXML=oXML_cobr) - - self.impostos(oXML=oXML) - self.transportes(oXML=oXML) - self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPaginator[0], - list_desc=list_desc, list_cod_prod=list_cod_prod) - - self.adicionais(oXML=oXML) - - # Gera o restante das páginas do XML - for oPag in oPaginator[1:]: - self.newpage() - self.ide_emit(oXML=oXML) - self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPag, - list_desc=list_desc, nHeight=77, - list_cod_prod=list_cod_prod) - - self.newpage() - - self.canvas.save() - - def ide_emit(self, oXML=None): - elem_infNFe = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}infNFe") - elem_protNFe = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}protNFe") - elem_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") - elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") - - cChave = elem_infNFe.attrib.get('Id')[3:] - barcode128 = code128.Code128(cChave, barHeight=10*mm, barWidth=0.25*mm) - - self.canvas.setLineWidth(.5) - self.rect(self.nLeft, self.nlin+1, self.nLeft+75, 32) - self.rect(self.nLeft+115, self.nlin+1, - self.width-self.nLeft-self.nRight-115, 39) - - self.hline(self.nLeft+85, self.nlin+1, 125) - - self.rect(self.nLeft+116, self.nlin+15, - self.width-self.nLeft-self.nRight-117, 6) - - self.rect(self.nLeft, self.nlin+33, - self.width-self.nLeft-self.nRight, 14) - self.hline(self.nLeft, self.nlin+40, self.width-self.nRight) - self.vline(self.nLeft+60, self.nlin+40, 7) - self.vline(self.nLeft+100, self.nlin+40, 7) - - # Labels - self.canvas.setFont('NimbusSanL-Bold', 12) - self.stringcenter(self.nLeft+98, self.nlin+5, 'DANFE') - self.stringcenter(self.nLeft+109, self.nlin+19.5, - tagtext(oNode=elem_ide, cTag='tpNF')) - self.canvas.setFont('NimbusSanL-Bold', 8) - cNF = tagtext(oNode=elem_ide, cTag='nNF') - cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") - self.stringcenter(self.nLeft+100, self.nlin+25, "Nº %s" % (cNF)) - - self.stringcenter(self.nLeft+100, self.nlin+29, "SÉRIE %s" % ( - tagtext(oNode=elem_ide, cTag='serie'))) - cPag = "Página %s de %s" % (str(self.Page), str(self.NrPages)) - self.stringcenter(self.nLeft+100, self.nlin+32, cPag) - self.canvas.setFont('NimbusSanL-Regu', 6) - 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+16, '0 - Entrada') - self.string(self.nLeft+86, self.nlin+19, '1 - Saída') - self.rect(self.nLeft+105, self.nlin+15, 8, 6) - - self.stringcenter( - self.nLeft+152, self.nlin+25, - 'Consulta de autenticidade no portal nacional da NF-e') - self.stringcenter( - self.nLeft+152, self.nlin+28, - 'www.nfe.fazenda.gov.br/portal ou no site da SEFAZ Autorizadora') - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+117, self.nlin+16.7, 'CHAVE DE ACESSO') - self.string(self.nLeft+116, self.nlin+2.7, 'CONTROLE DO FISCO') - - self.string(self.nLeft+1, self.nlin+34.7, 'NATUREZA DA OPERAÇÃO') - self.string(self.nLeft+116, self.nlin+34.7, - 'PROTOCOLO DE AUTORIZAÇÃO DE USO') - self.string(self.nLeft+1, self.nlin+41.7, 'INSCRIÇÃO ESTADUAL') - self.string(self.nLeft+61, self.nlin+41.7, - 'INSCRIÇÃO ESTADUAL DO SUBST. TRIB.') - self.string(self.nLeft+101, self.nlin+41.7, 'CNPJ') - - # Conteúdo campos - barcode128.drawOn(self.canvas, (self.nLeft+111.5)*mm, - (self.height-self.nlin-14)*mm) - self.canvas.setFont('NimbusSanL-Bold', 6) - nW_Rect = (self.width-self.nLeft-self.nRight-117) / 2 - self.stringcenter(self.nLeft+116.5+nW_Rect, self.nlin+19.5, - ' '.join(chunks(cChave, 4))) # Chave - self.canvas.setFont('NimbusSanL-Regu', 8) - cDt, cHr = getdateUTC(tagtext(oNode=elem_protNFe, cTag='dhRecbto')) - cProtocolo = tagtext(oNode=elem_protNFe, cTag='nProt') - cDt = cProtocolo + ' - ' + cDt + ' ' + cHr - nW_Rect = (self.width-self.nLeft-self.nRight-110) / 2 - self.stringcenter(self.nLeft+115+nW_Rect, self.nlin+38.7, cDt) - self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+38.7, - tagtext(oNode=elem_ide, cTag='natOp')) - self.string(self.nLeft+1, self.nlin+46, - tagtext(oNode=elem_emit, cTag='IE')) - self.string(self.nLeft+101, self.nlin+46, - format_cnpj_cpf(tagtext(oNode=elem_emit, cTag='CNPJ'))) - - styles = getSampleStyleSheet() - styleN = styles['Normal'] - styleN.fontSize = 10 - styleN.fontName = 'NimbusSanL-Bold' - styleN.alignment = TA_CENTER - - # Razão Social emitente - P = Paragraph(tagtext(oNode=elem_emit, cTag='xNome'), styleN) - w, h = P.wrap(55*mm, 50*mm) - P.drawOn(self.canvas, (self.nLeft+30)*mm, - (self.height-self.nlin-12)*mm) - - if self.logo: - img = get_image(self.logo, width=2*cm) - img.drawOn(self.canvas, (self.nLeft+5)*mm, - (self.height-self.nlin-22)*mm) - - cEnd = tagtext(oNode=elem_emit, cTag='xLgr') + ', ' + tagtext( - oNode=elem_emit, cTag='nro') + ' - ' - cEnd += tagtext(oNode=elem_emit, cTag='xBairro') + '
' + tagtext( - oNode=elem_emit, cTag='xMun') + ' - ' - cEnd += 'Fone: ' + tagtext(oNode=elem_emit, cTag='fone') + '
' - cEnd += tagtext(oNode=elem_emit, cTag='UF') + ' - ' + tagtext( - oNode=elem_emit, cTag='CEP') - - regime = tagtext(oNode=elem_emit, cTag='CRT') - cEnd += '
Regime Tributário: %s' % (REGIME_TRIBUTACAO[regime]) - - styleN.fontName = 'NimbusSanL-Regu' - styleN.fontSize = 7 - styleN.leading = 10 - P = Paragraph(cEnd, styleN) - w, h = P.wrap(55*mm, 30*mm) - P.drawOn(self.canvas, (self.nLeft+30)*mm, - (self.height-self.nlin-31)*mm) - - # Homologação - if tagtext(oNode=elem_ide, cTag='tpAmb') == '2': - self.canvas.saveState() - self.canvas.rotate(90) - self.canvas.setFont('Times-Bold', 40) - self.canvas.setFillColorRGB(0.57, 0.57, 0.57) - self.string(self.nLeft+65, 449, 'SEM VALOR FISCAL') - self.canvas.restoreState() - - self.nlin += 48 - - def destinatario(self, oXML=None): - elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") - elem_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") - nMr = self.width-self.nRight - - self.nlin += 1 - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'DESTINATÁRIO/REMETENTE') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 20) - self.vline(nMr-25, self.nlin+2, 20) - self.hline(self.nLeft, self.nlin+8.66, self.width-self.nLeft) - self.hline(self.nLeft, self.nlin+15.32, self.width-self.nLeft) - self.vline(nMr-70, self.nlin+2, 6.66) - self.vline(nMr-53, self.nlin+8.66, 6.66) - self.vline(nMr-99, self.nlin+8.66, 6.66) - self.vline(nMr-90, self.nlin+15.32, 6.66) - self.vline(nMr-102, self.nlin+15.32, 6.66) - self.vline(nMr-136, self.nlin+15.32, 6.66) - # Labels/Fields - self.canvas.setFont('NimbusSanL-Bold', 5) - self.string(self.nLeft+1, self.nlin+3.7, 'NOME/RAZÃO SOCIAL') - self.string(nMr-69, self.nlin+3.7, 'CNPJ/CPF') - self.string(nMr-24, self.nlin+3.7, 'DATA DA EMISSÃO') - self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') - self.string(nMr-98, self.nlin+10.3, 'BAIRRO/DISTRITO') - self.string(nMr-52, self.nlin+10.3, 'CEP') - self.string(nMr-24, self.nlin+10.3, 'DATA DE ENTRADA/SAÍDA') - self.string(self.nLeft+1, self.nlin+17.1, 'MUNICÍPIO') - self.string(nMr-135, self.nlin+17.1, 'FONE/FAX') - self.string(nMr-101, self.nlin+17.1, 'UF') - self.string(nMr-89, self.nlin+17.1, 'INSCRIÇÃO ESTADUAL') - self.string(nMr-24, self.nlin+17.1, 'HORA DE ENTRADA/SAÍDA') - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+7.5, - tagtext(oNode=elem_dest, cTag='xNome')) - self.string(nMr-69, self.nlin+7.5, - format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CNPJ'))) - cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhEmi')) - self.string(nMr-24, self.nlin+7.7, cDt + ' ' + cHr) - cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhSaiEnt')) - self.string(nMr-24, self.nlin+14.3, cDt + ' ' + cHr) # Dt saída - cEnd = tagtext(oNode=elem_dest, cTag='xLgr') + ', ' + tagtext( - oNode=elem_dest, cTag='nro') - self.string(self.nLeft+1, self.nlin+14.3, cEnd) - self.string(nMr-98, self.nlin+14.3, - tagtext(oNode=elem_dest, cTag='xBairro')) - self.string(nMr-52, self.nlin+14.3, - tagtext(oNode=elem_dest, cTag='CEP')) - self.string(self.nLeft+1, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='xMun')) - self.string(nMr-135, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='fone')) - self.string(nMr-101, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='UF')) - self.string(nMr-89, self.nlin+21.1, - tagtext(oNode=elem_dest, cTag='IE')) - - self.nlin += 24 # Nr linhas ocupadas pelo bloco - - def faturas(self, oXML=None): - - nMr = self.width-self.nRight - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'FATURA') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 13) - self.vline(nMr-47.5, self.nlin+2, 13) - self.vline(nMr-95, self.nlin+2, 13) - self.vline(nMr-142.5, self.nlin+2, 13) - self.hline(nMr-47.5, self.nlin+8.5, self.width-self.nLeft) - # Labels - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(nMr-46.5, self.nlin+3.8, 'CÓDIGO VENDEDOR') - self.string(nMr-46.5, self.nlin+10.2, 'NOME VENDEDOR') - self.string(nMr-93.5, self.nlin+3.8, - 'FATURA VENCIMENTO VALOR') - self.string(nMr-140.5, self.nlin+3.8, - 'FATURA VENCIMENTO VALOR') - self.string(self.nLeft+2, self.nlin+3.8, - 'FATURA VENCIMENTO VALOR') - - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Bold', 6) - nLin = 7 - nPar = 1 - nCol = 0 - nAju = 0 - - line_iter = iter(oXML[1:10]) # Salta elemt 1 e considera os próximos 9 - for oXML_dup in line_iter: - - cDt, cHr = getdateUTC(tagtext(oNode=oXML_dup, cTag='dVenc')) - self.string(self.nLeft+nCol+1, self.nlin+nLin, - tagtext(oNode=oXML_dup, cTag='nDup')) - self.string(self.nLeft+nCol+17, self.nlin+nLin, cDt) - self.stringRight( - self.nLeft+nCol+47, self.nlin+nLin, - format_number(tagtext(oNode=oXML_dup, cTag='vDup'), - precision=2)) - - if nPar == 3: - nLin = 7 - nPar = 1 - nCol += 47 - nAju += 1 - nCol += nAju * (0.3) - else: - nLin += 3.3 - nPar += 1 - - # Campos adicionais XML - Condicionados a existencia de financeiro - elem_infAdic = oXML.getparent().find( - ".//{http://www.portalfiscal.inf.br/nfe}infAdic") - if elem_infAdic is not None: - codvend = elem_infAdic.find( - ".//{http://www.portalfiscal.inf.br/nfe}obsCont\ -[@xCampo='CodVendedor']") - self.string(nMr-46.5, self.nlin+7.7, - tagtext(oNode=codvend, cTag='xTexto')) - vend = elem_infAdic.find(".//{http://www.portalfiscal.inf.br/nfe}\ -obsCont[@xCampo='NomeVendedor']") - self.string(nMr-46.5, self.nlin+14.3, - tagtext(oNode=vend, cTag='xTexto')[:36]) - - self.nlin += 16 # Nr linhas ocupadas pelo bloco - - def impostos(self, oXML=None): - # Impostos - el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") - nMr = self.width-self.nRight - self.nlin += 1 - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'CÁLCULO DO IMPOSTO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 13) - self.hline(self.nLeft, self.nlin+8.5, self.width-self.nLeft) - self.vline(nMr-35, self.nlin+2, 6.5) - self.vline(nMr-65, self.nlin+2, 6.5) - self.vline(nMr-95, self.nlin+2, 6.5) - self.vline(nMr-125, self.nlin+2, 6.5) - self.vline(nMr-155, self.nlin+2, 6.5) - self.vline(nMr-35, self.nlin+8.5, 6.5) - self.vline(nMr-65, self.nlin+8.5, 6.5) - self.vline(nMr-95, self.nlin+8.5, 6.5) - self.vline(nMr-125, self.nlin+8.5, 6.5) - self.vline(nMr-155, self.nlin+8.5, 6.5) - # Labels - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+1, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS') - self.string(nMr-154, self.nlin+3.8, 'VALOR DO ICMS') - self.string(nMr-124, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS ST') - self.string(nMr-94, self.nlin+3.8, 'VALOR DO ICMS ST') - self.string(nMr-64, self.nlin+3.8, 'VALOR APROX TRIBUTOS') - self.string(nMr-34, self.nlin+3.8, 'VALOR TOTAL DOS PRODUTOS') - - self.string(self.nLeft+1, self.nlin+10.2, 'VALOR DO FRETE') - self.string(nMr-154, self.nlin+10.2, 'VALOR DO SEGURO') - self.string(nMr-124, self.nlin+10.2, 'DESCONTO') - self.string(nMr-94, self.nlin+10.2, 'OUTRAS DESP. ACESSÓRIAS') - self.string(nMr-64, self.nlin+10.2, 'VALOR DO IPI') - self.string(nMr-34, self.nlin+10.2, 'VALOR TOTAL DA NOTA') - - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 8) - self.stringRight( - self.nLeft+34, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vBC'), precision=2)) - self.stringRight( - self.nLeft+64, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vICMS'), precision=2)) - self.stringRight( - self.nLeft+94, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vBCST'), precision=2)) - self.stringRight( - nMr-66, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vST'), precision=2)) - self.stringRight( - nMr-36, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vTotTrib'), - precision=2)) - self.stringRight( - nMr-1, self.nlin+7.7, - format_number(tagtext(oNode=el_total, cTag='vProd'), precision=2)) - self.stringRight( - self.nLeft+34, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vFrete'), precision=2)) - self.stringRight( - self.nLeft+64, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vSeg'), precision=2)) - self.stringRight( - self.nLeft+94, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vDesc'), precision=2)) - self.stringRight( - self.nLeft+124, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vOutro'), precision=2)) - self.stringRight( - self.nLeft+154, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vIPI'), precision=2)) - self.stringRight( - nMr-1, self.nlin+14.1, - format_number(tagtext(oNode=el_total, cTag='vNF'), precision=2)) - - self.nlin += 17 # Nr linhas ocupadas pelo bloco - - def transportes(self, oXML=None): - el_transp = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}transp") - nMr = self.width-self.nRight - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, - 'TRANSPORTADOR/VOLUMES TRANSPORTADOS') - self.canvas.setFont('NimbusSanL-Regu', 5) - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 20) - self.hline(self.nLeft, self.nlin+8.6, self.width-self.nLeft) - self.hline(self.nLeft, self.nlin+15.2, self.width-self.nLeft) - self.vline(nMr-40, self.nlin+2, 13.2) - self.vline(nMr-49, self.nlin+2, 20) - self.vline(nMr-92, self.nlin+2, 6.6) - self.vline(nMr-120, self.nlin+2, 6.6) - self.vline(nMr-75, self.nlin+2, 6.6) - self.vline(nMr-26, self.nlin+15.2, 6.6) - self.vline(nMr-102, self.nlin+8.6, 6.6) - self.vline(nMr-85, self.nlin+15.2, 6.6) - self.vline(nMr-121, self.nlin+15.2, 6.6) - self.vline(nMr-160, self.nlin+15.2, 6.6) - # Labels/Fields - self.string(nMr-39, self.nlin+3.8, 'CNPJ/CPF') - self.string(nMr-74, self.nlin+3.8, 'PLACA DO VEÍCULO') - self.string(nMr-91, self.nlin+3.8, 'CÓDIGO ANTT') - self.string(nMr-119, self.nlin+3.8, 'FRETE POR CONTA') - self.string(self.nLeft+1, self.nlin+3.8, 'RAZÃO SOCIAL') - self.string(nMr-48, self.nlin+3.8, 'UF') - self.string(nMr-39, self.nlin+10.3, 'INSCRIÇÃO ESTADUAL') - self.string(nMr-48, self.nlin+10.3, 'UF') - self.string(nMr-101, self.nlin+10.3, 'MUNICÍPIO') - self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') - self.string(nMr-48, self.nlin+17, 'PESO BRUTO') - self.string(nMr-25, self.nlin+17, 'PESO LÍQUIDO') - self.string(nMr-84, self.nlin+17, 'NUMERAÇÃO') - self.string(nMr-120, self.nlin+17, 'MARCA') - self.string(nMr-159, self.nlin+17, 'ESPÉCIE') - self.string(self.nLeft+1, self.nlin+17, 'QUANTIDADE') - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+7.7, - tagtext(oNode=el_transp, cTag='xNome')[:40]) - self.string(self.nLeft+71, self.nlin+7.7, - self.oFrete[tagtext(oNode=el_transp, cTag='modFrete')]) - self.string(nMr-39, self.nlin+7.7, - format_cnpj_cpf(tagtext(oNode=el_transp, cTag='CNPJ'))) - self.string(self.nLeft+1, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='xEnder')[:45]) - self.string(self.nLeft+89, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='xMun')) - self.string(nMr-48, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='UF')) - self.string(nMr-39, self.nlin+14.2, - tagtext(oNode=el_transp, cTag='IE')) - self.string(self.nLeft+1, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='qVol')) - self.string(self.nLeft+31, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='esp')) - self.string(self.nLeft+70, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='marca')) - self.string(self.nLeft+106, self.nlin+21.2, - tagtext(oNode=el_transp, cTag='nVol')) - self.stringRight( - nMr-27, self.nlin+21.2, - format_number(tagtext(oNode=el_transp, cTag='pesoB'), precision=3)) - self.stringRight( - nMr-1, self.nlin+21.2, - format_number(tagtext(oNode=el_transp, cTag='pesoL'), precision=3)) - - self.nlin += 23 - - def produtos(self, oXML=None, el_det=None, oPaginator=None, - list_desc=None, list_cod_prod=None, nHeight=29): - - nMr = self.width-self.nRight - nStep = 2.5 # Passo entre linhas - nH = 7.5 + (nHeight * nStep) # cabeçalho 7.5 - self.nlin += 1 - - self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'DADOS DO PRODUTO/SERVIÇO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, nH) - self.hline(self.nLeft, self.nlin+8, self.width-self.nLeft) - - self.canvas.setFont('NimbusSanL-Regu', 5.5) - # Colunas - self.vline(self.nLeft+15, self.nlin+2, nH) - self.stringcenter(self.nLeft+7.5, self.nlin+5.5, 'CÓDIGO') - self.vline(nMr-7, self.nlin+2, nH) - self.stringcenter(nMr-3.5, self.nlin+4.5, 'ALÍQ') - self.stringcenter(nMr-3.5, self.nlin+6.5, 'IPI') - self.vline(nMr-14, self.nlin+2, nH) - self.stringcenter(nMr-10.5, self.nlin+4.5, 'ALÍQ') - self.stringcenter(nMr-10.5, self.nlin+6.5, 'ICMS') - self.vline(nMr-26, self.nlin+2, nH) - self.stringcenter(nMr-20, self.nlin+5.5, 'VLR. IPI') - self.vline(nMr-38, self.nlin+2, nH) - self.stringcenter(nMr-32, self.nlin+5.5, 'VLR. ICMS') - self.vline(nMr-50, self.nlin+2, nH) - self.stringcenter(nMr-44, self.nlin+5.5, 'BC ICMS') - self.vline(nMr-64, self.nlin+2, nH) - self.stringcenter(nMr-57, self.nlin+5.5, 'VLR TOTAL') - self.vline(nMr-77, self.nlin+2, nH) - self.stringcenter(nMr-70.5, self.nlin+5.5, 'VLR UNIT') - self.vline(nMr-90, self.nlin+2, nH) - self.stringcenter(nMr-83.5, self.nlin+5.5, 'QTD') - self.vline(nMr-96, self.nlin+2, nH) - self.stringcenter(nMr-93, self.nlin+5.5, 'UNID') - self.vline(nMr-102, self.nlin+2, nH) - self.stringcenter(nMr-99, self.nlin+5.5, 'CFOP') - self.vline(nMr-108, self.nlin+2, nH) - self.stringcenter(nMr-105, self.nlin+5.5, 'CST') - self.vline(nMr-117, self.nlin+2, nH) - self.stringcenter(nMr-112.5, self.nlin+5.5, 'NCM/SH') - - nWidth_Prod = nMr-135-self.nLeft-11 - nCol_ = self.nLeft+20 + (nWidth_Prod / 2) - self.stringcenter(nCol_, self.nlin+5.5, 'DESCRIÇÃO DO PRODUTO/SERVIÇO') - - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Regu', 5) - nLin = self.nlin+10.5 - - for id in range(oPaginator[0], oPaginator[1]): - item = el_det[id] - el_prod = item.find(".//{http://www.portalfiscal.inf.br/nfe}prod") - el_imp = item.find( - ".//{http://www.portalfiscal.inf.br/nfe}imposto") - - el_imp_ICMS = el_imp.find( - ".//{http://www.portalfiscal.inf.br/nfe}ICMS") - el_imp_IPI = el_imp.find( - ".//{http://www.portalfiscal.inf.br/nfe}IPI") - - cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \ - tagtext(oNode=el_imp_ICMS, cTag='CST') - vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC') - vICMS = tagtext(oNode=el_imp_ICMS, cTag='vICMS') - pICMS = tagtext(oNode=el_imp_ICMS, cTag='pICMS') - - vIPI = tagtext(oNode=el_imp_IPI, cTag='vIPI') - pIPI = tagtext(oNode=el_imp_IPI, cTag='pIPI') - - self.stringcenter(nMr-112.5, nLin, - tagtext(oNode=el_prod, cTag='NCM')) - self.stringcenter(nMr-105, nLin, cCST) - self.stringcenter(nMr-99, nLin, - tagtext(oNode=el_prod, cTag='CFOP')) - self.stringcenter(nMr-93, nLin, - 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-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)) - - if vIPI: - self.stringRight(nMr-14.5, nLin, - format_number(vIPI, precision=2)) - if pIPI: - self.stringRight(nMr-0.5, nLin, - format_number(pIPI, precision=2)) - - # Código Item - line_cod = nLin - for des in list_cod_prod[id]: - self.string(self.nLeft+0.2, line_cod, des) - line_cod += nStep - - # Descrição Item - line_desc = nLin - for des in list_desc[id]: - self.string(self.nLeft+15.5, line_desc, des) - line_desc += nStep - - nLin = max(line_cod, line_desc) - self.canvas.setStrokeColor(gray) - self.hline(self.nLeft, nLin-2, self.width-self.nLeft) - self.canvas.setStrokeColor(black) - - self.nlin += nH + 3 - - def adicionais(self, oXML=None): - el_infAdic = oXML.find( - ".//{http://www.portalfiscal.inf.br/nfe}infAdic") - - self.nlin += 2 - self.canvas.setFont('NimbusSanL-Bold', 6) - self.string(self.nLeft+1, self.nlin+1, 'DADOS ADICIONAIS') - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+1, self.nlin+4, 'INFORMAÇÕES COMPLEMENTARES') - self.string((self.width/2)+1, self.nlin+4, 'RESERVADO AO FISCO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 42) - self.vline(self.width/2, self.nlin+2, 42) - # Conteúdo campos - styles = getSampleStyleSheet() - styleN = styles['Normal'] - styleN.fontSize = 6 - styleN.fontName = 'NimbusSanL-Regu' - styleN.leading = 7 - - fisco = tagtext(oNode=el_infAdic, cTag='infAdFisco') - observacoes = tagtext(oNode=el_infAdic, cTag='infCpl') - if fisco: - observacoes = fisco + ' ' + observacoes - P = Paragraph(observacoes, styles['Normal']) - w, h = P.wrap(92*mm, 32*mm) - altura = (self.height-self.nlin-5)*mm - P.drawOn(self.canvas, (self.nLeft+1)*mm, altura - h) - self.nlin += 36 - - def recibo_entrega(self, oXML=None): - el_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") - el_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") - el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") - el_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") - - # self.nlin = self.height-self.nBottom-18 # 17 altura recibo - nW = 40 - nH = 17 - self.canvas.setLineWidth(.5) - self.rect(self.nLeft, self.nlin, - self.width-(self.nLeft+self.nRight), nH) - self.hline(self.nLeft, self.nlin+8.5, self.width-self.nRight-nW) - self.vline(self.width-self.nRight-nW, self.nlin, nH) - self.vline(self.nLeft+nW, self.nlin+8.5, 8.5) - - # Labels - self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+1, self.nlin+10.2, 'DATA DE RECEBIMENTO') - self.string(self.nLeft+41, self.nlin+10.2, - 'IDENTIFICAÇÃO E ASSINATURA DO RECEBEDOR') - self.stringcenter(self.width-self.nRight-(nW/2), self.nlin+2, 'NF-e') - # Conteúdo campos - self.canvas.setFont('NimbusSanL-Bold', 8) - cNF = tagtext(oNode=el_ide, cTag='nNF') - cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") - self.string(self.width-self.nRight-nW+2, self.nlin+8, "Nº %s" % (cNF)) - self.string(self.width-self.nRight-nW+2, self.nlin+14, - "SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie'))) - - cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi')) - cTotal = format_number(tagtext(oNode=el_total, cTag='vNF'), - precision=2) - - cEnd = tagtext(oNode=el_dest, cTag='xNome') + ' - ' - cEnd += tagtext(oNode=el_dest, cTag='xLgr') + ', ' + tagtext( - oNode=el_dest, cTag='nro') + ', ' - cEnd += tagtext(oNode=el_dest, cTag='xBairro') + ', ' + tagtext( - oNode=el_dest, cTag='xMun') + ' - ' - cEnd += tagtext(oNode=el_dest, cTag='UF') - - cString = """ - RECEBEMOS DE %s OS PRODUTOS/SERVIÇOS CONSTANTES DA NOTA FISCAL INDICADA - ABAIXO. EMISSÃO: %s VALOR TOTAL: %s - DESTINATARIO: %s""" % (tagtext(oNode=el_emit, cTag='xNome'), - cDt, cTotal, cEnd) - - styles = getSampleStyleSheet() - styleN = styles['Normal'] - styleN.fontName = 'NimbusSanL-Regu' - styleN.fontSize = 6 - styleN.leading = 7 - - P = Paragraph(cString, styleN) - w, h = P.wrap(149*mm, 7*mm) - P.drawOn(self.canvas, (self.nLeft+1)*mm, - ((self.height-self.nlin)*mm) - h) - - self.nlin += 20 - self.hline(self.nLeft, self.nlin, self.width-self.nRight) - self.nlin += 2 - - def newpage(self): - self.nlin = self.nTop - self.Page += 1 - self.canvas.showPage() - - def hline(self, x, y, width): - y = self.height - y - self.canvas.line(x*mm, y*mm, width*mm, y*mm) - - def vline(self, x, y, width): - width = self.height - y - width - y = self.height - y - self.canvas.line(x*mm, y*mm, x*mm, width*mm) - - def rect(self, col, lin, nWidth, nHeight, fill=False): - lin = self.height - nHeight - lin - self.canvas.rect(col*mm, lin*mm, nWidth*mm, nHeight*mm, - stroke=True, fill=fill) - - def string(self, x, y, value): - y = self.height - y - self.canvas.drawString(x*mm, y*mm, value) - - def stringRight(self, x, y, value): - y = self.height - y - self.canvas.drawRightString(x*mm, y*mm, value) - - def stringcenter(self, x, y, value): - y = self.height - y - self.canvas.drawCentredString(x*mm, y*mm, value) - - def writeto_pdf(self, fileObj): - pdf_out = self.oPDF_IO.getvalue() - self.oPDF_IO.close() - fileObj.write(pdf_out) +# -*- coding: utf-8 -*- +# © 2017 Edson Bernardino, ITK Soft +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Classe para geração de PDF da DANFE a partir de xml etree.fromstring + + +from cStringIO import StringIO as IO +from textwrap import wrap + +from reportlab.lib import utils +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm, cm +from reportlab.lib.pagesizes import A4 +from reportlab.lib.colors import black, gray +from reportlab.graphics.barcode import code128 +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.enums import TA_CENTER +from reportlab.platypus import Paragraph, Image + + +def chunks(cString, nLen): + for start in range(0, len(cString), nLen): + yield cString[start:start+nLen] + + +def format_cnpj_cpf(value): + if len(value) < 12: # CPF + cValue = '%s.%s.%s-%s' % (value[:-8], value[-8:-5], + value[-5:-2], value[-2:]) + else: + cValue = '%s.%s.%s/%s-%s' % (value[:-12], value[-12:-9], + value[-9:-6], value[-6:-2], value[-2:]) + return cValue + + +def getdateUTC(cDateUTC): + cDt = cDateUTC[0:10].split('-') + cDt.reverse() + return '/'.join(cDt), cDateUTC[11:16] + + +def format_number(cNumber, precision=0, group_sep='.', decimal_sep=','): + if cNumber: + number = float(cNumber) + return ("{:,." + str(precision) + "f}").format(number).\ + replace(",", "X").replace(".", ",").replace("X", ".") + return "" + + +def tagtext(oNode=None, cTag=None): + try: + xpath = ".//{http://www.portalfiscal.inf.br/nfe}%s" % (cTag) + cText = oNode.find(xpath).text + except: + cText = '' + return cText + + +REGIME_TRIBUTACAO = { + '1': 'Simples Nacional', + '2': 'Simples Nacional, excesso sublimite de receita bruta', + '3': 'Regime Normal' +} + + +def get_image(path, width=1*cm): + img = utils.ImageReader(path) + iw, ih = img.getSize() + aspect = ih / float(iw) + return Image(path, width=width, height=(width * aspect)) + + +class danfe(object): + def __init__(self, sizepage=A4, list_xml=None, recibo=True, + orientation='portrait', logo=None): + self.width = 210 # 21 x 29,7cm + self.height = 297 + self.nLeft = 10 + self.nRight = 10 + self.nTop = 7 + self.nBottom = 8 + self.nlin = self.nTop + self.logo = logo + self.oFrete = {'0': '0 - Emitente', + '1': '1 - Destinatário', + '2': '2 - Terceiros', + '9': '9 - Sem Frete'} + + self.oPDF_IO = IO() + if orientation == 'landscape': + raise NameError('Rotina não implementada') + else: + size = sizepage + + self.canvas = canvas.Canvas(self.oPDF_IO, pagesize=size) + self.canvas.setTitle('DANFE') + self.canvas.setStrokeColor(black) + + for oXML in list_xml: + oXML_cobr = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}cobr") + + self.NrPages = 1 + self.Page = 1 + + # Calculando total linhas usadas para descrições dos itens + # Com bloco fatura, apenas 29 linhas para itens na primeira folha + nNr_Lin_Pg_1 = 34 if oXML_cobr is None else 30 + # [ rec_ini , rec_fim , lines , limit_lines ] + oPaginator = [[0, 0, 0, nNr_Lin_Pg_1]] + el_det = oXML.findall(".//{http://www.portalfiscal.inf.br/nfe}det") + if el_det is not None: + list_desc = [] + list_cod_prod = [] + nPg = 0 + for nId, item in enumerate(el_det): + el_prod = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}prod") + infAdProd = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdProd") + + list_ = wrap(tagtext(oNode=el_prod, cTag='xProd'), 56) + if infAdProd is not None: + list_.extend(wrap(infAdProd.text, 56)) + list_desc.append(list_) + + list_cProd = wrap(tagtext(oNode=el_prod, cTag='cProd'), 14) + list_cod_prod.append(list_cProd) + + # Nr linhas necessárias p/ descrição item + nLin_Itens = len(list_) + + if (oPaginator[nPg][2] + nLin_Itens) >= oPaginator[nPg][3]: + oPaginator.append([0, 0, 0, 77]) + nPg += 1 + oPaginator[nPg][0] = nId + oPaginator[nPg][1] = nId + 1 + oPaginator[nPg][2] = nLin_Itens + else: + # adiciona-se 1 pelo funcionamento de xrange + oPaginator[nPg][1] = nId + 1 + oPaginator[nPg][2] += nLin_Itens + + self.NrPages = len(oPaginator) # Calculando nr. páginas + + if recibo: + self.recibo_entrega(oXML=oXML) + + self.ide_emit(oXML=oXML) + self.destinatario(oXML=oXML) + + if oXML_cobr is not None: + self.faturas(oXML=oXML_cobr) + + self.impostos(oXML=oXML) + self.transportes(oXML=oXML) + self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPaginator[0], + list_desc=list_desc, list_cod_prod=list_cod_prod) + + self.adicionais(oXML=oXML) + + # Gera o restante das páginas do XML + for oPag in oPaginator[1:]: + self.newpage() + self.ide_emit(oXML=oXML) + self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPag, + list_desc=list_desc, nHeight=77, + list_cod_prod=list_cod_prod) + + self.newpage() + + self.canvas.save() + + def ide_emit(self, oXML=None): + elem_infNFe = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}infNFe") + elem_protNFe = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}protNFe") + elem_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") + elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + + cChave = elem_infNFe.attrib.get('Id')[3:] + barcode128 = code128.Code128(cChave, barHeight=10*mm, barWidth=0.25*mm) + + self.canvas.setLineWidth(.5) + self.rect(self.nLeft, self.nlin+1, self.nLeft+75, 32) + self.rect(self.nLeft+115, self.nlin+1, + self.width-self.nLeft-self.nRight-115, 39) + + self.hline(self.nLeft+85, self.nlin+1, 125) + + self.rect(self.nLeft+116, self.nlin+15, + self.width-self.nLeft-self.nRight-117, 6) + + self.rect(self.nLeft, self.nlin+33, + self.width-self.nLeft-self.nRight, 14) + self.hline(self.nLeft, self.nlin+40, self.width-self.nRight) + self.vline(self.nLeft+60, self.nlin+40, 7) + self.vline(self.nLeft+100, self.nlin+40, 7) + + # Labels + self.canvas.setFont('NimbusSanL-Bold', 12) + self.stringcenter(self.nLeft+98, self.nlin+5, 'DANFE') + self.stringcenter(self.nLeft+109, self.nlin+19.5, + tagtext(oNode=elem_ide, cTag='tpNF')) + self.canvas.setFont('NimbusSanL-Bold', 8) + cNF = tagtext(oNode=elem_ide, cTag='nNF') + cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") + self.stringcenter(self.nLeft+100, self.nlin+25, "Nº %s" % (cNF)) + + self.stringcenter(self.nLeft+100, self.nlin+29, u"SÉRIE %s" % ( + tagtext(oNode=elem_ide, cTag='serie'))) + cPag = "Página %s de %s" % (str(self.Page), str(self.NrPages)) + self.stringcenter(self.nLeft+100, self.nlin+32, cPag) + self.canvas.setFont('NimbusSanL-Regu', 6) + 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+16, '0 - Entrada') + self.string(self.nLeft+86, self.nlin+19, '1 - Saída') + self.rect(self.nLeft+105, self.nlin+15, 8, 6) + + self.stringcenter( + self.nLeft+152, self.nlin+25, + 'Consulta de autenticidade no portal nacional da NF-e') + self.stringcenter( + self.nLeft+152, self.nlin+28, + 'www.nfe.fazenda.gov.br/portal ou no site da SEFAZ Autorizadora') + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+117, self.nlin+16.7, 'CHAVE DE ACESSO') + self.string(self.nLeft+116, self.nlin+2.7, 'CONTROLE DO FISCO') + + self.string(self.nLeft+1, self.nlin+34.7, 'NATUREZA DA OPERAÇÃO') + self.string(self.nLeft+116, self.nlin+34.7, + 'PROTOCOLO DE AUTORIZAÇÃO DE USO') + self.string(self.nLeft+1, self.nlin+41.7, 'INSCRIÇÃO ESTADUAL') + self.string(self.nLeft+61, self.nlin+41.7, + 'INSCRIÇÃO ESTADUAL DO SUBST. TRIB.') + self.string(self.nLeft+101, self.nlin+41.7, 'CNPJ') + + # Conteúdo campos + barcode128.drawOn(self.canvas, (self.nLeft+111.5)*mm, + (self.height-self.nlin-14)*mm) + self.canvas.setFont('NimbusSanL-Bold', 6) + nW_Rect = (self.width-self.nLeft-self.nRight-117) / 2 + self.stringcenter(self.nLeft+116.5+nW_Rect, self.nlin+19.5, + ' '.join(chunks(cChave, 4))) # Chave + self.canvas.setFont('NimbusSanL-Regu', 8) + cDt, cHr = getdateUTC(tagtext(oNode=elem_protNFe, cTag='dhRecbto')) + cProtocolo = tagtext(oNode=elem_protNFe, cTag='nProt') + cDt = cProtocolo + ' - ' + cDt + ' ' + cHr + nW_Rect = (self.width-self.nLeft-self.nRight-110) / 2 + self.stringcenter(self.nLeft+115+nW_Rect, self.nlin+38.7, cDt) + self.canvas.setFont('NimbusSanL-Regu', 8) + self.string(self.nLeft+1, self.nlin+38.7, + tagtext(oNode=elem_ide, cTag='natOp')) + self.string(self.nLeft+1, self.nlin+46, + tagtext(oNode=elem_emit, cTag='IE')) + self.string(self.nLeft+101, self.nlin+46, + format_cnpj_cpf(tagtext(oNode=elem_emit, cTag='CNPJ'))) + + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleN.fontSize = 10 + styleN.fontName = 'NimbusSanL-Bold' + styleN.alignment = TA_CENTER + + # Razão Social emitente + P = Paragraph(tagtext(oNode=elem_emit, cTag='xNome'), styleN) + w, h = P.wrap(55*mm, 50*mm) + P.drawOn(self.canvas, (self.nLeft+30)*mm, + (self.height-self.nlin-12)*mm) + + if self.logo: + img = get_image(self.logo, width=2*cm) + img.drawOn(self.canvas, (self.nLeft+5)*mm, + (self.height-self.nlin-22)*mm) + + cEnd = tagtext(oNode=elem_emit, cTag='xLgr') + ', ' + tagtext( + oNode=elem_emit, cTag='nro') + ' - ' + cEnd += tagtext(oNode=elem_emit, cTag='xBairro') + '
' + tagtext( + oNode=elem_emit, cTag='xMun') + ' - ' + cEnd += 'Fone: ' + tagtext(oNode=elem_emit, cTag='fone') + '
' + cEnd += tagtext(oNode=elem_emit, cTag='UF') + ' - ' + tagtext( + oNode=elem_emit, cTag='CEP') + + regime = tagtext(oNode=elem_emit, cTag='CRT') + cEnd += u'
Regime Tributário: %s' % (REGIME_TRIBUTACAO[regime]) + + styleN.fontName = 'NimbusSanL-Regu' + styleN.fontSize = 7 + styleN.leading = 10 + P = Paragraph(cEnd, styleN) + w, h = P.wrap(55*mm, 30*mm) + P.drawOn(self.canvas, (self.nLeft+30)*mm, + (self.height-self.nlin-31)*mm) + + # Homologação + if tagtext(oNode=elem_ide, cTag='tpAmb') == '2': + self.canvas.saveState() + self.canvas.rotate(90) + self.canvas.setFont('Times-Bold', 40) + self.canvas.setFillColorRGB(0.57, 0.57, 0.57) + self.string(self.nLeft+65, 449, 'SEM VALOR FISCAL') + self.canvas.restoreState() + + self.nlin += 48 + + def destinatario(self, oXML=None): + elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + elem_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") + nMr = self.width-self.nRight + + self.nlin += 1 + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'DESTINATÁRIO/REMETENTE') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 20) + self.vline(nMr-25, self.nlin+2, 20) + self.hline(self.nLeft, self.nlin+8.66, self.width-self.nLeft) + self.hline(self.nLeft, self.nlin+15.32, self.width-self.nLeft) + self.vline(nMr-70, self.nlin+2, 6.66) + self.vline(nMr-53, self.nlin+8.66, 6.66) + self.vline(nMr-99, self.nlin+8.66, 6.66) + self.vline(nMr-90, self.nlin+15.32, 6.66) + self.vline(nMr-102, self.nlin+15.32, 6.66) + self.vline(nMr-136, self.nlin+15.32, 6.66) + # Labels/Fields + self.canvas.setFont('NimbusSanL-Bold', 5) + self.string(self.nLeft+1, self.nlin+3.7, 'NOME/RAZÃO SOCIAL') + self.string(nMr-69, self.nlin+3.7, 'CNPJ/CPF') + self.string(nMr-24, self.nlin+3.7, 'DATA DA EMISSÃO') + self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') + self.string(nMr-98, self.nlin+10.3, 'BAIRRO/DISTRITO') + self.string(nMr-52, self.nlin+10.3, 'CEP') + self.string(nMr-24, self.nlin+10.3, 'DATA DE ENTRADA/SAÍDA') + self.string(self.nLeft+1, self.nlin+17.1, 'MUNICÍPIO') + self.string(nMr-135, self.nlin+17.1, 'FONE/FAX') + self.string(nMr-101, self.nlin+17.1, 'UF') + self.string(nMr-89, self.nlin+17.1, 'INSCRIÇÃO ESTADUAL') + self.string(nMr-24, self.nlin+17.1, 'HORA DE ENTRADA/SAÍDA') + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 8) + self.string(self.nLeft+1, self.nlin+7.5, + tagtext(oNode=elem_dest, cTag='xNome')) + cnpj_cpf = format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CNPJ')) + if cnpj_cpf == '..-' or not cnpj_cpf: + 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')) + self.string(nMr-24, self.nlin+7.7, cDt + ' ' + cHr) + cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhSaiEnt')) + self.string(nMr-24, self.nlin+14.3, cDt + ' ' + cHr) # Dt saída + cEnd = tagtext(oNode=elem_dest, cTag='xLgr') + ', ' + tagtext( + oNode=elem_dest, cTag='nro') + self.string(self.nLeft+1, self.nlin+14.3, cEnd) + self.string(nMr-98, self.nlin+14.3, + tagtext(oNode=elem_dest, cTag='xBairro')) + self.string(nMr-52, self.nlin+14.3, + tagtext(oNode=elem_dest, cTag='CEP')) + self.string(self.nLeft+1, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='xMun')) + self.string(nMr-135, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='fone')) + self.string(nMr-101, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='UF')) + self.string(nMr-89, self.nlin+21.1, + tagtext(oNode=elem_dest, cTag='IE')) + + self.nlin += 24 # Nr linhas ocupadas pelo bloco + + def faturas(self, oXML=None): + + nMr = self.width-self.nRight + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'FATURA') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 13) + self.vline(nMr-47.5, self.nlin+2, 13) + self.vline(nMr-95, self.nlin+2, 13) + self.vline(nMr-142.5, self.nlin+2, 13) + self.hline(nMr-47.5, self.nlin+8.5, self.width-self.nLeft) + # Labels + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(nMr-46.5, self.nlin+3.8, 'CÓDIGO VENDEDOR') + self.string(nMr-46.5, self.nlin+10.2, 'NOME VENDEDOR') + self.string(nMr-93.5, self.nlin+3.8, + 'FATURA VENCIMENTO VALOR') + self.string(nMr-140.5, self.nlin+3.8, + 'FATURA VENCIMENTO VALOR') + self.string(self.nLeft+2, self.nlin+3.8, + 'FATURA VENCIMENTO VALOR') + + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Bold', 6) + nLin = 7 + nPar = 1 + nCol = 0 + nAju = 0 + + line_iter = iter(oXML[1:10]) # Salta elemt 1 e considera os próximos 9 + for oXML_dup in line_iter: + + cDt, cHr = getdateUTC(tagtext(oNode=oXML_dup, cTag='dVenc')) + self.string(self.nLeft+nCol+1, self.nlin+nLin, + tagtext(oNode=oXML_dup, cTag='nDup')) + self.string(self.nLeft+nCol+17, self.nlin+nLin, cDt) + self.stringRight( + self.nLeft+nCol+47, self.nlin+nLin, + format_number(tagtext(oNode=oXML_dup, cTag='vDup'), + precision=2)) + + if nPar == 3: + nLin = 7 + nPar = 1 + nCol += 47 + nAju += 1 + nCol += nAju * (0.3) + else: + nLin += 3.3 + nPar += 1 + + # Campos adicionais XML - Condicionados a existencia de financeiro + elem_infAdic = oXML.getparent().find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdic") + if elem_infAdic is not None: + codvend = elem_infAdic.find( + ".//{http://www.portalfiscal.inf.br/nfe}obsCont\ +[@xCampo='CodVendedor']") + self.string(nMr-46.5, self.nlin+7.7, + tagtext(oNode=codvend, cTag='xTexto')) + vend = elem_infAdic.find(".//{http://www.portalfiscal.inf.br/nfe}\ +obsCont[@xCampo='NomeVendedor']") + self.string(nMr-46.5, self.nlin+14.3, + tagtext(oNode=vend, cTag='xTexto')[:36]) + + self.nlin += 16 # Nr linhas ocupadas pelo bloco + + def impostos(self, oXML=None): + # Impostos + el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") + nMr = self.width-self.nRight + self.nlin += 1 + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'CÁLCULO DO IMPOSTO') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 13) + self.hline(self.nLeft, self.nlin+8.5, self.width-self.nLeft) + self.vline(nMr-35, self.nlin+2, 6.5) + self.vline(nMr-65, self.nlin+2, 6.5) + self.vline(nMr-95, self.nlin+2, 6.5) + self.vline(nMr-125, self.nlin+2, 6.5) + self.vline(nMr-155, self.nlin+2, 6.5) + self.vline(nMr-35, self.nlin+8.5, 6.5) + self.vline(nMr-65, self.nlin+8.5, 6.5) + self.vline(nMr-95, self.nlin+8.5, 6.5) + self.vline(nMr-125, self.nlin+8.5, 6.5) + self.vline(nMr-155, self.nlin+8.5, 6.5) + # Labels + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+1, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS') + self.string(nMr-154, self.nlin+3.8, 'VALOR DO ICMS') + self.string(nMr-124, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS ST') + self.string(nMr-94, self.nlin+3.8, 'VALOR DO ICMS ST') + self.string(nMr-64, self.nlin+3.8, 'VALOR APROX TRIBUTOS') + self.string(nMr-34, self.nlin+3.8, 'VALOR TOTAL DOS PRODUTOS') + + self.string(self.nLeft+1, self.nlin+10.2, 'VALOR DO FRETE') + self.string(nMr-154, self.nlin+10.2, 'VALOR DO SEGURO') + self.string(nMr-124, self.nlin+10.2, 'DESCONTO') + self.string(nMr-94, self.nlin+10.2, 'OUTRAS DESP. ACESSÓRIAS') + self.string(nMr-64, self.nlin+10.2, 'VALOR DO IPI') + self.string(nMr-34, self.nlin+10.2, 'VALOR TOTAL DA NOTA') + + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 8) + self.stringRight( + self.nLeft+34, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vBC'), precision=2)) + self.stringRight( + self.nLeft+64, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vICMS'), precision=2)) + self.stringRight( + self.nLeft+94, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vBCST'), precision=2)) + self.stringRight( + nMr-66, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vST'), precision=2)) + self.stringRight( + nMr-36, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vTotTrib'), + precision=2)) + self.stringRight( + nMr-1, self.nlin+7.7, + format_number(tagtext(oNode=el_total, cTag='vProd'), precision=2)) + self.stringRight( + self.nLeft+34, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vFrete'), precision=2)) + self.stringRight( + self.nLeft+64, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vSeg'), precision=2)) + self.stringRight( + self.nLeft+94, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vDesc'), precision=2)) + self.stringRight( + self.nLeft+124, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vOutro'), precision=2)) + self.stringRight( + self.nLeft+154, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vIPI'), precision=2)) + self.stringRight( + nMr-1, self.nlin+14.1, + format_number(tagtext(oNode=el_total, cTag='vNF'), precision=2)) + + self.nlin += 17 # Nr linhas ocupadas pelo bloco + + def transportes(self, oXML=None): + el_transp = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}transp") + nMr = self.width-self.nRight + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, + 'TRANSPORTADOR/VOLUMES TRANSPORTADOS') + self.canvas.setFont('NimbusSanL-Regu', 5) + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 20) + self.hline(self.nLeft, self.nlin+8.6, self.width-self.nLeft) + self.hline(self.nLeft, self.nlin+15.2, self.width-self.nLeft) + self.vline(nMr-40, self.nlin+2, 13.2) + self.vline(nMr-49, self.nlin+2, 20) + self.vline(nMr-92, self.nlin+2, 6.6) + self.vline(nMr-120, self.nlin+2, 6.6) + self.vline(nMr-75, self.nlin+2, 6.6) + self.vline(nMr-26, self.nlin+15.2, 6.6) + self.vline(nMr-102, self.nlin+8.6, 6.6) + self.vline(nMr-85, self.nlin+15.2, 6.6) + self.vline(nMr-121, self.nlin+15.2, 6.6) + self.vline(nMr-160, self.nlin+15.2, 6.6) + # Labels/Fields + self.string(nMr-39, self.nlin+3.8, 'CNPJ/CPF') + self.string(nMr-74, self.nlin+3.8, 'PLACA DO VEÍCULO') + self.string(nMr-91, self.nlin+3.8, 'CÓDIGO ANTT') + self.string(nMr-119, self.nlin+3.8, 'FRETE POR CONTA') + self.string(self.nLeft+1, self.nlin+3.8, 'RAZÃO SOCIAL') + self.string(nMr-48, self.nlin+3.8, 'UF') + self.string(nMr-39, self.nlin+10.3, 'INSCRIÇÃO ESTADUAL') + self.string(nMr-48, self.nlin+10.3, 'UF') + self.string(nMr-101, self.nlin+10.3, 'MUNICÍPIO') + self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') + self.string(nMr-48, self.nlin+17, 'PESO BRUTO') + self.string(nMr-25, self.nlin+17, 'PESO LÍQUIDO') + self.string(nMr-84, self.nlin+17, 'NUMERAÇÃO') + self.string(nMr-120, self.nlin+17, 'MARCA') + self.string(nMr-159, self.nlin+17, 'ESPÉCIE') + self.string(self.nLeft+1, self.nlin+17, 'QUANTIDADE') + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 8) + self.string(self.nLeft+1, self.nlin+7.7, + tagtext(oNode=el_transp, cTag='xNome')[:40]) + self.string(self.nLeft+71, self.nlin+7.7, + self.oFrete[tagtext(oNode=el_transp, cTag='modFrete')]) + self.string(nMr-39, self.nlin+7.7, + format_cnpj_cpf(tagtext(oNode=el_transp, cTag='CNPJ'))) + self.string(self.nLeft+1, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='xEnder')[:45]) + self.string(self.nLeft+89, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='xMun')) + self.string(nMr-48, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='UF')) + self.string(nMr-39, self.nlin+14.2, + tagtext(oNode=el_transp, cTag='IE')) + self.string(self.nLeft+1, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='qVol')) + self.string(self.nLeft+31, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='esp')) + self.string(self.nLeft+70, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='marca')) + self.string(self.nLeft+106, self.nlin+21.2, + tagtext(oNode=el_transp, cTag='nVol')) + self.stringRight( + nMr-27, self.nlin+21.2, + format_number(tagtext(oNode=el_transp, cTag='pesoB'), precision=3)) + self.stringRight( + nMr-1, self.nlin+21.2, + format_number(tagtext(oNode=el_transp, cTag='pesoL'), precision=3)) + + self.nlin += 23 + + def produtos(self, oXML=None, el_det=None, oPaginator=None, + list_desc=None, list_cod_prod=None, nHeight=29): + + nMr = self.width-self.nRight + nStep = 2.5 # Passo entre linhas + nH = 7.5 + (nHeight * nStep) # cabeçalho 7.5 + self.nlin += 1 + + self.canvas.setFont('NimbusSanL-Bold', 7) + self.string(self.nLeft+1, self.nlin+1, 'DADOS DO PRODUTO/SERVIÇO') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, nH) + self.hline(self.nLeft, self.nlin+8, self.width-self.nLeft) + + self.canvas.setFont('NimbusSanL-Regu', 5.5) + # Colunas + self.vline(self.nLeft+15, self.nlin+2, nH) + self.stringcenter(self.nLeft+7.5, self.nlin+5.5, 'CÓDIGO') + self.vline(nMr-7, self.nlin+2, nH) + self.stringcenter(nMr-3.5, self.nlin+4.5, 'ALÍQ') + self.stringcenter(nMr-3.5, self.nlin+6.5, 'IPI') + self.vline(nMr-14, self.nlin+2, nH) + self.stringcenter(nMr-10.5, self.nlin+4.5, 'ALÍQ') + self.stringcenter(nMr-10.5, self.nlin+6.5, 'ICMS') + self.vline(nMr-26, self.nlin+2, nH) + self.stringcenter(nMr-20, self.nlin+5.5, 'VLR. IPI') + self.vline(nMr-38, self.nlin+2, nH) + self.stringcenter(nMr-32, self.nlin+5.5, 'VLR. ICMS') + self.vline(nMr-50, self.nlin+2, nH) + self.stringcenter(nMr-44, self.nlin+5.5, 'BC ICMS') + self.vline(nMr-64, self.nlin+2, nH) + self.stringcenter(nMr-57, self.nlin+5.5, 'VLR TOTAL') + self.vline(nMr-77, self.nlin+2, nH) + self.stringcenter(nMr-70.5, self.nlin+5.5, 'VLR UNIT') + self.vline(nMr-90, self.nlin+2, nH) + self.stringcenter(nMr-83.5, self.nlin+5.5, 'QTD') + self.vline(nMr-96, self.nlin+2, nH) + self.stringcenter(nMr-93, self.nlin+5.5, 'UNID') + self.vline(nMr-102, self.nlin+2, nH) + self.stringcenter(nMr-99, self.nlin+5.5, 'CFOP') + self.vline(nMr-108, self.nlin+2, nH) + self.stringcenter(nMr-105, self.nlin+5.5, 'CST') + self.vline(nMr-117, self.nlin+2, nH) + self.stringcenter(nMr-112.5, self.nlin+5.5, 'NCM/SH') + + nWidth_Prod = nMr-135-self.nLeft-11 + nCol_ = self.nLeft+20 + (nWidth_Prod / 2) + self.stringcenter(nCol_, self.nlin+5.5, 'DESCRIÇÃO DO PRODUTO/SERVIÇO') + + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Regu', 5) + nLin = self.nlin+10.5 + + for id in xrange(oPaginator[0], oPaginator[1]): + item = el_det[id] + el_prod = item.find(".//{http://www.portalfiscal.inf.br/nfe}prod") + el_imp = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}imposto") + + el_imp_ICMS = el_imp.find( + ".//{http://www.portalfiscal.inf.br/nfe}ICMS") + el_imp_IPI = el_imp.find( + ".//{http://www.portalfiscal.inf.br/nfe}IPI") + cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \ + tagtext(oNode=el_imp_ICMS, cTag='CSOSN') + vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC') + vICMS = tagtext(oNode=el_imp_ICMS, cTag='vICMS') + pICMS = tagtext(oNode=el_imp_ICMS, cTag='pICMS') + + vIPI = tagtext(oNode=el_imp_IPI, cTag='vIPI') + pIPI = tagtext(oNode=el_imp_IPI, cTag='pIPI') + + self.stringcenter(nMr-112.5, nLin, + tagtext(oNode=el_prod, cTag='NCM')) + self.stringcenter(nMr-105, nLin, cCST) + self.stringcenter(nMr-99, nLin, + tagtext(oNode=el_prod, cTag='CFOP')) + self.stringcenter(nMr-93, nLin, + 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-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)) + + if vIPI: + self.stringRight(nMr-14.5, nLin, + format_number(vIPI, precision=2)) + if pIPI: + self.stringRight(nMr-0.5, nLin, + format_number(pIPI, precision=2)) + + # Código Item + line_cod = nLin + for des in list_cod_prod[id]: + self.string(self.nLeft+0.2, line_cod, des) + line_cod += nStep + + # Descrição Item + line_desc = nLin + for des in list_desc[id]: + self.string(self.nLeft+15.5, line_desc, des) + line_desc += nStep + + nLin = max(line_cod, line_desc) + self.canvas.setStrokeColor(gray) + self.hline(self.nLeft, nLin-2, self.width-self.nLeft) + self.canvas.setStrokeColor(black) + + self.nlin += nH + 3 + + def adicionais(self, oXML=None): + el_infAdic = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdic") + + self.nlin += 2 + self.canvas.setFont('NimbusSanL-Bold', 6) + self.string(self.nLeft+1, self.nlin+1, 'DADOS ADICIONAIS') + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+1, self.nlin+4, 'INFORMAÇÕES COMPLEMENTARES') + self.string((self.width/2)+1, self.nlin+4, 'RESERVADO AO FISCO') + self.rect(self.nLeft, self.nlin+2, + self.width-self.nLeft-self.nRight, 42) + self.vline(self.width/2, self.nlin+2, 42) + # Conteúdo campos + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleN.fontSize = 6 + styleN.fontName = 'NimbusSanL-Regu' + styleN.leading = 7 + fisco = tagtext(oNode=el_infAdic, cTag='infAdFisco') + observacoes = tagtext(oNode=el_infAdic, cTag='infCpl') + if fisco: + observacoes = fisco + ' ' + observacoes + P = Paragraph(observacoes, styles['Normal']) + w, h = P.wrap(92*mm, 32*mm) + altura = (self.height-self.nlin-5)*mm + P.drawOn(self.canvas, (self.nLeft+1)*mm, altura - h) + self.nlin += 36 + + def recibo_entrega(self, oXML=None): + el_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + el_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") + el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") + el_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") + + # self.nlin = self.height-self.nBottom-18 # 17 altura recibo + nW = 40 + nH = 17 + self.canvas.setLineWidth(.5) + self.rect(self.nLeft, self.nlin, + self.width-(self.nLeft+self.nRight), nH) + self.hline(self.nLeft, self.nlin+8.5, self.width-self.nRight-nW) + self.vline(self.width-self.nRight-nW, self.nlin, nH) + self.vline(self.nLeft+nW, self.nlin+8.5, 8.5) + + # Labels + self.canvas.setFont('NimbusSanL-Regu', 5) + self.string(self.nLeft+1, self.nlin+10.2, 'DATA DE RECEBIMENTO') + self.string(self.nLeft+41, self.nlin+10.2, + 'IDENTIFICAÇÃO E ASSINATURA DO RECEBEDOR') + self.stringcenter(self.width-self.nRight-(nW/2), self.nlin+2, 'NF-e') + # Conteúdo campos + self.canvas.setFont('NimbusSanL-Bold', 8) + cNF = tagtext(oNode=el_ide, cTag='nNF') + cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") + self.string(self.width-self.nRight-nW+2, self.nlin+8, "Nº %s" % (cNF)) + self.string(self.width-self.nRight-nW+2, self.nlin+14, + u"SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie'))) + + cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi')) + cTotal = format_number(tagtext(oNode=el_total, cTag='vNF'), + precision=2) + + cEnd = tagtext(oNode=el_dest, cTag='xNome') + ' - ' + cEnd += tagtext(oNode=el_dest, cTag='xLgr') + ', ' + tagtext( + oNode=el_dest, cTag='nro') + ', ' + cEnd += tagtext(oNode=el_dest, cTag='xBairro') + ', ' + tagtext( + oNode=el_dest, cTag='xMun') + ' - ' + cEnd += tagtext(oNode=el_dest, cTag='UF') + + cString = u""" + RECEBEMOS DE %s OS PRODUTOS/SERVIÇOS CONSTANTES DA NOTA FISCAL INDICADA + ABAIXO. EMISSÃO: %s VALOR TOTAL: %s + DESTINATARIO: %s""" % (tagtext(oNode=el_emit, cTag='xNome'), + cDt, cTotal, cEnd) + + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleN.fontName = 'NimbusSanL-Regu' + styleN.fontSize = 6 + styleN.leading = 7 + + P = Paragraph(cString, styleN) + w, h = P.wrap(149*mm, 7*mm) + P.drawOn(self.canvas, (self.nLeft+1)*mm, + ((self.height-self.nlin)*mm) - h) + + self.nlin += 20 + self.hline(self.nLeft, self.nlin, self.width-self.nRight) + self.nlin += 2 + + def newpage(self): + self.nlin = self.nTop + self.Page += 1 + self.canvas.showPage() + + def hline(self, x, y, width): + y = self.height - y + self.canvas.line(x*mm, y*mm, width*mm, y*mm) + + def vline(self, x, y, width): + width = self.height - y - width + y = self.height - y + self.canvas.line(x*mm, y*mm, x*mm, width*mm) + + def rect(self, col, lin, nWidth, nHeight, fill=False): + lin = self.height - nHeight - lin + self.canvas.rect(col*mm, lin*mm, nWidth*mm, nHeight*mm, + stroke=True, fill=fill) + + def string(self, x, y, value): + y = self.height - y + self.canvas.drawString(x*mm, y*mm, value) + + def stringRight(self, x, y, value): + y = self.height - y + self.canvas.drawRightString(x*mm, y*mm, value) + + def stringcenter(self, x, y, value): + y = self.height - y + self.canvas.drawCentredString(x*mm, y*mm, value) + + def writeto_pdf(self, fileObj): + pdf_out = self.oPDF_IO.getvalue() + self.oPDF_IO.close() + fileObj.write(pdf_out) diff --git a/setup.py b/setup.py index ff137cf6..a4bf038d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.9.3" +VERSION = "0.9.4" setup( name="PyTrustNFe3", From 4915472aa7b1347f16c9f72d0269e970c9e5de31 Mon Sep 17 00:00:00 2001 From: carcaroff Date: Fri, 22 Dec 2017 15:49:52 -0200 Subject: [PATCH 22/31] [FIX] --- pytrustnfe/nfe/danfe.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/pytrustnfe/nfe/danfe.py b/pytrustnfe/nfe/danfe.py index bfe96926..1aeca9bd 100644 --- a/pytrustnfe/nfe/danfe.py +++ b/pytrustnfe/nfe/danfe.py @@ -3,8 +3,8 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # Classe para geração de PDF da DANFE a partir de xml etree.fromstring - -from cStringIO import StringIO as IO +import os +from io import BytesIO from textwrap import wrap from reportlab.lib import utils @@ -16,6 +16,8 @@ from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.enums import TA_CENTER from reportlab.platypus import Paragraph, Image +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont def chunks(cString, nLen): @@ -55,7 +57,6 @@ def tagtext(oNode=None, cTag=None): cText = '' return cText - REGIME_TRIBUTACAO = { '1': 'Simples Nacional', '2': 'Simples Nacional, excesso sublimite de receita bruta', @@ -73,6 +74,14 @@ def get_image(path, width=1*cm): class danfe(object): def __init__(self, sizepage=A4, list_xml=None, recibo=True, orientation='portrait', logo=None): + + path = os.path.join(os.path.dirname(__file__), 'fonts') + pdfmetrics.registerFont( + TTFont('NimbusSanL-Regu', + os.path.join(path, 'NimbusSanL Regular.ttf'))) + pdfmetrics.registerFont( + TTFont('NimbusSanL-Bold', + os.path.join(path, 'NimbusSanL Bold.ttf'))) self.width = 210 # 21 x 29,7cm self.height = 297 self.nLeft = 10 @@ -86,7 +95,7 @@ def __init__(self, sizepage=A4, list_xml=None, recibo=True, '2': '2 - Terceiros', '9': '9 - Sem Frete'} - self.oPDF_IO = IO() + self.oPDF_IO = BytesIO() if orientation == 'landscape': raise NameError('Rotina não implementada') else: @@ -208,7 +217,7 @@ def ide_emit(self, oXML=None): cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") self.stringcenter(self.nLeft+100, self.nlin+25, "Nº %s" % (cNF)) - self.stringcenter(self.nLeft+100, self.nlin+29, u"SÉRIE %s" % ( + self.stringcenter(self.nLeft+100, self.nlin+29, "SÉRIE %s" % ( tagtext(oNode=elem_ide, cTag='serie'))) cPag = "Página %s de %s" % (str(self.Page), str(self.NrPages)) self.stringcenter(self.nLeft+100, self.nlin+32, cPag) @@ -284,7 +293,7 @@ def ide_emit(self, oXML=None): oNode=elem_emit, cTag='CEP') regime = tagtext(oNode=elem_emit, cTag='CRT') - cEnd += u'
Regime Tributário: %s' % (REGIME_TRIBUTACAO[regime]) + cEnd += '
Regime Tributário: %s' % (REGIME_TRIBUTACAO[regime]) styleN.fontName = 'NimbusSanL-Regu' styleN.fontSize = 7 @@ -640,7 +649,7 @@ def produtos(self, oXML=None, el_det=None, oPaginator=None, self.canvas.setFont('NimbusSanL-Regu', 5) nLin = self.nlin+10.5 - for id in xrange(oPaginator[0], oPaginator[1]): + for id in range(oPaginator[0], oPaginator[1]): item = el_det[id] el_prod = item.find(".//{http://www.portalfiscal.inf.br/nfe}prod") el_imp = item.find( @@ -650,6 +659,7 @@ def produtos(self, oXML=None, el_det=None, oPaginator=None, ".//{http://www.portalfiscal.inf.br/nfe}ICMS") el_imp_IPI = el_imp.find( ".//{http://www.portalfiscal.inf.br/nfe}IPI") + cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \ tagtext(oNode=el_imp_ICMS, cTag='CSOSN') vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC') @@ -721,6 +731,7 @@ def adicionais(self, oXML=None): styleN.fontSize = 6 styleN.fontName = 'NimbusSanL-Regu' styleN.leading = 7 + fisco = tagtext(oNode=el_infAdic, cTag='infAdFisco') observacoes = tagtext(oNode=el_infAdic, cTag='infCpl') if fisco: @@ -759,7 +770,7 @@ def recibo_entrega(self, oXML=None): cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") self.string(self.width-self.nRight-nW+2, self.nlin+8, "Nº %s" % (cNF)) self.string(self.width-self.nRight-nW+2, self.nlin+14, - u"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')) cTotal = format_number(tagtext(oNode=el_total, cTag='vNF'), @@ -772,7 +783,7 @@ def recibo_entrega(self, oXML=None): oNode=el_dest, cTag='xMun') + ' - ' cEnd += tagtext(oNode=el_dest, cTag='UF') - cString = u""" + cString = """ RECEBEMOS DE %s OS PRODUTOS/SERVIÇOS CONSTANTES DA NOTA FISCAL INDICADA ABAIXO. EMISSÃO: %s VALOR TOTAL: %s DESTINATARIO: %s""" % (tagtext(oNode=el_emit, cTag='xNome'), From f42937ae5f2213e574483b523cfdf9c777d86158 Mon Sep 17 00:00:00 2001 From: carcaroff Date: Tue, 26 Dec 2017 11:58:10 -0200 Subject: [PATCH 23/31] [FIX] --- pytrustnfe/nfe/danfe.py | 563 ++++++++++++++++++++-------------------- 1 file changed, 288 insertions(+), 275 deletions(-) diff --git a/pytrustnfe/nfe/danfe.py b/pytrustnfe/nfe/danfe.py index 1aeca9bd..7cb73034 100644 --- a/pytrustnfe/nfe/danfe.py +++ b/pytrustnfe/nfe/danfe.py @@ -22,7 +22,7 @@ def chunks(cString, nLen): for start in range(0, len(cString), nLen): - yield cString[start:start+nLen] + yield cString[start:start + nLen] def format_cnpj_cpf(value): @@ -64,7 +64,7 @@ def tagtext(oNode=None, cTag=None): } -def get_image(path, width=1*cm): +def get_image(path, width=1 * cm): img = utils.ImageReader(path) iw, ih = img.getSize() aspect = ih / float(iw) @@ -72,6 +72,7 @@ def get_image(path, width=1*cm): class danfe(object): + def __init__(self, sizepage=A4, list_xml=None, recibo=True, orientation='portrait', logo=None): @@ -189,82 +190,84 @@ def ide_emit(self, oXML=None): elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") 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.rect(self.nLeft, self.nlin+1, self.nLeft+75, 32) - self.rect(self.nLeft+115, self.nlin+1, - self.width-self.nLeft-self.nRight-115, 39) + self.rect(self.nLeft, self.nlin + 1, self.nLeft + 75, 32) + self.rect(self.nLeft + 115, self.nlin + 1, + self.width - self.nLeft - self.nRight - 115, 39) - self.hline(self.nLeft+85, self.nlin+1, 125) + self.hline(self.nLeft + 85, self.nlin + 1, 125) - self.rect(self.nLeft+116, self.nlin+15, - self.width-self.nLeft-self.nRight-117, 6) + self.rect(self.nLeft + 116, self.nlin + 15, + self.width - self.nLeft - self.nRight - 117, 6) - self.rect(self.nLeft, self.nlin+33, - self.width-self.nLeft-self.nRight, 14) - self.hline(self.nLeft, self.nlin+40, self.width-self.nRight) - self.vline(self.nLeft+60, self.nlin+40, 7) - self.vline(self.nLeft+100, self.nlin+40, 7) + self.rect(self.nLeft, self.nlin + 33, + self.width - self.nLeft - self.nRight, 14) + self.hline(self.nLeft, self.nlin + 40, self.width - self.nRight) + self.vline(self.nLeft + 60, self.nlin + 40, 7) + self.vline(self.nLeft + 100, self.nlin + 40, 7) # Labels self.canvas.setFont('NimbusSanL-Bold', 12) - self.stringcenter(self.nLeft+98, self.nlin+5, 'DANFE') - self.stringcenter(self.nLeft+109, self.nlin+19.5, + self.stringcenter(self.nLeft + 98, self.nlin + 5, 'DANFE') + self.stringcenter(self.nLeft + 109, self.nlin + 19.5, tagtext(oNode=elem_ide, cTag='tpNF')) self.canvas.setFont('NimbusSanL-Bold', 8) cNF = tagtext(oNode=elem_ide, cTag='nNF') cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") - self.stringcenter(self.nLeft+100, self.nlin+25, "Nº %s" % (cNF)) + self.stringcenter(self.nLeft + 100, self.nlin + 25, "Nº %s" % (cNF)) - self.stringcenter(self.nLeft+100, self.nlin+29, "SÉRIE %s" % ( + self.stringcenter(self.nLeft + 100, self.nlin + 29, "SÉRIE %s" % ( tagtext(oNode=elem_ide, cTag='serie'))) cPag = "Página %s de %s" % (str(self.Page), str(self.NrPages)) - 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.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+16, '0 - Entrada') - self.string(self.nLeft+86, self.nlin+19, '1 - Saída') - self.rect(self.nLeft+105, self.nlin+15, 8, 6) + 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 + 16, '0 - Entrada') + self.string(self.nLeft + 86, self.nlin + 19, '1 - Saída') + self.rect(self.nLeft + 105, self.nlin + 15, 8, 6) self.stringcenter( - self.nLeft+152, self.nlin+25, + self.nLeft + 152, self.nlin + 25, 'Consulta de autenticidade no portal nacional da NF-e') self.stringcenter( - self.nLeft+152, self.nlin+28, + self.nLeft + 152, self.nlin + 28, 'www.nfe.fazenda.gov.br/portal ou no site da SEFAZ Autorizadora') self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+117, self.nlin+16.7, 'CHAVE DE ACESSO') - self.string(self.nLeft+116, self.nlin+2.7, 'CONTROLE DO FISCO') + self.string(self.nLeft + 117, self.nlin + 16.7, 'CHAVE DE ACESSO') + self.string(self.nLeft + 116, self.nlin + 2.7, 'CONTROLE DO FISCO') - self.string(self.nLeft+1, self.nlin+34.7, 'NATUREZA DA OPERAÇÃO') - self.string(self.nLeft+116, self.nlin+34.7, + self.string(self.nLeft + 1, self.nlin + 34.7, 'NATUREZA DA OPERAÇÃO') + self.string(self.nLeft + 116, self.nlin + 34.7, 'PROTOCOLO DE AUTORIZAÇÃO DE USO') - self.string(self.nLeft+1, self.nlin+41.7, 'INSCRIÇÃO ESTADUAL') - self.string(self.nLeft+61, self.nlin+41.7, + self.string(self.nLeft + 1, self.nlin + 41.7, 'INSCRIÇÃO ESTADUAL') + self.string(self.nLeft + 61, self.nlin + 41.7, 'INSCRIÇÃO ESTADUAL DO SUBST. TRIB.') - self.string(self.nLeft+101, self.nlin+41.7, 'CNPJ') + self.string(self.nLeft + 101, self.nlin + 41.7, 'CNPJ') # Conteúdo campos - barcode128.drawOn(self.canvas, (self.nLeft+111.5)*mm, - (self.height-self.nlin-14)*mm) + barcode128.drawOn(self.canvas, (self.nLeft + 111.5) * mm, + (self.height - self.nlin - 14) * mm) self.canvas.setFont('NimbusSanL-Bold', 6) - nW_Rect = (self.width-self.nLeft-self.nRight-117) / 2 - self.stringcenter(self.nLeft+116.5+nW_Rect, self.nlin+19.5, + nW_Rect = (self.width - self.nLeft - self.nRight - 117) / 2 + self.stringcenter(self.nLeft + 116.5 + nW_Rect, self.nlin + 19.5, ' '.join(chunks(cChave, 4))) # Chave self.canvas.setFont('NimbusSanL-Regu', 8) cDt, cHr = getdateUTC(tagtext(oNode=elem_protNFe, cTag='dhRecbto')) cProtocolo = tagtext(oNode=elem_protNFe, cTag='nProt') cDt = cProtocolo + ' - ' + cDt + ' ' + cHr - nW_Rect = (self.width-self.nLeft-self.nRight-110) / 2 - self.stringcenter(self.nLeft+115+nW_Rect, self.nlin+38.7, cDt) + nW_Rect = (self.width - self.nLeft - self.nRight - 110) / 2 + self.stringcenter(self.nLeft + 115 + nW_Rect, self.nlin + 38.7, cDt) self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+38.7, + self.string(self.nLeft + 1, self.nlin + 38.7, tagtext(oNode=elem_ide, cTag='natOp')) - self.string(self.nLeft+1, self.nlin+46, + self.string(self.nLeft + 1, self.nlin + 46, tagtext(oNode=elem_emit, cTag='IE')) - self.string(self.nLeft+101, self.nlin+46, + self.string(self.nLeft + 101, self.nlin + 46, format_cnpj_cpf(tagtext(oNode=elem_emit, cTag='CNPJ'))) styles = getSampleStyleSheet() @@ -275,14 +278,14 @@ def ide_emit(self, oXML=None): # Razão Social emitente P = Paragraph(tagtext(oNode=elem_emit, cTag='xNome'), styleN) - w, h = P.wrap(55*mm, 50*mm) - P.drawOn(self.canvas, (self.nLeft+30)*mm, - (self.height-self.nlin-12)*mm) + w, h = P.wrap(55 * mm, 50 * mm) + P.drawOn(self.canvas, (self.nLeft + 30) * mm, + (self.height - self.nlin - 12) * mm) if self.logo: - img = get_image(self.logo, width=2*cm) - img.drawOn(self.canvas, (self.nLeft+5)*mm, - (self.height-self.nlin-22)*mm) + img = get_image(self.logo, width=2 * cm) + img.drawOn(self.canvas, (self.nLeft + 5) * mm, + (self.height - self.nlin - 22) * mm) cEnd = tagtext(oNode=elem_emit, cTag='xLgr') + ', ' + tagtext( oNode=elem_emit, cTag='nro') + ' - ' @@ -299,9 +302,9 @@ def ide_emit(self, oXML=None): styleN.fontSize = 7 styleN.leading = 10 P = Paragraph(cEnd, styleN) - w, h = P.wrap(55*mm, 30*mm) - P.drawOn(self.canvas, (self.nLeft+30)*mm, - (self.height-self.nlin-31)*mm) + w, h = P.wrap(55 * mm, 30 * mm) + P.drawOn(self.canvas, (self.nLeft + 30) * mm, + (self.height - self.nlin - 31) * mm) # Homologação if tagtext(oNode=elem_ide, cTag='tpAmb') == '2': @@ -309,7 +312,7 @@ def ide_emit(self, oXML=None): self.canvas.rotate(90) self.canvas.setFont('Times-Bold', 40) self.canvas.setFillColorRGB(0.57, 0.57, 0.57) - self.string(self.nLeft+65, 449, 'SEM VALOR FISCAL') + self.string(self.nLeft + 65, 449, 'SEM VALOR FISCAL') self.canvas.restoreState() self.nlin += 48 @@ -317,88 +320,90 @@ def ide_emit(self, oXML=None): def destinatario(self, oXML=None): elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") elem_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") - nMr = self.width-self.nRight + nMr = self.width - self.nRight self.nlin += 1 self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'DESTINATÁRIO/REMETENTE') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 20) - self.vline(nMr-25, self.nlin+2, 20) - self.hline(self.nLeft, self.nlin+8.66, self.width-self.nLeft) - self.hline(self.nLeft, self.nlin+15.32, self.width-self.nLeft) - self.vline(nMr-70, self.nlin+2, 6.66) - self.vline(nMr-53, self.nlin+8.66, 6.66) - self.vline(nMr-99, self.nlin+8.66, 6.66) - self.vline(nMr-90, self.nlin+15.32, 6.66) - self.vline(nMr-102, self.nlin+15.32, 6.66) - self.vline(nMr-136, self.nlin+15.32, 6.66) + self.string(self.nLeft + 1, self.nlin + 1, 'DESTINATÁRIO/REMETENTE') + self.rect(self.nLeft, self.nlin + 2, + self.width - self.nLeft - self.nRight, 20) + self.vline(nMr - 25, self.nlin + 2, 20) + self.hline(self.nLeft, self.nlin + 8.66, self.width - self.nLeft) + self.hline(self.nLeft, self.nlin + 15.32, self.width - self.nLeft) + self.vline(nMr - 70, self.nlin + 2, 6.66) + self.vline(nMr - 53, self.nlin + 8.66, 6.66) + self.vline(nMr - 99, self.nlin + 8.66, 6.66) + self.vline(nMr - 90, self.nlin + 15.32, 6.66) + self.vline(nMr - 102, self.nlin + 15.32, 6.66) + self.vline(nMr - 136, self.nlin + 15.32, 6.66) # Labels/Fields self.canvas.setFont('NimbusSanL-Bold', 5) - self.string(self.nLeft+1, self.nlin+3.7, 'NOME/RAZÃO SOCIAL') - self.string(nMr-69, self.nlin+3.7, 'CNPJ/CPF') - self.string(nMr-24, self.nlin+3.7, 'DATA DA EMISSÃO') - self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') - self.string(nMr-98, self.nlin+10.3, 'BAIRRO/DISTRITO') - self.string(nMr-52, self.nlin+10.3, 'CEP') - self.string(nMr-24, self.nlin+10.3, 'DATA DE ENTRADA/SAÍDA') - self.string(self.nLeft+1, self.nlin+17.1, 'MUNICÍPIO') - self.string(nMr-135, self.nlin+17.1, 'FONE/FAX') - self.string(nMr-101, self.nlin+17.1, 'UF') - self.string(nMr-89, self.nlin+17.1, 'INSCRIÇÃO ESTADUAL') - self.string(nMr-24, self.nlin+17.1, 'HORA DE ENTRADA/SAÍDA') + self.string(self.nLeft + 1, self.nlin + 3.7, 'NOME/RAZÃO SOCIAL') + self.string(nMr - 69, self.nlin + 3.7, 'CNPJ/CPF') + self.string(nMr - 24, self.nlin + 3.7, 'DATA DA EMISSÃO') + self.string(self.nLeft + 1, self.nlin + 10.3, 'ENDEREÇO') + self.string(nMr - 98, self.nlin + 10.3, 'BAIRRO/DISTRITO') + self.string(nMr - 52, self.nlin + 10.3, 'CEP') + self.string(nMr - 24, self.nlin + 10.3, 'DATA DE ENTRADA/SAÍDA') + self.string(self.nLeft + 1, self.nlin + 17.1, 'MUNICÍPIO') + self.string(nMr - 135, self.nlin + 17.1, 'FONE/FAX') + self.string(nMr - 101, self.nlin + 17.1, 'UF') + self.string(nMr - 89, self.nlin + 17.1, 'INSCRIÇÃO ESTADUAL') + self.string(nMr - 24, self.nlin + 17.1, 'HORA DE ENTRADA/SAÍDA') # Conteúdo campos 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')) - cnpj_cpf = format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CNPJ')) - if cnpj_cpf == '..-' or not cnpj_cpf: + 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) + self.string(nMr - 69, self.nlin + 7.5, cnpj_cpf) 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')) - self.string(nMr-24, self.nlin+14.3, cDt + ' ' + cHr) # Dt saída + self.string(nMr - 24, self.nlin + 14.3, cDt + ' ' + cHr) # Dt saída cEnd = tagtext(oNode=elem_dest, cTag='xLgr') + ', ' + tagtext( oNode=elem_dest, cTag='nro') - self.string(self.nLeft+1, self.nlin+14.3, cEnd) - self.string(nMr-98, self.nlin+14.3, + self.string(self.nLeft + 1, self.nlin + 14.3, cEnd) + self.string(nMr - 98, self.nlin + 14.3, tagtext(oNode=elem_dest, cTag='xBairro')) - self.string(nMr-52, self.nlin+14.3, + self.string(nMr - 52, self.nlin + 14.3, tagtext(oNode=elem_dest, cTag='CEP')) - self.string(self.nLeft+1, self.nlin+21.1, + self.string(self.nLeft + 1, self.nlin + 21.1, tagtext(oNode=elem_dest, cTag='xMun')) - self.string(nMr-135, self.nlin+21.1, + self.string(nMr - 135, self.nlin + 21.1, tagtext(oNode=elem_dest, cTag='fone')) - self.string(nMr-101, self.nlin+21.1, + self.string(nMr - 101, self.nlin + 21.1, tagtext(oNode=elem_dest, cTag='UF')) - self.string(nMr-89, self.nlin+21.1, + self.string(nMr - 89, self.nlin + 21.1, tagtext(oNode=elem_dest, cTag='IE')) self.nlin += 24 # Nr linhas ocupadas pelo bloco def faturas(self, oXML=None): - nMr = self.width-self.nRight + nMr = self.width - self.nRight self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'FATURA') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 13) - self.vline(nMr-47.5, self.nlin+2, 13) - self.vline(nMr-95, self.nlin+2, 13) - self.vline(nMr-142.5, self.nlin+2, 13) - self.hline(nMr-47.5, self.nlin+8.5, self.width-self.nLeft) + self.string(self.nLeft + 1, self.nlin + 1, 'FATURA') + self.rect(self.nLeft, self.nlin + 2, + self.width - self.nLeft - self.nRight, 13) + self.vline(nMr - 47.5, self.nlin + 2, 13) + self.vline(nMr - 95, self.nlin + 2, 13) + self.vline(nMr - 142.5, self.nlin + 2, 13) + self.hline(nMr - 47.5, self.nlin + 8.5, self.width - self.nLeft) # Labels self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(nMr-46.5, self.nlin+3.8, 'CÓDIGO VENDEDOR') - self.string(nMr-46.5, self.nlin+10.2, 'NOME VENDEDOR') - self.string(nMr-93.5, self.nlin+3.8, + self.string(nMr - 46.5, self.nlin + 3.8, 'CÓDIGO VENDEDOR') + self.string(nMr - 46.5, self.nlin + 10.2, 'NOME VENDEDOR') + self.string(nMr - 93.5, self.nlin + 3.8, 'FATURA VENCIMENTO VALOR') - self.string(nMr-140.5, self.nlin+3.8, + self.string(nMr - 140.5, self.nlin + 3.8, 'FATURA VENCIMENTO VALOR') - self.string(self.nLeft+2, self.nlin+3.8, + self.string(self.nLeft + 2, self.nlin + 3.8, 'FATURA VENCIMENTO VALOR') # Conteúdo campos @@ -412,11 +417,11 @@ def faturas(self, oXML=None): for oXML_dup in line_iter: cDt, cHr = getdateUTC(tagtext(oNode=oXML_dup, cTag='dVenc')) - self.string(self.nLeft+nCol+1, self.nlin+nLin, + self.string(self.nLeft + nCol + 1, self.nlin + nLin, tagtext(oNode=oXML_dup, cTag='nDup')) - self.string(self.nLeft+nCol+17, self.nlin+nLin, cDt) + self.string(self.nLeft + nCol + 17, self.nlin + nLin, cDt) 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)) @@ -437,11 +442,11 @@ def faturas(self, oXML=None): codvend = elem_infAdic.find( ".//{http://www.portalfiscal.inf.br/nfe}obsCont\ [@xCampo='CodVendedor']") - self.string(nMr-46.5, self.nlin+7.7, + self.string(nMr - 46.5, self.nlin + 7.7, tagtext(oNode=codvend, cTag='xTexto')) vend = elem_infAdic.find(".//{http://www.portalfiscal.inf.br/nfe}\ obsCont[@xCampo='NomeVendedor']") - self.string(nMr-46.5, self.nlin+14.3, + self.string(nMr - 46.5, self.nlin + 14.3, tagtext(oNode=vend, cTag='xTexto')[:36]) self.nlin += 16 # Nr linhas ocupadas pelo bloco @@ -449,149 +454,149 @@ def faturas(self, oXML=None): def impostos(self, oXML=None): # Impostos el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") - nMr = self.width-self.nRight + nMr = self.width - self.nRight self.nlin += 1 self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'CÁLCULO DO IMPOSTO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 13) - self.hline(self.nLeft, self.nlin+8.5, self.width-self.nLeft) - self.vline(nMr-35, self.nlin+2, 6.5) - self.vline(nMr-65, self.nlin+2, 6.5) - self.vline(nMr-95, self.nlin+2, 6.5) - self.vline(nMr-125, self.nlin+2, 6.5) - self.vline(nMr-155, self.nlin+2, 6.5) - self.vline(nMr-35, self.nlin+8.5, 6.5) - self.vline(nMr-65, self.nlin+8.5, 6.5) - self.vline(nMr-95, self.nlin+8.5, 6.5) - self.vline(nMr-125, self.nlin+8.5, 6.5) - self.vline(nMr-155, self.nlin+8.5, 6.5) + self.string(self.nLeft + 1, self.nlin + 1, 'CÁLCULO DO IMPOSTO') + self.rect(self.nLeft, self.nlin + 2, + self.width - self.nLeft - self.nRight, 13) + self.hline(self.nLeft, self.nlin + 8.5, self.width - self.nLeft) + self.vline(nMr - 35, self.nlin + 2, 6.5) + self.vline(nMr - 65, self.nlin + 2, 6.5) + self.vline(nMr - 95, self.nlin + 2, 6.5) + self.vline(nMr - 125, self.nlin + 2, 6.5) + self.vline(nMr - 155, self.nlin + 2, 6.5) + self.vline(nMr - 35, self.nlin + 8.5, 6.5) + self.vline(nMr - 65, self.nlin + 8.5, 6.5) + self.vline(nMr - 95, self.nlin + 8.5, 6.5) + self.vline(nMr - 125, self.nlin + 8.5, 6.5) + self.vline(nMr - 155, self.nlin + 8.5, 6.5) # Labels self.canvas.setFont('NimbusSanL-Regu', 5) - self.string(self.nLeft+1, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS') - self.string(nMr-154, self.nlin+3.8, 'VALOR DO ICMS') - self.string(nMr-124, self.nlin+3.8, 'BASE DE CÁLCULO DO ICMS ST') - self.string(nMr-94, self.nlin+3.8, 'VALOR DO ICMS ST') - self.string(nMr-64, self.nlin+3.8, 'VALOR APROX TRIBUTOS') - self.string(nMr-34, self.nlin+3.8, 'VALOR TOTAL DOS PRODUTOS') - - self.string(self.nLeft+1, self.nlin+10.2, 'VALOR DO FRETE') - self.string(nMr-154, self.nlin+10.2, 'VALOR DO SEGURO') - self.string(nMr-124, self.nlin+10.2, 'DESCONTO') - self.string(nMr-94, self.nlin+10.2, 'OUTRAS DESP. ACESSÓRIAS') - self.string(nMr-64, self.nlin+10.2, 'VALOR DO IPI') - self.string(nMr-34, self.nlin+10.2, 'VALOR TOTAL DA NOTA') + self.string(self.nLeft + 1, self.nlin + 3.8, 'BASE DE CÁLCULO DO ICMS') + self.string(nMr - 154, self.nlin + 3.8, 'VALOR DO ICMS') + self.string(nMr - 124, self.nlin + 3.8, 'BASE DE CÁLCULO DO ICMS ST') + self.string(nMr - 94, self.nlin + 3.8, 'VALOR DO ICMS ST') + self.string(nMr - 64, self.nlin + 3.8, 'VALOR APROX TRIBUTOS') + self.string(nMr - 34, self.nlin + 3.8, 'VALOR TOTAL DOS PRODUTOS') + + self.string(self.nLeft + 1, self.nlin + 10.2, 'VALOR DO FRETE') + self.string(nMr - 154, self.nlin + 10.2, 'VALOR DO SEGURO') + self.string(nMr - 124, self.nlin + 10.2, 'DESCONTO') + self.string(nMr - 94, self.nlin + 10.2, 'OUTRAS DESP. ACESSÓRIAS') + self.string(nMr - 64, self.nlin + 10.2, 'VALOR DO IPI') + self.string(nMr - 34, self.nlin + 10.2, 'VALOR TOTAL DA NOTA') # Conteúdo campos self.canvas.setFont('NimbusSanL-Regu', 8) 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)) 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)) 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)) self.stringRight( - nMr-66, self.nlin+7.7, + nMr - 66, self.nlin + 7.7, format_number(tagtext(oNode=el_total, cTag='vST'), precision=2)) self.stringRight( - nMr-36, self.nlin+7.7, + nMr - 36, self.nlin + 7.7, format_number(tagtext(oNode=el_total, cTag='vTotTrib'), precision=2)) self.stringRight( - nMr-1, self.nlin+7.7, + nMr - 1, self.nlin + 7.7, format_number(tagtext(oNode=el_total, cTag='vProd'), precision=2)) 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)) 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)) 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)) 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)) 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)) self.stringRight( - nMr-1, self.nlin+14.1, + nMr - 1, self.nlin + 14.1, format_number(tagtext(oNode=el_total, cTag='vNF'), precision=2)) self.nlin += 17 # Nr linhas ocupadas pelo bloco def transportes(self, oXML=None): el_transp = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}transp") - nMr = self.width-self.nRight + nMr = self.width - self.nRight self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, + self.string(self.nLeft + 1, self.nlin + 1, 'TRANSPORTADOR/VOLUMES TRANSPORTADOS') self.canvas.setFont('NimbusSanL-Regu', 5) - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 20) - self.hline(self.nLeft, self.nlin+8.6, self.width-self.nLeft) - self.hline(self.nLeft, self.nlin+15.2, self.width-self.nLeft) - self.vline(nMr-40, self.nlin+2, 13.2) - self.vline(nMr-49, self.nlin+2, 20) - self.vline(nMr-92, self.nlin+2, 6.6) - self.vline(nMr-120, self.nlin+2, 6.6) - self.vline(nMr-75, self.nlin+2, 6.6) - self.vline(nMr-26, self.nlin+15.2, 6.6) - self.vline(nMr-102, self.nlin+8.6, 6.6) - self.vline(nMr-85, self.nlin+15.2, 6.6) - self.vline(nMr-121, self.nlin+15.2, 6.6) - self.vline(nMr-160, self.nlin+15.2, 6.6) + self.rect(self.nLeft, self.nlin + 2, + self.width - self.nLeft - self.nRight, 20) + self.hline(self.nLeft, self.nlin + 8.6, self.width - self.nLeft) + self.hline(self.nLeft, self.nlin + 15.2, self.width - self.nLeft) + self.vline(nMr - 40, self.nlin + 2, 13.2) + self.vline(nMr - 49, self.nlin + 2, 20) + self.vline(nMr - 92, self.nlin + 2, 6.6) + self.vline(nMr - 120, self.nlin + 2, 6.6) + self.vline(nMr - 75, self.nlin + 2, 6.6) + self.vline(nMr - 26, self.nlin + 15.2, 6.6) + self.vline(nMr - 102, self.nlin + 8.6, 6.6) + self.vline(nMr - 85, self.nlin + 15.2, 6.6) + self.vline(nMr - 121, self.nlin + 15.2, 6.6) + self.vline(nMr - 160, self.nlin + 15.2, 6.6) # Labels/Fields - self.string(nMr-39, self.nlin+3.8, 'CNPJ/CPF') - self.string(nMr-74, self.nlin+3.8, 'PLACA DO VEÍCULO') - self.string(nMr-91, self.nlin+3.8, 'CÓDIGO ANTT') - self.string(nMr-119, self.nlin+3.8, 'FRETE POR CONTA') - self.string(self.nLeft+1, self.nlin+3.8, 'RAZÃO SOCIAL') - self.string(nMr-48, self.nlin+3.8, 'UF') - self.string(nMr-39, self.nlin+10.3, 'INSCRIÇÃO ESTADUAL') - self.string(nMr-48, self.nlin+10.3, 'UF') - self.string(nMr-101, self.nlin+10.3, 'MUNICÍPIO') - self.string(self.nLeft+1, self.nlin+10.3, 'ENDEREÇO') - self.string(nMr-48, self.nlin+17, 'PESO BRUTO') - self.string(nMr-25, self.nlin+17, 'PESO LÍQUIDO') - self.string(nMr-84, self.nlin+17, 'NUMERAÇÃO') - self.string(nMr-120, self.nlin+17, 'MARCA') - self.string(nMr-159, self.nlin+17, 'ESPÉCIE') - self.string(self.nLeft+1, self.nlin+17, 'QUANTIDADE') + self.string(nMr - 39, self.nlin + 3.8, 'CNPJ/CPF') + self.string(nMr - 74, self.nlin + 3.8, 'PLACA DO VEÍCULO') + self.string(nMr - 91, self.nlin + 3.8, 'CÓDIGO ANTT') + self.string(nMr - 119, self.nlin + 3.8, 'FRETE POR CONTA') + self.string(self.nLeft + 1, self.nlin + 3.8, 'RAZÃO SOCIAL') + self.string(nMr - 48, self.nlin + 3.8, 'UF') + self.string(nMr - 39, self.nlin + 10.3, 'INSCRIÇÃO ESTADUAL') + self.string(nMr - 48, self.nlin + 10.3, 'UF') + self.string(nMr - 101, self.nlin + 10.3, 'MUNICÍPIO') + self.string(self.nLeft + 1, self.nlin + 10.3, 'ENDEREÇO') + self.string(nMr - 48, self.nlin + 17, 'PESO BRUTO') + self.string(nMr - 25, self.nlin + 17, 'PESO LÍQUIDO') + self.string(nMr - 84, self.nlin + 17, 'NUMERAÇÃO') + self.string(nMr - 120, self.nlin + 17, 'MARCA') + self.string(nMr - 159, self.nlin + 17, 'ESPÉCIE') + self.string(self.nLeft + 1, self.nlin + 17, 'QUANTIDADE') # Conteúdo campos self.canvas.setFont('NimbusSanL-Regu', 8) - self.string(self.nLeft+1, self.nlin+7.7, + self.string(self.nLeft + 1, self.nlin + 7.7, tagtext(oNode=el_transp, cTag='xNome')[:40]) - self.string(self.nLeft+71, self.nlin+7.7, + self.string(self.nLeft + 71, self.nlin + 7.7, self.oFrete[tagtext(oNode=el_transp, cTag='modFrete')]) - self.string(nMr-39, self.nlin+7.7, + self.string(nMr - 39, self.nlin + 7.7, format_cnpj_cpf(tagtext(oNode=el_transp, cTag='CNPJ'))) - self.string(self.nLeft+1, self.nlin+14.2, + self.string(self.nLeft + 1, self.nlin + 14.2, tagtext(oNode=el_transp, cTag='xEnder')[:45]) - self.string(self.nLeft+89, self.nlin+14.2, + self.string(self.nLeft + 89, self.nlin + 14.2, tagtext(oNode=el_transp, cTag='xMun')) - self.string(nMr-48, self.nlin+14.2, + self.string(nMr - 48, self.nlin + 14.2, tagtext(oNode=el_transp, cTag='UF')) - self.string(nMr-39, self.nlin+14.2, + self.string(nMr - 39, self.nlin + 14.2, tagtext(oNode=el_transp, cTag='IE')) - self.string(self.nLeft+1, self.nlin+21.2, + self.string(self.nLeft + 1, self.nlin + 21.2, tagtext(oNode=el_transp, cTag='qVol')) - self.string(self.nLeft+31, self.nlin+21.2, + self.string(self.nLeft + 31, self.nlin + 21.2, tagtext(oNode=el_transp, cTag='esp')) - self.string(self.nLeft+70, self.nlin+21.2, + self.string(self.nLeft + 70, self.nlin + 21.2, tagtext(oNode=el_transp, cTag='marca')) - self.string(self.nLeft+106, self.nlin+21.2, + self.string(self.nLeft + 106, self.nlin + 21.2, tagtext(oNode=el_transp, cTag='nVol')) self.stringRight( - nMr-27, self.nlin+21.2, + nMr - 27, self.nlin + 21.2, format_number(tagtext(oNode=el_transp, cTag='pesoB'), precision=3)) self.stringRight( - nMr-1, self.nlin+21.2, + nMr - 1, self.nlin + 21.2, format_number(tagtext(oNode=el_transp, cTag='pesoL'), precision=3)) self.nlin += 23 @@ -599,55 +604,56 @@ def transportes(self, oXML=None): def produtos(self, oXML=None, el_det=None, oPaginator=None, list_desc=None, list_cod_prod=None, nHeight=29): - nMr = self.width-self.nRight + nMr = self.width - self.nRight nStep = 2.5 # Passo entre linhas nH = 7.5 + (nHeight * nStep) # cabeçalho 7.5 self.nlin += 1 self.canvas.setFont('NimbusSanL-Bold', 7) - self.string(self.nLeft+1, self.nlin+1, 'DADOS DO PRODUTO/SERVIÇO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, nH) - self.hline(self.nLeft, self.nlin+8, self.width-self.nLeft) + self.string(self.nLeft + 1, self.nlin + 1, 'DADOS DO PRODUTO/SERVIÇO') + self.rect(self.nLeft, self.nlin + 2, + self.width - self.nLeft - self.nRight, nH) + self.hline(self.nLeft, self.nlin + 8, self.width - self.nLeft) self.canvas.setFont('NimbusSanL-Regu', 5.5) # Colunas - self.vline(self.nLeft+15, self.nlin+2, nH) - self.stringcenter(self.nLeft+7.5, self.nlin+5.5, 'CÓDIGO') - self.vline(nMr-7, self.nlin+2, nH) - self.stringcenter(nMr-3.5, self.nlin+4.5, 'ALÍQ') - self.stringcenter(nMr-3.5, self.nlin+6.5, 'IPI') - self.vline(nMr-14, self.nlin+2, nH) - self.stringcenter(nMr-10.5, self.nlin+4.5, 'ALÍQ') - self.stringcenter(nMr-10.5, self.nlin+6.5, 'ICMS') - self.vline(nMr-26, self.nlin+2, nH) - self.stringcenter(nMr-20, self.nlin+5.5, 'VLR. IPI') - self.vline(nMr-38, self.nlin+2, nH) - self.stringcenter(nMr-32, self.nlin+5.5, 'VLR. ICMS') - self.vline(nMr-50, self.nlin+2, nH) - self.stringcenter(nMr-44, self.nlin+5.5, 'BC ICMS') - self.vline(nMr-64, self.nlin+2, nH) - self.stringcenter(nMr-57, self.nlin+5.5, 'VLR TOTAL') - self.vline(nMr-77, self.nlin+2, nH) - self.stringcenter(nMr-70.5, self.nlin+5.5, 'VLR UNIT') - self.vline(nMr-90, self.nlin+2, nH) - self.stringcenter(nMr-83.5, self.nlin+5.5, 'QTD') - self.vline(nMr-96, self.nlin+2, nH) - self.stringcenter(nMr-93, self.nlin+5.5, 'UNID') - self.vline(nMr-102, self.nlin+2, nH) - self.stringcenter(nMr-99, self.nlin+5.5, 'CFOP') - self.vline(nMr-108, self.nlin+2, nH) - self.stringcenter(nMr-105, self.nlin+5.5, 'CST') - self.vline(nMr-117, self.nlin+2, nH) - self.stringcenter(nMr-112.5, self.nlin+5.5, 'NCM/SH') - - nWidth_Prod = nMr-135-self.nLeft-11 - nCol_ = self.nLeft+20 + (nWidth_Prod / 2) - self.stringcenter(nCol_, self.nlin+5.5, 'DESCRIÇÃO DO PRODUTO/SERVIÇO') + self.vline(self.nLeft + 15, self.nlin + 2, nH) + self.stringcenter(self.nLeft + 7.5, self.nlin + 5.5, 'CÓDIGO') + self.vline(nMr - 7, self.nlin + 2, nH) + self.stringcenter(nMr - 3.5, self.nlin + 4.5, 'ALÍQ') + self.stringcenter(nMr - 3.5, self.nlin + 6.5, 'IPI') + self.vline(nMr - 14, self.nlin + 2, nH) + self.stringcenter(nMr - 10.5, self.nlin + 4.5, 'ALÍQ') + self.stringcenter(nMr - 10.5, self.nlin + 6.5, 'ICMS') + self.vline(nMr - 26, self.nlin + 2, nH) + self.stringcenter(nMr - 20, self.nlin + 5.5, 'VLR. IPI') + self.vline(nMr - 38, self.nlin + 2, nH) + self.stringcenter(nMr - 32, self.nlin + 5.5, 'VLR. ICMS') + self.vline(nMr - 50, self.nlin + 2, nH) + self.stringcenter(nMr - 44, self.nlin + 5.5, 'BC ICMS') + self.vline(nMr - 64, self.nlin + 2, nH) + self.stringcenter(nMr - 57, self.nlin + 5.5, 'VLR TOTAL') + self.vline(nMr - 77, self.nlin + 2, nH) + self.stringcenter(nMr - 70.5, self.nlin + 5.5, 'VLR UNIT') + self.vline(nMr - 90, self.nlin + 2, nH) + self.stringcenter(nMr - 83.5, self.nlin + 5.5, 'QTD') + self.vline(nMr - 96, self.nlin + 2, nH) + self.stringcenter(nMr - 93, self.nlin + 5.5, 'UNID') + self.vline(nMr - 102, self.nlin + 2, nH) + self.stringcenter(nMr - 99, self.nlin + 5.5, 'CFOP') + self.vline(nMr - 108, self.nlin + 2, nH) + self.stringcenter(nMr - 105, self.nlin + 5.5, 'CST') + self.vline(nMr - 117, self.nlin + 2, nH) + self.stringcenter(nMr - 112.5, self.nlin + 5.5, 'NCM/SH') + + nWidth_Prod = nMr - 135 - self.nLeft - 11 + nCol_ = self.nLeft + 20 + (nWidth_Prod / 2) + self.stringcenter(nCol_, self.nlin + 5.5, + 'DESCRIÇÃO DO PRODUTO/SERVIÇO') # Conteúdo campos self.canvas.setFont('NimbusSanL-Regu', 5) - nLin = self.nlin+10.5 + nLin = self.nlin + 10.5 for id in range(oPaginator[0], oPaginator[1]): item = el_det[id] @@ -661,7 +667,8 @@ def produtos(self, oXML=None, el_det=None, oPaginator=None, ".//{http://www.portalfiscal.inf.br/nfe}IPI") cCST = tagtext(oNode=el_imp_ICMS, cTag='orig') + \ - tagtext(oNode=el_imp_ICMS, cTag='CSOSN') + (tagtext(oNode=el_imp_ICMS, cTag='CST') or + tagtext(oNode=el_imp_ICMS, cTag='CSOSN')) vBC = tagtext(oNode=el_imp_ICMS, cTag='vBC') vICMS = tagtext(oNode=el_imp_ICMS, cTag='vICMS') pICMS = tagtext(oNode=el_imp_ICMS, cTag='pICMS') @@ -669,45 +676,47 @@ def produtos(self, oXML=None, el_det=None, oPaginator=None, vIPI = tagtext(oNode=el_imp_IPI, cTag='vIPI') pIPI = tagtext(oNode=el_imp_IPI, cTag='pIPI') - self.stringcenter(nMr-112.5, nLin, + self.stringcenter(nMr - 112.5, nLin, tagtext(oNode=el_prod, cTag='NCM')) - self.stringcenter(nMr-105, nLin, cCST) - self.stringcenter(nMr-99, nLin, + self.stringcenter(nMr - 105, nLin, cCST) + self.stringcenter(nMr - 99, nLin, tagtext(oNode=el_prod, cTag='CFOP')) - self.stringcenter(nMr-93, nLin, + self.stringcenter(nMr - 93, nLin, tagtext(oNode=el_prod, cTag='uCom')) - self.stringRight(nMr-77.5, nLin, format_number( + self.stringRight(nMr - 77.5, nLin, format_number( tagtext(oNode=el_prod, cTag='qCom'), precision=4)) - 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( + 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)) + 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)) if vIPI: - self.stringRight(nMr-14.5, nLin, + self.stringRight(nMr - 14.5, nLin, format_number(vIPI, precision=2)) if pIPI: - self.stringRight(nMr-0.5, nLin, + self.stringRight(nMr - 0.5, nLin, format_number(pIPI, precision=2)) # Código Item line_cod = nLin for des in list_cod_prod[id]: - self.string(self.nLeft+0.2, line_cod, des) + self.string(self.nLeft + 0.2, line_cod, des) line_cod += nStep # Descrição Item line_desc = nLin for des in list_desc[id]: - self.string(self.nLeft+15.5, line_desc, des) + self.string(self.nLeft + 15.5, line_desc, des) line_desc += nStep nLin = max(line_cod, line_desc) self.canvas.setStrokeColor(gray) - self.hline(self.nLeft, nLin-2, self.width-self.nLeft) + self.hline(self.nLeft, nLin - 2, self.width - self.nLeft) self.canvas.setStrokeColor(black) self.nlin += nH + 3 @@ -718,13 +727,14 @@ def adicionais(self, oXML=None): self.nlin += 2 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.string(self.nLeft+1, self.nlin+4, 'INFORMAÇÕES COMPLEMENTARES') - self.string((self.width/2)+1, self.nlin+4, 'RESERVADO AO FISCO') - self.rect(self.nLeft, self.nlin+2, - self.width-self.nLeft-self.nRight, 42) - self.vline(self.width/2, self.nlin+2, 42) + self.string(self.nLeft + 1, self.nlin + 4, + 'INFORMAÇÕES COMPLEMENTARES') + self.string((self.width / 2) + 1, self.nlin + 4, 'RESERVADO AO FISCO') + self.rect(self.nLeft, self.nlin + 2, + self.width - self.nLeft - self.nRight, 42) + self.vline(self.width / 2, self.nlin + 2, 42) # Conteúdo campos styles = getSampleStyleSheet() styleN = styles['Normal'] @@ -737,9 +747,9 @@ def adicionais(self, oXML=None): if fisco: observacoes = fisco + ' ' + observacoes P = Paragraph(observacoes, styles['Normal']) - w, h = P.wrap(92*mm, 32*mm) - altura = (self.height-self.nlin-5)*mm - P.drawOn(self.canvas, (self.nLeft+1)*mm, altura - h) + w, h = P.wrap(92 * mm, 32 * mm) + altura = (self.height - self.nlin - 5) * mm + P.drawOn(self.canvas, (self.nLeft + 1) * mm, altura - h) self.nlin += 36 def recibo_entrega(self, oXML=None): @@ -753,23 +763,25 @@ def recibo_entrega(self, oXML=None): nH = 17 self.canvas.setLineWidth(.5) self.rect(self.nLeft, self.nlin, - self.width-(self.nLeft+self.nRight), nH) - self.hline(self.nLeft, self.nlin+8.5, self.width-self.nRight-nW) - self.vline(self.width-self.nRight-nW, self.nlin, nH) - self.vline(self.nLeft+nW, self.nlin+8.5, 8.5) + self.width - (self.nLeft + self.nRight), nH) + self.hline(self.nLeft, self.nlin + 8.5, self.width - self.nRight - nW) + self.vline(self.width - self.nRight - nW, self.nlin, nH) + self.vline(self.nLeft + nW, self.nlin + 8.5, 8.5) # Labels self.canvas.setFont('NimbusSanL-Regu', 5) - 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 + 1, self.nlin + 10.2, 'DATA DE RECEBIMENTO') + self.string(self.nLeft + 41, self.nlin + 10.2, '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 self.canvas.setFont('NimbusSanL-Bold', 8) cNF = tagtext(oNode=el_ide, cTag='nNF') cNF = '{0:011,}'.format(int(cNF)).replace(",", ".") - self.string(self.width-self.nRight-nW+2, self.nlin+8, "Nº %s" % (cNF)) - self.string(self.width-self.nRight-nW+2, self.nlin+14, + self.string(self.width - self.nRight - nW + + 2, self.nlin + 8, "Nº %s" % (cNF)) + self.string(self.width - self.nRight - nW + 2, self.nlin + 14, "SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie'))) cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi')) @@ -796,12 +808,12 @@ def recibo_entrega(self, oXML=None): styleN.leading = 7 P = Paragraph(cString, styleN) - w, h = P.wrap(149*mm, 7*mm) - P.drawOn(self.canvas, (self.nLeft+1)*mm, - ((self.height-self.nlin)*mm) - h) + w, h = P.wrap(149 * mm, 7 * mm) + P.drawOn(self.canvas, (self.nLeft + 1) * mm, + ((self.height - self.nlin) * mm) - h) self.nlin += 20 - self.hline(self.nLeft, self.nlin, self.width-self.nRight) + self.hline(self.nLeft, self.nlin, self.width - self.nRight) self.nlin += 2 def newpage(self): @@ -811,31 +823,32 @@ def newpage(self): def hline(self, x, y, width): y = self.height - y - self.canvas.line(x*mm, y*mm, width*mm, y*mm) + self.canvas.line(x * mm, y * mm, width * mm, y * mm) def vline(self, x, y, width): width = self.height - y - width y = self.height - y - self.canvas.line(x*mm, y*mm, x*mm, width*mm) + self.canvas.line(x * mm, y * mm, x * mm, width * mm) def rect(self, col, lin, nWidth, nHeight, fill=False): lin = self.height - nHeight - lin - self.canvas.rect(col*mm, lin*mm, nWidth*mm, nHeight*mm, + self.canvas.rect(col * mm, lin * mm, nWidth * mm, nHeight * mm, stroke=True, fill=fill) def string(self, x, y, value): y = self.height - y - self.canvas.drawString(x*mm, y*mm, value) + self.canvas.drawString(x * mm, y * mm, value) def stringRight(self, x, y, value): y = self.height - y - self.canvas.drawRightString(x*mm, y*mm, value) + self.canvas.drawRightString(x * mm, y * mm, value) def stringcenter(self, x, y, value): y = self.height - y - self.canvas.drawCentredString(x*mm, y*mm, value) + self.canvas.drawCentredString(x * mm, y * mm, value) def writeto_pdf(self, fileObj): pdf_out = self.oPDF_IO.getvalue() self.oPDF_IO.close() fileObj.write(pdf_out) + From 4fc6b9dc89ac64fbbea22a51dcddd9d9f85e0e99 Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 21 Dec 2017 17:14:45 -0200 Subject: [PATCH 24/31] adicionados urls de cidades do dsf --- pytrustnfe/nfse/campinas/__init__.py | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pytrustnfe/nfse/campinas/__init__.py b/pytrustnfe/nfse/campinas/__init__.py index e5536af2..80d839d9 100644 --- a/pytrustnfe/nfse/campinas/__init__.py +++ b/pytrustnfe/nfse/campinas/__init__.py @@ -24,8 +24,35 @@ def _render(certificado, method, **kwargs): return xml_send +def _get_url(**kwargs): + + try: + cod_cidade = kwargs['CodCidade'] + except (KeyError, TypeError): + return '' + + urls = { + # Belém - PA + '2715': 'http://www.issdigitalbel.com.br/WsNFe2/LoteRps.jws', + # Sorocaba - SP + '5363': 'http://issdigital.sorocaba.sp.gov.br/WsNFe2/LoteRps.jws', + # Teresina - PI + '3182': 'http://www.issdigitalthe.com.br/WsNFe2/LoteRps.jws', + # Campinas - SP + '4888': 'http://issdigital.campinas.sp.gov.br/WsNFe2/LoteRps.jws?wsdl', + # Uberlandia - MG + '2170': 'http://udigital.uberlandia.mg.gov.br/WsNFe2/LoteRps.jws', + # São Luis - MA + '1314': 'https://stm.semfaz.saoluis.ma.gov.br/WsNFe2/LoteRps?wsdl', + # Campo Grande - MS + '2218': 'http://issdigital.pmcg.ms.gov.br/WsNFe2/LoteRps.jws', + } + + return urls[str(cod_cidade)] + + 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') From d2a0c26b1a7162cc75d01d07ef8a259ef69f7c00 Mon Sep 17 00:00:00 2001 From: Felipe Date: Fri, 22 Dec 2017 11:09:17 -0200 Subject: [PATCH 25/31] =?UTF-8?q?mudan=C3=A7a=20nfse=20campinas=20para=20d?= =?UTF-8?q?sf,=20adicionadas=20outras=20cidades=20tamb=C3=A9m=20emitidas?= =?UTF-8?q?=20pela=20dsf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfse/{campinas => dsf}/__init__.py | 0 pytrustnfe/nfse/{campinas => dsf}/templates/cancelar.xml | 0 pytrustnfe/nfse/{campinas => dsf}/templates/consulta_notas.xml | 0 pytrustnfe/nfse/{campinas => dsf}/templates/consultarLote.xml | 0 pytrustnfe/nfse/{campinas => dsf}/templates/enviar.xml | 0 pytrustnfe/nfse/{campinas => dsf}/templates/soap_header.xml | 0 setup.py | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) rename pytrustnfe/nfse/{campinas => dsf}/__init__.py (100%) rename pytrustnfe/nfse/{campinas => dsf}/templates/cancelar.xml (100%) rename pytrustnfe/nfse/{campinas => dsf}/templates/consulta_notas.xml (100%) rename pytrustnfe/nfse/{campinas => dsf}/templates/consultarLote.xml (100%) rename pytrustnfe/nfse/{campinas => dsf}/templates/enviar.xml (100%) rename pytrustnfe/nfse/{campinas => dsf}/templates/soap_header.xml (100%) diff --git a/pytrustnfe/nfse/campinas/__init__.py b/pytrustnfe/nfse/dsf/__init__.py similarity index 100% rename from pytrustnfe/nfse/campinas/__init__.py rename to pytrustnfe/nfse/dsf/__init__.py diff --git a/pytrustnfe/nfse/campinas/templates/cancelar.xml b/pytrustnfe/nfse/dsf/templates/cancelar.xml similarity index 100% rename from pytrustnfe/nfse/campinas/templates/cancelar.xml rename to pytrustnfe/nfse/dsf/templates/cancelar.xml diff --git a/pytrustnfe/nfse/campinas/templates/consulta_notas.xml b/pytrustnfe/nfse/dsf/templates/consulta_notas.xml similarity index 100% rename from pytrustnfe/nfse/campinas/templates/consulta_notas.xml rename to pytrustnfe/nfse/dsf/templates/consulta_notas.xml diff --git a/pytrustnfe/nfse/campinas/templates/consultarLote.xml b/pytrustnfe/nfse/dsf/templates/consultarLote.xml similarity index 100% rename from pytrustnfe/nfse/campinas/templates/consultarLote.xml rename to pytrustnfe/nfse/dsf/templates/consultarLote.xml diff --git a/pytrustnfe/nfse/campinas/templates/enviar.xml b/pytrustnfe/nfse/dsf/templates/enviar.xml similarity index 100% rename from pytrustnfe/nfse/campinas/templates/enviar.xml rename to pytrustnfe/nfse/dsf/templates/enviar.xml diff --git a/pytrustnfe/nfse/campinas/templates/soap_header.xml b/pytrustnfe/nfse/dsf/templates/soap_header.xml similarity index 100% rename from pytrustnfe/nfse/campinas/templates/soap_header.xml rename to pytrustnfe/nfse/dsf/templates/soap_header.xml diff --git a/setup.py b/setup.py index a4bf038d..d0474572 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ 'nfe/templates/*xml', 'nfe/fonts/*ttf', 'nfse/paulistana/templates/*xml', - 'nfse/campinas/templates/*xml', + 'nfse/dsf/templates/*xml', 'nfse/ginfes/templates/*xml', 'nfse/simpliss/templates/*xml', 'nfse/betha/templates/*xml', From 36611c8f812e790dff5e1fb935e72327e338c5c2 Mon Sep 17 00:00:00 2001 From: Felipe Date: Tue, 26 Dec 2017 16:11:11 -0200 Subject: [PATCH 26/31] =?UTF-8?q?altera=C3=A7=C3=A3o=20nfse=20campinas=20p?= =?UTF-8?q?ara=20dsf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfse/dsf/__init__.py | 47 ++++++++++++++----- .../nfse/dsf/templates/consultarNFSeRps.xml | 22 +++++++++ pytrustnfe/xml/__init__.py | 1 - 3 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 pytrustnfe/nfse/dsf/templates/consultarNFSeRps.xml diff --git a/pytrustnfe/nfse/dsf/__init__.py b/pytrustnfe/nfse/dsf/__init__.py index 80d839d9..60273d06 100644 --- a/pytrustnfe/nfse/dsf/__init__.py +++ b/pytrustnfe/nfse/dsf/__init__.py @@ -27,28 +27,32 @@ def _render(certificado, method, **kwargs): def _get_url(**kwargs): try: - cod_cidade = kwargs['CodCidade'] + cod_cidade = kwargs['nfse']['cidade'] except (KeyError, TypeError): - return '' + raise KeyError("Código de cidade inválido!") urls = { # Belém - PA '2715': 'http://www.issdigitalbel.com.br/WsNFe2/LoteRps.jws', # Sorocaba - SP - '5363': 'http://issdigital.sorocaba.sp.gov.br/WsNFe2/LoteRps.jws', + '7145': 'http://issdigital.sorocaba.sp.gov.br/WsNFe2/LoteRps.jws', # Teresina - PI - '3182': 'http://www.issdigitalthe.com.br/WsNFe2/LoteRps.jws', + '1219': 'http://www.issdigitalthe.com.br/WsNFe2/LoteRps.jws', # Campinas - SP - '4888': 'http://issdigital.campinas.sp.gov.br/WsNFe2/LoteRps.jws?wsdl', + '6291': 'http://issdigital.campinas.sp.gov.br/WsNFe2/LoteRps.jws?wsdl', # Uberlandia - MG - '2170': 'http://udigital.uberlandia.mg.gov.br/WsNFe2/LoteRps.jws', + '5403': 'http://udigital.uberlandia.mg.gov.br/WsNFe2/LoteRps.jws', # São Luis - MA - '1314': 'https://stm.semfaz.saoluis.ma.gov.br/WsNFe2/LoteRps?wsdl', + '0921': 'https://stm.semfaz.saoluis.ma.gov.br/WsNFe2/LoteRps?wsdl', # Campo Grande - MS - '2218': 'http://issdigital.pmcg.ms.gov.br/WsNFe2/LoteRps.jws', + '2729': 'http://issdigital.pmcg.ms.gov.br/WsNFe2/LoteRps.jws', } - return urls[str(cod_cidade)] + 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): @@ -75,6 +79,9 @@ def _send(certificado, method, **kwargs): 'received_xml': e.fault.faultstring, 'object': None } + except Exception as e: + print (response) + print (e) return { 'sent_xml': xml_send, @@ -83,11 +90,23 @@ def _send(certificado, method, **kwargs): } +def xml_enviar(certificado, **kwargs): + return _render(certificado, 'enviar', **kwargs) + + def enviar(certificado, **kwargs): + if "xml" not in kwargs: + kwargs['xml'] = xml_enviar(certificado, **kwargs) return _send(certificado, 'enviar', **kwargs) +def xml_teste_enviar(certificado, **kwargs): + return _render(certificado, 'testeEnviar', **kwargs) + + def teste_enviar(certificado, **kwargs): + if "xml" not in kwargs: + kwargs['xml'] = xml_teste_enviar(certificado, **kwargs) return _send(certificado, 'testeEnviar', **kwargs) @@ -99,5 +118,11 @@ def consulta_lote(**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) diff --git a/pytrustnfe/nfse/dsf/templates/consultarNFSeRps.xml b/pytrustnfe/nfse/dsf/templates/consultarNFSeRps.xml new file mode 100644 index 00000000..a6a51bc5 --- /dev/null +++ b/pytrustnfe/nfse/dsf/templates/consultarNFSeRps.xml @@ -0,0 +1,22 @@ + + + {{ nfse.cidade }} + {{ nfse.cpf_cnpj }} + true + 1 + + + {% for rps in nfse.lista_rps -%} + + + {{ rps.prestador.inscricao_municipal }} + {{ rps.numero }} + {{ rps.serie_prestacao }} + + + {% endfor %} + + \ No newline at end of file diff --git a/pytrustnfe/xml/__init__.py b/pytrustnfe/xml/__init__.py index 0ed0ee52..413df5ea 100644 --- a/pytrustnfe/xml/__init__.py +++ b/pytrustnfe/xml/__init__.py @@ -27,7 +27,6 @@ def render_xml(path, template_name, remove_empty, **nfe): env.filters["format_date"] = filters.format_date template = env.get_template(template_name) - xml = template.render(**nfe) parser = etree.XMLParser(remove_blank_text=True, remove_comments=True, strip_cdata=False) From 7d4f9079c8b14e4dd9cd1519014eb1e779a68248 Mon Sep 17 00:00:00 2001 From: pal0schi <31492998+pal0schi@users.noreply.github.com> Date: Fri, 5 Jan 2018 11:22:42 -0200 Subject: [PATCH 27/31] =?UTF-8?q?*=20corre=C3=A7=C3=A3o=20da=20url=20de=20?= =?UTF-8?q?campo=20grande-ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfse/dsf/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pytrustnfe/nfse/dsf/__init__.py b/pytrustnfe/nfse/dsf/__init__.py index 60273d06..df4ce4a4 100644 --- a/pytrustnfe/nfse/dsf/__init__.py +++ b/pytrustnfe/nfse/dsf/__init__.py @@ -45,7 +45,7 @@ def _get_url(**kwargs): # 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', + '2729': 'http://issdigital.pmcg.ms.gov.br/WsNFe2/LoteRps.jws?wsdl', } try: @@ -62,6 +62,7 @@ def _send(certificado, method, **kwargs): xml_send = _render(path, method, **kwargs) client = get_client(url) + response = False if certificado: cert, key = extract_cert_and_key_from_pfx( @@ -80,8 +81,10 @@ def _send(certificado, method, **kwargs): 'object': None } except Exception as e: - print (response) - print (e) + if response: + raise Exception(response) + else: + raise e return { 'sent_xml': xml_send, From 78b0e47dfb1f3050021d1391a3e014b58d323e7e Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Thu, 1 Feb 2018 01:00:36 -0200 Subject: [PATCH 28/31] =?UTF-8?q?Finalizado=20a=20parte=20de=20emiss=C3=A3?= =?UTF-8?q?o=20de=20NFSe=20Floripa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfse/floripa/__init__.py | 16 ++++------------ setup.py | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pytrustnfe/nfse/floripa/__init__.py b/pytrustnfe/nfse/floripa/__init__.py index 7dceb102..1a5de249 100644 --- a/pytrustnfe/nfse/floripa/__init__.py +++ b/pytrustnfe/nfse/floripa/__init__.py @@ -50,17 +50,12 @@ def _get_oauth_token(**kwargs): def _send(certificado, method, **kwargs): if kwargs['ambiente'] == 'producao': - url = 'https://nfps-e.pmf.sc.gov.br/api/v1/processamento/notas/processa' + url = 'https://nfps-e.pmf.sc.gov.br/api/v1/processamento/notas/processa' #noqa else: url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/processamento/notas/processa' xml_send = kwargs['xml'] - base = dict( - ambiente='homologacao', client_id="trustcode-tecnologia-client", - secret_id="", username="", - password="" - ) token = _get_oauth_token(**kwargs) kwargs.update({"numero": 1, 'access_token': token["access_token"]}) @@ -68,16 +63,13 @@ def _send(certificado, method, **kwargs): headers = {"Accept": "application/xml", "Authorization": "Bearer %s" % kwargs['access_token']} r = requests.post(url, headers=headers, data=xml_send) - print(r.status_code) - if r.status_code != 200: - raise Exception(r.text) - print(r.text) - response, obj = sanitize_response(r.text) + response, obj = sanitize_response(r.text.strip().encode('utf-8')) return { 'sent_xml': xml_send, 'received_xml': response, - 'object': obj + 'object': obj, + 'status_code': r.status_code, } diff --git a/setup.py b/setup.py index 2ebafd05..b39eb643 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.9.2" +VERSION = "0.9.5" setup( name="PyTrustNFe3", From 7d6aacb6550108be9337a9f63b62f910d7348c68 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Thu, 1 Feb 2018 10:31:13 -0200 Subject: [PATCH 29/31] =?UTF-8?q?Incremento=20de=20vers=C3=B5es=20das=20de?= =?UTF-8?q?pendencias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 8 ++++---- setup.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 37ad7fd1..abb08363 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -lxml >= 3.5.0, < 4 +lxml >= 3.5.0, < 5 coveralls Jinja2 signxml urllib3 >= 1.22 suds-jurko >= 0.6 suds-jurko-requests >= 1.1 -defusedxml >= 0.4.1, < 0.6 -eight >= 0.3.0, < 0.5 -cryptography >= 1.8, < 2.1 +defusedxml >= 0.4.1, < 1 +eight >= 0.3.0, < 1 +cryptography >= 1.8, < 3 pyOpenSSL >= 16.0.0, < 18 certifi >= 2015.11.20.1 xmlsec >= 1.3.3 diff --git a/setup.py b/setup.py index cb097dfb..9406a2f5 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.9.5" +VERSION = "0.9.6" setup( name="PyTrustNFe3", @@ -42,7 +42,7 @@ install_requires=[ 'Jinja2 >= 2.8', 'signxml >= 2.4.0', - 'lxml >= 3.5.0, < 4', + 'lxml >= 3.5.0, < 5', 'suds-jurko >= 0.6', 'suds-jurko-requests >= 1.1', 'reportlab' From 1f56dd5fa76f0023299f3ae3c889dad1735660c9 Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Mon, 5 Feb 2018 15:49:54 -0200 Subject: [PATCH 30/31] =?UTF-8?q?Retorna=20uma=20exce=C3=A7=C3=A3o=20corre?= =?UTF-8?q?ta=20quando=20n=C3=A3o=20for=20poss=C3=ADvel=20obter=20o=20toke?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfse/floripa/__init__.py | 12 ++++++++---- setup.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pytrustnfe/nfse/floripa/__init__.py b/pytrustnfe/nfse/floripa/__init__.py index 1a5de249..10ca2115 100644 --- a/pytrustnfe/nfse/floripa/__init__.py +++ b/pytrustnfe/nfse/floripa/__init__.py @@ -45,7 +45,7 @@ def _get_oauth_token(**kwargs): if r.status_code == 200: return r.json() else: - return r.text + return r.json() def _send(certificado, method, **kwargs): @@ -57,7 +57,9 @@ def _send(certificado, method, **kwargs): 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", @@ -94,8 +96,10 @@ def cancelar_nota(certificado, **kwargs): def consultar_nota(certificado, **kwargs): - url = "https://nfps-e-hml.pmf.sc.gov.br/api/v1/consultas/notas/numero/%s" % (kwargs["numero"]) - url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/consultas/notas/prestador/24158233000185?pagina=1' + 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']} diff --git a/setup.py b/setup.py index 9406a2f5..cc20e846 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # coding=utf-8 from setuptools import setup, find_packages -VERSION = "0.9.6" +VERSION = "0.9.7" setup( name="PyTrustNFe3", From 55031fe78d255d1e4f6dad495483f84fb4e089fc Mon Sep 17 00:00:00 2001 From: Danimar Ribeiro Date: Mon, 5 Feb 2018 20:42:06 -0200 Subject: [PATCH 31/31] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20do=20cancelamen?= =?UTF-8?q?to=20de=20NFSe=20-=20Floripa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pytrustnfe/nfse/floripa/__init__.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pytrustnfe/nfse/floripa/__init__.py b/pytrustnfe/nfse/floripa/__init__.py index 10ca2115..a8282d35 100644 --- a/pytrustnfe/nfse/floripa/__init__.py +++ b/pytrustnfe/nfse/floripa/__init__.py @@ -10,6 +10,17 @@ 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') @@ -49,11 +60,7 @@ def _get_oauth_token(**kwargs): def _send(certificado, method, **kwargs): - if kwargs['ambiente'] == 'producao': - url = 'https://nfps-e.pmf.sc.gov.br/api/v1/processamento/notas/processa' #noqa - else: - url = 'https://nfps-e-hml.pmf.sc.gov.br/api/v1/processamento/notas/processa' - + url = URLS[kwargs['ambiente']][method] xml_send = kwargs['xml'] token = _get_oauth_token(**kwargs) @@ -62,7 +69,8 @@ def _send(certificado, method, **kwargs): token["message"])) kwargs.update({"numero": 1, 'access_token': token["access_token"]}) - headers = {"Accept": "application/xml", + 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)