diff --git a/pytrustnfe/servicos/NfeStatusServico.py b/pytrustnfe/servicos/NfeStatusServico.py index fd03ec4..a88839a 100644 --- a/pytrustnfe/servicos/NfeStatusServico.py +++ b/pytrustnfe/servicos/NfeStatusServico.py @@ -4,9 +4,7 @@ Created on 21/06/2015 @author: danimar ''' -from pytrustnfe.servicos.Comunicacao import Comunicacao -from pytrustnfe.xml import DynamicXml - +from pytrustnfe.servicos.comunicacao import Comunicacao class NfeStatusServico(Comunicacao): diff --git a/pytrustnfe/servicos/RecepcaoEvento.py b/pytrustnfe/servicos/RecepcaoEvento.py index 487c73c..7ea125e 100644 --- a/pytrustnfe/servicos/RecepcaoEvento.py +++ b/pytrustnfe/servicos/RecepcaoEvento.py @@ -5,11 +5,10 @@ Created on 21/06/2015 @author: danimar ''' from pytrustnfe.servicos.Comunicacao import Comunicacao -from pytrustnfe.xml import DynamicXml class RecepcaoEvento(Comunicacao): - + def registrar_evento(self, evento): xml = self._validar_xml(recibo) @@ -18,4 +17,4 @@ class RecepcaoEvento(Comunicacao): self.web_service = 'ws/recepcaoevento/recepcaoevento.asmx' self.url = 'nfe.sefazrs.rs.gov.br' - return self._executar_consulta(xml) \ No newline at end of file + return self._executar_consulta(xml) diff --git a/pytrustnfe/servicos/Validacao.py b/pytrustnfe/servicos/Validacao.py index 7ba0014..ce5ecdb 100644 --- a/pytrustnfe/servicos/Validacao.py +++ b/pytrustnfe/servicos/Validacao.py @@ -8,8 +8,8 @@ def validar_schema(): arquivo_esquema = '' xml = tira_abertura(self.xml).encode('utf-8') - esquema = etree.XMLSchema(etree.parse(arquivo_esquema)) + esquema = etree.XMLSchema(etree.parse(arquivo_esquema)) esquema.validate(etree.fromstring(xml)) namespace = '{http://www.portalfiscal.inf.br/nfe}' - return "\n".join([x.message.replace(namespace, '') for x in esquema.error_log]) \ No newline at end of file + return "\n".join([x.message.replace(namespace, '') for x in esquema.error_log]) diff --git a/pytrustnfe/servicos/Assinatura.py b/pytrustnfe/servicos/assinatura.py similarity index 63% rename from pytrustnfe/servicos/Assinatura.py rename to pytrustnfe/servicos/assinatura.py index 92df75b..2d3b9c9 100644 --- a/pytrustnfe/servicos/Assinatura.py +++ b/pytrustnfe/servicos/assinatura.py @@ -1,14 +1,57 @@ -#coding=utf-8 +# coding=utf-8 ''' Created on Jun 14, 2015 @author: danimar ''' -import xmlsec, libxml2 + +import xmlsec +import libxml2 import os.path +from signxml import xmldsig +from signxml import methods +from lxml import etree NAMESPACE_SIG = 'http://www.w3.org/2000/09/xmldsig#' + +def recursively_empty(e): + if e.text: + return False + return all((recursively_empty(c) for c in e.iterchildren())) + + +def assinar(xml, cert, key, reference, pfx, senha): + context = etree.iterwalk(xml) + for action, elem in context: + parent = elem.getparent() + if recursively_empty(elem): + parent.remove(elem) + + element = xml.find('{' + xml.nsmap[None] + '}NFe') + not_signed = etree.tostring(element) + not_signed = "]>" + \ + not_signed + + signer = xmldsig(element, digest_algorithm=u'sha1') + ns = {} + ns[None] = signer.namespaces['ds'] + signer.namespaces = ns + signed_root = signer.sign( + key=str(key), cert=cert, reference_uri=reference, + algorithm="rsa-sha1", method=methods.enveloped, + c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315') + + xmldsig(signed_root, digest_algorithm=u'sha1').verify(x509_cert=cert) + # signature = Assinatura(pfx, senha) + # xmlsec = signature.assina_xml(not_signed, reference) + # xmlsec = xmlsec.replace(""" +# ]>\n""", "") + return etree.tostring(signed_root) + # , xmlsec + + class Assinatura(object): def __init__(self, arquivo, senha): @@ -34,14 +77,13 @@ class Assinatura(object): libxml2.cleanupParser() - - def assina_xml(self, xml): + def assina_xml(self, xml, reference): self._checar_certificado() self._inicializar_cripto() try: doc_xml = libxml2.parseMemory(xml.encode('utf-8'), len(xml.encode('utf-8'))) - + import ipdb; ipdb.set_trace() signNode = xmlsec.TmplSignature(doc_xml, xmlsec.transformInclC14NId(), xmlsec.transformRsaSha1Id(), None) @@ -49,7 +91,7 @@ class Assinatura(object): doc_xml.getRootElement().addChild(signNode) refNode = signNode.addReference( xmlsec.transformSha1Id(), - None, '#NFe43150602261542000143550010000000761792265342', None) + None, reference, None) refNode.addTransform(xmlsec.transformEnvelopedId()) refNode.addTransform(xmlsec.transformInclC14NId()) diff --git a/pytrustnfe/servicos/Comunicacao.py b/pytrustnfe/servicos/comunicacao.py similarity index 82% rename from pytrustnfe/servicos/Comunicacao.py rename to pytrustnfe/servicos/comunicacao.py index d2745c9..99ed9d9 100644 --- a/pytrustnfe/servicos/Comunicacao.py +++ b/pytrustnfe/servicos/comunicacao.py @@ -7,7 +7,6 @@ Created on Jun 14, 2015 from lxml import objectify from uuid import uuid4 -from pytrustnfe.xml.DynamicXml import DynamicXml from pytrustnfe.HttpClient import HttpClient from pytrustnfe.Certificado import converte_pfx_pem @@ -25,9 +24,9 @@ class Comunicacao(object): metodo = '' tag_retorno = '' - def __init__(self, certificado, senha): - self.certificado = certificado - self.senha = senha + def __init__(self, cert, key): + self.certificado = cert + self.senha = key def _soap_xml(self, body): xml = ''' @@ -66,20 +65,15 @@ class Comunicacao(object): assert self.metodo != '', "Método não configurado" assert self.tag_retorno != '', "Tag de retorno não configurado" - def _validar_xml(self, obj): - xml = None - if isinstance(obj, DynamicXml): - xml = obj.render() - if isinstance(obj, basestring): - xml = obj - assert xml is not None, "Objeto deve ser do tipo DynamicXml ou string" - return xml + def _validar_nfe(self, obj): + if not isinstance(obj, dict): + raise u"Objeto deve ser um dicionário de valores" def _executar_consulta(self, xmlEnviar): self._validar_dados() - chave, certificado = self._preparar_temp_pem() + # chave, certificado = self._preparar_temp_pem() - client = HttpClient(self.url, chave, certificado) + client = HttpClient(self.url, self.certificado, self.senha) soap_xml = self._soap_xml(xmlEnviar) xml_retorno = client.post_xml(self.web_service, soap_xml) diff --git a/pytrustnfe/servicos/nfe_autorizacao.py b/pytrustnfe/servicos/nfe_autorizacao.py index 2e6aed4..d7595d6 100644 --- a/pytrustnfe/servicos/nfe_autorizacao.py +++ b/pytrustnfe/servicos/nfe_autorizacao.py @@ -4,17 +4,23 @@ Created on 21/06/2015 @author: danimar ''' -from pytrustnfe.servicos.Comunicacao import Comunicacao +from lxml import etree +from pytrustnfe.servicos.comunicacao import Comunicacao from pytrustnfe import utils +from pytrustnfe.xml import render_xml +from pytrustnfe.servicos.assinatura import assinar class NfeAutorizacao(Comunicacao): - def __init__(self, certificado, senha): + def __init__(self, cert, key, certificado, senha): Comunicacao.__init__(self, certificado, senha) + self.cert = cert + self.key = key def autorizar_nfe(self, nfe): - xml = self._validar_xml(nfe) + self._validar_nfe(nfe) + xml = render_xml('nfeEnv.xml', **nfe) self.metodo = 'NFeAutorizacao' self.tag_retorno = 'retEnviNFe' @@ -23,8 +29,13 @@ class NfeAutorizacao(Comunicacao): return self._executar_consulta(xml) - def autorizar_nfe_e_recibo(self, nfe): - xml = self._validar_xml(nfe) + def autorizar_nfe_e_recibo(self, nfe, id): + self._validar_nfe(nfe) + xml = render_xml('nfeEnv.xml', **nfe) + + return assinar(xml, self.cert, self.key, + '#%s' % id, + self.certificado, self.senha) self.metodo = 'NFeAutorizacao' self.tag_retorno = 'retEnviNFe' @@ -34,7 +45,7 @@ class NfeAutorizacao(Comunicacao): xml_recibo, recibo = self._executar_consulta(xml) consulta_recibo = utils.gerar_consulta_recibo(recibo) - xml = self._validar_xml(nfe) + self._validar_nfe(nfe) self.metodo = 'NFeRetAutorizacao' self.tag_retorno = 'retConsReciNFe' diff --git a/pytrustnfe/utils.py b/pytrustnfe/utils.py index 8adafdd..933e4e3 100644 --- a/pytrustnfe/utils.py +++ b/pytrustnfe/utils.py @@ -6,7 +6,6 @@ Created on 22/06/2015 ''' from datetime import date, datetime from pytrustnfe.ChaveNFe import ChaveNFe -from pytrustnfe.xml.DynamicXml import DynamicXml def date_tostring(data): diff --git a/pytrustnfe/xml/DynamicXml.py b/pytrustnfe/xml/DynamicXml.py deleted file mode 100644 index 124c939..0000000 --- a/pytrustnfe/xml/DynamicXml.py +++ /dev/null @@ -1,76 +0,0 @@ -# coding=utf-8 -''' -Created on Jun 17, 2015 - -@author: danimar -''' - -import xml.etree.ElementTree as ET -from lxml.etree import Element, tostring -from __builtin__ import str - - -class DynamicXml(object): - - def __getattr__(self, name): - try: - return object.__getattribute__(self, name) - except: - self.__setattr__(name, None) - return object.__getattribute__(self, name) - - def __setattr__(self, obj, val): - if(obj == "value" or obj == "atributos" or obj == "_indice"): - object.__setattr__(self, obj, val) - else: - self._indice = self._indice + 1 - object.__setattr__(self, obj, DynamicXml(val, self._indice)) - - def __init__(self, value, indice=0): - self.value = unicode(value, 'utf-8') if isinstance(value, - str) else value - self.atributos = {} - self._indice = indice - - def __str__(self): - return unicode(self.value) - - def __call__(self, *args, **kw): - if(len(kw) > 0): - self.atributos = kw - if(len(args) > 0): - self.value = args[0] - else: - return self.value - - def __getitem__(self, i): - if not isinstance(self.value, list): - self.value = [] - if(i + 1 > len(self.value)): - self.value.append(DynamicXml(None)) - return self.value[i] - - def render(self, pretty_print=False): - root = Element(self.value) - self._gerar_xml(root, self) - return tostring(root, pretty_print=pretty_print) - - def _gerar_xml(self, xml, objeto): - items = sorted( - objeto.__dict__.items(), - key=lambda x: x[1]._indice if isinstance(x[1], DynamicXml) else 0 - ) - for attr, value in items: - if(attr != "value" and attr != "atributos" and attr != "_indice"): - if isinstance(value(), list): - for item in value(): - sub = ET.SubElement(xml, attr) - self._gerar_xml(sub, item) - else: - sub = ET.SubElement(xml, attr) - if(unicode(value) != u"None"): - sub.text = unicode(value) - self._gerar_xml(sub, value) - elif(attr == "atributos"): - for atr, val in value.items(): - xml.set(atr.replace("__", ":"), str(val)) diff --git a/pytrustnfe/xml/__init__.py b/pytrustnfe/xml/__init__.py index e69de29..759854f 100644 --- a/pytrustnfe/xml/__init__.py +++ b/pytrustnfe/xml/__init__.py @@ -0,0 +1,22 @@ +import os.path +from lxml import etree +from jinja2 import Environment, FileSystemLoader +from . import filters + + +def render_xml(template_name, **nfe): + path = os.path.dirname(__file__) + env = Environment( + loader=FileSystemLoader(path), extensions=['jinja2.ext.with_']) + + env.filters["normalize"] = filters.normalize_str + env.filters["format_percent"] = filters.format_percent + env.filters["format_datetime"] = filters.format_datetime + env.filters["format_date"] = filters.format_date + + template = env.get_template(template_name) + + xml = template.render(**nfe) + xml = xml.replace('&', '&') + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) + return etree.fromstring(xml, parser=parser) diff --git a/pytrustnfe/xml/filters.py b/pytrustnfe/xml/filters.py new file mode 100644 index 0000000..5f275fd --- /dev/null +++ b/pytrustnfe/xml/filters.py @@ -0,0 +1,58 @@ +# coding=utf-8 + +from decimal import Decimal +from datetime import datetime +import dateutil.parser as parser_dateutil + +from unicodedata import normalize + + +def normalize_str(string): + """ + Remove special characters and return the ascii string + """ + if string: + if not isinstance(string, unicode): + string = unicode(string, 'utf-8', 'replace') + + string = string.encode('utf-8') + return normalize( + 'NFKD', string.decode('utf-8')).encode('ASCII', 'ignore') + return '' + + +def format_percent(value): + if value: + return Decimal(value) / 100 + + +def format_datetime(value): + """ + Format datetime + """ + dt_format = '%Y-%m-%dT%H:%M:%I' + if isinstance(value, datetime): + return value.strftime(dt_format) + + try: + value = parser_dateutil.parse(value).strftime(dt_format) + except AttributeError: + pass + + return value + + +def format_date(value): + """ + Format date + """ + dt_format = '%Y-%m-%d' + if isinstance(value, datetime): + return value.strftime(dt_format) + + try: + value = parser_dateutil.parse(value).strftime(dt_format) + except AttributeError: + pass + + return value diff --git a/pytrustnfe/xml/nfeEnv.xml b/pytrustnfe/xml/nfeEnv.xml new file mode 100644 index 0000000..01e137f --- /dev/null +++ b/pytrustnfe/xml/nfeEnv.xml @@ -0,0 +1,174 @@ + + {{ idLote }} + {{ indSinc }} + {% for NFe in NFes %} + + + + {% with ide = NFe.infNFe.ide %} + {{ ide.cUF }} + {{ ide.cNF }} + {{ ide.natOp }} + {{ ide.indPag }} + {{ ide.mod }} + {{ ide.serie }} + {{ ide.nNF }} + {{ ide.dhEmi }} + {{ ide.dhSaiEnt }} + {{ ide.tpNF }} + {{ ide.idDest }} + {{ ide.cMunFG }} + {{ ide.tpImp }} + {{ ide.tpEmis }} + {{ ide.cDV }} + {{ ide.tpAmb }} + {{ ide.finNFe }} + {{ ide.indFinal }} + {{ ide.indPres }} + {{ ide.procEmi }} + Odoo Brasil 9.0 + {% endwith %} + + + {% with emit = NFe.infNFe.emit %} + {{ emit.CNPJ }} + {{ emit.xNome }} + {{ emit.xFant }} + + {{ emit.enderEmit.xLgr }} + {{ emit.enderEmit.nro }} + {{ emit.enderEmit.xBairro }} + {{ emit.enderEmit.cMun }} + {{ emit.enderEmit.xMun }} + {{ emit.enderEmit.UF }} + {{ emit.enderEmit.CEP }} + {{ emit.enderEmit.cPais }} + {{ emit.enderEmit.xPais }} + {{ emit.enderEmit.fone }} + + {{ emit.IE }} + {{ emit.CRT }} + {% endwith %} + + + {% with dest = NFe.infNFe.dest %} + {{ dest.CNPJ }} + {{ dest.CPF }} + {{ dest.xNome }} + + {{ dest.enderDest.xLgr }} + {{ dest.enderDest.nro }} + {{ dest.enderDest.xBairro }} + {{ dest.enderDest.cMun }} + {{ dest.enderDest.xMun }} + {{ dest.enderDest.UF }} + {{ dest.enderDest.CEP }} + {{ dest.enderDest.cPais }} + {{ dest.enderDest.xPais }} + {{ dest.enderDest.fone }} + + {{ dest.indIEDest }} + {{ dest.IE }} + {% endwith %} + + {% for det in NFe.infNFe.detalhes %} + + + {% with prod = det.prod %} + {{ prod.cProd }} + {{ prod.cEAN }} + {{ prod.xProd }} + {{ prod.NCM }} + {{ prod.CFOP }} + {{ prod.uCom }} + {{ prod.qCom }} + {{ prod.vUnCom }} + {{ prod.vProd }} + {{ prod.cEANTrib }} + {{ prod.uTrib }} + {{ prod.qTrib }} + {{ prod.vUnTrib }} + {{ prod.indTot }} + {% endwith %} + + + {% with imposto = det.imposto %} + {{ imposto.vTotTrib }} + + + {{ imposto.ICMS.ICMS00.orig }} + {{ imposto.ICMS.ICMS00.CST }} + {{ imposto.ICMS.ICMS00.modBC }} + {{ imposto.ICMS.ICMS00.vBC }} + {{ imposto.ICMS.ICMS00.pICMS }} + {{ imposto.ICMS.ICMS00.vICMS }} + + + + {{ imposto.IPI.cEnq }} + + {{ imposto.IPI.IPITrib.CST }} + {{ imposto.IPI.IPITrib.vBC }} + {{ imposto.IPI.IPITrib.pIPI }} + {{ imposto.IPI.IPITrib.vIPI }} + + + + + {{ imposto.PIS.PISAliq.CST }} + {{ imposto.PIS.PISAliq.vBC }} + {{ imposto.PIS.PISAliq.pPIS }} + {{ imposto.PIS.PISAliq.vPIS }} + + + + + {{ imposto.COFINS.COFINSAliq.CST }} + {{ imposto.COFINS.COFINSAliq.vBC }} + {{ imposto.COFINS.COFINSAliq.pCOFINS }} + {{ imposto.COFINS.COFINSAliq.vCOFINS }} + + + {% endwith %} + + + {% endfor %} + + {% with total = NFe.infNFe.total %} + + {{ total.vBC }} + {{ total.vICMS }} + {{ total.vICMSDeson }} + {{ total.vBCST }} + {{ total.vST }} + {{ total.vProd }} + {{ total.vFrete }} + {{ total.vSeg }} + {{ total.vDesc }} + {{ total.vII }} + {{ total.vIPI }} + {{ total.vPIS }} + {{ total.vCOFINS }} + {{ total.vOutro }} + {{ total.vNF }} + {{ total.vTotTrib }} + + {% endwith %} + + + {{ NFe.infNFe.transp.modFrete }} + + + + 339/1 + 2016-06-02 + 8611.76 + + + + {{ NFe.infNFe.infAdic.infCpl }} + + + + {% endfor %} + diff --git a/setup.py b/setup.py index bf50a14..04fa4a2 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ setup( 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=find_packages(exclude=['*test*']), + package_data={'pytrustnfe': ['xml/*xml']}, url='https://github.com/danimaribeiro/PyNfeTrust', license='LGPL-v2.1+', description='PyNfeTrust é uma biblioteca para envio de NF-e',