From 9624c61f73149ab66c128a77914213f56501d6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Mon, 18 Jan 2010 15:30:58 -0200 Subject: [PATCH 1/7] Ajustes gerais --- README | 3 +++ pynfe/processamento/__init__.py | 4 ++-- pynfe/processamento/comunicacao.py | 33 +++++++++++++++++++++++++++++- run_fake_soap_server.py | 11 +++++----- tests/01-basico.txt | 2 +- tests/03-processamento-03-assinatura.txt | 2 +- tests/03-processamento-04-comunicacao.txt | 5 +++++ tests/04-servidor-soap.txt | 17 +++++++++++---- tests/certificado.pfx | Bin 0 -> 4590 bytes 9 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 tests/03-processamento-04-comunicacao.txt create mode 100644 tests/certificado.pfx diff --git a/README b/README index f441e60..2ae896f 100644 --- a/README +++ b/README @@ -83,3 +83,6 @@ Referências - Lista de codigos para campo EX TIPI - http://www.fisconet.com.br/ipi/tipi/04.htm +- Certificado para testes + - http://nf-eletronica.com/blog/?p=133 + diff --git a/pynfe/processamento/__init__.py b/pynfe/processamento/__init__.py index da1622f..86cae50 100644 --- a/pynfe/processamento/__init__.py +++ b/pynfe/processamento/__init__.py @@ -1,6 +1,6 @@ -from interfaces import InterfaceXML +from serializacao import SerializacaoXML from validacao import Validacao from assinatura import AssinaturaA1 -from comunicacao import Comunicacao +from comunicacao import ComunicacaoSefaz from danfe import DANFE diff --git a/pynfe/processamento/comunicacao.py b/pynfe/processamento/comunicacao.py index 449e8b1..031be29 100644 --- a/pynfe/processamento/comunicacao.py +++ b/pynfe/processamento/comunicacao.py @@ -1,3 +1,34 @@ +# -*- coding: utf-8 -*- + class Comunicacao(object): - pass + u"""Classe abstrata responsavel por definir os metodos e logica das classes + de comunicação com os webservices da NF-e.""" + + servidor = None + porta = None + + def __init__(self, servidor, porta): + self.servidor = servidor + self.porta = porta + +class ComunicacaoSefaz(Comunicacao): + u"""Classe de comunicação que segue o padrão definido para as SEFAZ dos Estados.""" + + def transmitir(self, nota_fiscal): + pass + + def cancelar(self, nota_fiscal): + pass + + def situacao_nfe(self, nota_fiscal): + pass + + def status_servico(self): + pass + + def consultar_cadastro(self, instancia): + pass + + def inutilizar_faixa_numeracao(self, faixa): + pass diff --git a/run_fake_soap_server.py b/run_fake_soap_server.py index 77a3ad2..c760285 100644 --- a/run_fake_soap_server.py +++ b/run_fake_soap_server.py @@ -1,12 +1,13 @@ from soaplib.wsgi_soap import SimpleWSGISoapApp from soaplib.service import soapmethod +from soaplib.serializers.primitive import String, Integer, Array, Null class ServidorNFEFalso(SimpleWSGISoapApp): - from soaplib.serializers.primitive import String, Integer, Array, Null - - @soapmethod(String, Integer, _returns=String) - def ping(self, palavra, vezes): - return ','.join([palavra for i in range(vezes)]) + @soapmethod(String, Integer, _returns=Array(String)) + def ping(self, nome, vezes): + ret = [nome for i in range(vezes)] + print ret + return ret if __name__ == '__main__': porta = 8080 diff --git a/tests/01-basico.txt b/tests/01-basico.txt index d597861..ffc1211 100644 --- a/tests/01-basico.txt +++ b/tests/01-basico.txt @@ -91,7 +91,7 @@ Os pacotes da biblioteca sao: >>> from pynfe import processamento >>> set([attr for attr in dir(processamento) if not attr.startswith('__')]) == set([ - ... 'AssinaturaA1', 'Comunicacao', 'DANFE', 'InterfaceXML', 'Validacao', + ... 'AssinaturaA1', 'ComunicacaoSefaz', 'DANFE', 'InterfaceXML', 'Validacao', ... 'assinatura', 'comunicacao', 'danfe', 'interfaces', 'validacao']) True diff --git a/tests/03-processamento-03-assinatura.txt b/tests/03-processamento-03-assinatura.txt index c17ac1f..407288a 100644 --- a/tests/03-processamento-03-assinatura.txt +++ b/tests/03-processamento-03-assinatura.txt @@ -15,7 +15,7 @@ Assinando NF-e Na hora de assinar, selecionar um Certificado Digital - >>> assinatura = AssinaturaA1(certificado, 'senha') + >>> assinatura = AssinaturaA1(certificado, senha='associacao') TODO: A senha deveria ser criptografada de forma a evitar que alguem entre nesse processo e a capture. diff --git a/tests/03-processamento-04-comunicacao.txt b/tests/03-processamento-04-comunicacao.txt new file mode 100644 index 0000000..864a922 --- /dev/null +++ b/tests/03-processamento-04-comunicacao.txt @@ -0,0 +1,5 @@ +PROCESSAMENTO - COMUNICACAO +=========================== + + >>> from pynfe.processamento import ComunicacaoSefaz + diff --git a/tests/04-servidor-soap.txt b/tests/04-servidor-soap.txt index 5f9208a..d7d4cfd 100644 --- a/tests/04-servidor-soap.txt +++ b/tests/04-servidor-soap.txt @@ -5,9 +5,18 @@ Este teste vai verificar um servidor, executado atraves do comando 'run_fake_soap_server.py', para enviar requisicoes SOAP e esperar as responstas em formato WSDL, de forma a simular o servidor da SEFAZ. +Usando suds + >>> from suds.client import Client - >>> url = 'http://localhost:8080/ServidorNFEFalso?wsdl' - >>> #client = Client(url) - >>> #print client - >>> #print client.service.ping('mario', 5) + >>> client = Client('http://localhost:8080/ServidorNFEFalso?wsdl', cache=None) + >>> client.set_options(retxml=True) + >>> print client.service.ping('mario', 5) + +Usando soaplib + + >>> #from run_fake_soap_server import ServidorNFEFalso + >>> #from soaplib.client import ServiceClient, make_service_client + >>> #client = make_service_client('http://localhost:8080/', ServidorNFEFalso()) + >>> #print client.ping('Brasil', 5) + ['Brasil', 'Brasil', 'Brasil', 'Brasil', 'Brasil'] diff --git a/tests/certificado.pfx b/tests/certificado.pfx new file mode 100644 index 0000000000000000000000000000000000000000..428d6c49162da5c733ee3b64114d20de05fefa21 GIT binary patch literal 4590 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 literal 0 HcmV?d00001 From 7c5327770a6dc617a69a71bf7eddc37eaa6cdc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Mon, 18 Jan 2010 15:53:29 -0200 Subject: [PATCH 2/7] Ajustes nos testes --- tests/01-basico.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/01-basico.txt b/tests/01-basico.txt index ffc1211..c751688 100644 --- a/tests/01-basico.txt +++ b/tests/01-basico.txt @@ -91,8 +91,8 @@ Os pacotes da biblioteca sao: >>> from pynfe import processamento >>> set([attr for attr in dir(processamento) if not attr.startswith('__')]) == set([ - ... 'AssinaturaA1', 'ComunicacaoSefaz', 'DANFE', 'InterfaceXML', 'Validacao', - ... 'assinatura', 'comunicacao', 'danfe', 'interfaces', 'validacao']) + ... 'AssinaturaA1', 'ComunicacaoSefaz', 'DANFE', 'SerializacaoXML', 'Validacao', + ... 'assinatura', 'comunicacao', 'danfe', 'serializacao', 'validacao']) True Ha ainda uma pasta dentro da pasta 'pynfe', chamada 'data', que deve @@ -113,28 +113,28 @@ Geracao e importacao de XML As objetos Python devem ser traduzidos para arquivos XML e o caminho inverso, de introspecao, tambem deve ser feito, atraves de uma fabrica. - >>> from pynfe.processamento.interfaces import Interface + >>> from pynfe.processamento.serializacao import Serializacao - >>> bool(Interface.exportar) + >>> bool(Serializacao.exportar) True - >>> bool(Interface.importar) + >>> bool(Serializacao.importar) True -A classe basica de Interface eh abstrata, ou seja, nao pode ser +A classe basica de Serializacao eh abstrata, ou seja, nao pode ser instanciada diretamente. >>> lista_de_nfs = [] >>> try: - ... Interface(lista_de_nfs) + ... Serializacao(lista_de_nfs) ... except Exception, e: ... print e.message Esta classe nao pode ser instanciada diretamente! -Classe de interface especifica para XML, usando lxml +Classe de serializacao especifica para XML, usando lxml - >>> from pynfe.processamento.interfaces import InterfaceXML + >>> from pynfe.processamento.serializacao import SerializacaoXML Esse procedimento sera feito por padrao atraves da lxml, devido ao seu desempenho, mas pode ser extendido para outros formatos, dependendo do From 36d05d8bcd45283fabbc263d1fe708051d84577d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Mon, 18 Jan 2010 15:55:53 -0200 Subject: [PATCH 3/7] Ajuste no teste de serializacao --- tests/03-processamento-01-serializacao-xml.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/03-processamento-01-serializacao-xml.txt b/tests/03-processamento-01-serializacao-xml.txt index dcd4a02..c80a2ee 100644 --- a/tests/03-processamento-01-serializacao-xml.txt +++ b/tests/03-processamento-01-serializacao-xml.txt @@ -8,7 +8,7 @@ Populando fonte de dados >>> from decimal import Decimal >>> from pynfe.entidades import Emitente, Cliente, NotaFiscal, Transportadora >>> from pynfe.entidades.notafiscal import NotaFiscalEntregaRetirada - >>> from pynfe.entidades.fontes_dados import _fonte_dados + >>> from pynfe.entidades.fonte_dados import _fonte_dados >>> from pynfe.utils.flags import CODIGO_BRASIL Popula dependentes da NF From 7af49e80c34db625632ba3258da316706c477691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Mon, 18 Jan 2010 15:57:04 -0200 Subject: [PATCH 4/7] Ajuste na importacao da fonte de dados --- pynfe/entidades/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynfe/entidades/base.py b/pynfe/entidades/base.py index f56c745..152f0ea 100644 --- a/pynfe/entidades/base.py +++ b/pynfe/entidades/base.py @@ -11,7 +11,7 @@ class Entidade(object): # Adiciona o objeto à fonte de dados informada if not self._fonte_dados: - from fontes_dados import _fonte_dados + from fonte_dados import _fonte_dados self._fonte_dados = _fonte_dados self._fonte_dados.adicionar_objeto(self) From 8022a445a865d214e261f8301c87e0d622c07765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Mon, 18 Jan 2010 19:09:47 -0200 Subject: [PATCH 5/7] =?UTF-8?q?Trabalhando=20no=20modulo=20de=20comunica?= =?UTF-8?q?=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/entidades/certificado.py | 35 +++++++++++- pynfe/processamento/assinatura.py | 36 ++---------- pynfe/processamento/comunicacao.py | 92 +++++++++++++++++++++++++++++-- pynfe/processamento/serializacao.py | 23 +------- pynfe/utils/__init__.py | 26 +++++++++ pynfe/utils/flags.py | 4 ++ tests/03-processamento-03-assinatura.txt | 2 +- tests/03-processamento-04-comunicacao.txt | 18 ++++++ tests/04-servidor-soap.txt | 17 ++---- 9 files changed, 180 insertions(+), 73 deletions(-) diff --git a/pynfe/entidades/certificado.py b/pynfe/entidades/certificado.py index 9c34a5c..60be860 100644 --- a/pynfe/entidades/certificado.py +++ b/pynfe/entidades/certificado.py @@ -1,6 +1,10 @@ -# -*- coding; utf-8 -*- +# -*- coding: utf-8 -*- +import os + from base import Entidade +from OpenSSL import crypto + class Certificado(Entidade): u"""Classe abstrata responsavel por definir o modelo padrao para as demais classes de certificados digitais. @@ -15,13 +19,40 @@ class Certificado(Entidade): return super(Certificado, cls).__new__(cls, *args, **kwargs) class CertificadoA1(Certificado): - """Implementa a entidade do certificado eCNPJ A1, suportado pelo OpenSSL, + u"""Implementa a entidade do certificado eCNPJ A1, suportado pelo OpenSSL, e amplamente utilizado.""" caminho_arquivo = None conteudo_x509 = None + pasta_temporaria = '/tmp/' + arquivo_chave = 'key.pem' + arquivo_cert = 'cert.pem' def __init__(self, caminho_arquivo=None, conteudo_x509=None): self.caminho_arquivo = caminho_arquivo or self.caminho_arquivo self.conteudo_x509 = conteudo_x509 or self.conteudo_x509 + + def separar_arquivo(self, senha, caminho_chave=None, caminho_cert=None): + u"""Separa o arquivo de certificado em dois: de chave e de certificado, + em arquivos temporários separados""" + + caminho_chave = caminho_chave or os.path.join(self.pasta_temporaria, self.arquivo_chave) + caminho_cert = caminho_cert or os.path.join(self.pasta_temporaria, self.arquivo_cert) + + # Lendo o arquivo pfx no formato pkcs12 como binario + pkcs12 = crypto.load_pkcs12(file(self.caminho_arquivo, 'rb').read(), senha) + + # Retorna a string decodificado da chave privada + key_str = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkcs12.get_privatekey()) + + # Retorna a string decodificado do certificado + cert_str = crypto.dump_certificate(crypto.FILETYPE_PEM, pkcs12.get_certificate()) + + # Gravando a string no dicso + file(caminho_cert, 'wb').write(cert_str) + + # Gravando a string no dicso + file(caminho_chave, 'wb').write(key_str) + + return caminho_chave, caminho_cert diff --git a/pynfe/processamento/assinatura.py b/pynfe/processamento/assinatura.py index 5b5c0b9..27bb5ad 100644 --- a/pynfe/processamento/assinatura.py +++ b/pynfe/processamento/assinatura.py @@ -1,37 +1,11 @@ # -*- coding: utf-8 -*- -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -try: - from lxml import etree -except ImportError: - try: - # Python 2.5 - cElementTree - import xml.etree.cElementTree as etree - except ImportError: - try: - # Python 2.5 - ElementTree - import xml.etree.ElementTree as etree - except ImportError: - try: - # Instalacao normal do cElementTree - import cElementTree as etree - except ImportError: - try: - # Instalacao normal do ElementTree - import elementtree.ElementTree as etree - except ImportError: - raise Exception('Falhou ao importar lxml/ElementTree') - import xmlsec, libxml2 # FIXME: verificar ambiguidade de dependencias: lxml e libxml2 from geraldo.utils import memoize -NAMESPACE_NFE = u'http://www.portalfiscal.inf.br/nfe' -NAMESPACE_SIG = u'http://www.w3.org/2000/09/xmldsig#' +from pynfe.utils import etree, StringIO +from pynfe.utils.flags import NAMESPACE_NFE, NAMESPACE_SIG class Assinatura(object): """Classe abstrata responsavel por definir os metodos e logica das classes @@ -113,11 +87,10 @@ class AssinaturaA1(Assinatura): # Tag de assinatura if raiz.getroot().find('Signature') is None: signature = etree.Element( - 'Signature', + '{%s}Signature'%NAMESPACE_SIG, URI=raiz.getroot().getchildren()[0].attrib['Id'], - xmlns=NAMESPACE_SIG, + nsmap={'sig': NAMESPACE_SIG}, ) - signature.text = '' raiz.getroot().insert(0, signature) # Acrescenta a tag de doctype (como o lxml nao suporta alteracao do doctype, @@ -202,6 +175,7 @@ class AssinaturaA1(Assinatura): return resultado def _antes_de_assinar_ou_verificar(self, raiz): + raise Exception(dir(raiz)) # Converte etree para string xml = etree.tostring(raiz, xml_declaration=True, encoding='utf-8') diff --git a/pynfe/processamento/comunicacao.py b/pynfe/processamento/comunicacao.py index 031be29..27bfbec 100644 --- a/pynfe/processamento/comunicacao.py +++ b/pynfe/processamento/comunicacao.py @@ -1,19 +1,28 @@ # -*- coding: utf-8 -*- +from httplib import HTTPSConnection, HTTPResponse + +from pynfe.utils import etree, StringIO +from pynfe.utils.flags import NAMESPACE_NFE, NAMESPACE_SOAP + class Comunicacao(object): u"""Classe abstrata responsavel por definir os metodos e logica das classes de comunicação com os webservices da NF-e.""" servidor = None - porta = None + certificado = None + certificado_senha = None - def __init__(self, servidor, porta): + def __init__(self, servidor, certificado, certificado_senha): self.servidor = servidor - self.porta = porta + self.certificado = certificado + self.certificado_senha = certificado_senha class ComunicacaoSefaz(Comunicacao): u"""Classe de comunicação que segue o padrão definido para as SEFAZ dos Estados.""" + _versao = '1.01' + def transmitir(self, nota_fiscal): pass @@ -24,11 +33,84 @@ class ComunicacaoSefaz(Comunicacao): pass def status_servico(self): - pass + post = '/nfeweb/services/nfestatusservico.asmx' + + # Monta XML do corpo da requisição # FIXME + raiz = etree.Element('teste') + dados = etree.tostring(raiz) + + # Monta XML para envio da requisição + xml = self._construir_xml_soap( + metodo='CadConsultaCadastro', + tag_metodo='consultaCadastro', + cabecalho=self._cabecalho_soap(), + dados=dados, + ) + + # Chama método que efetua a requisição POST no servidor SOAP + retorno = self._post(post, xml, self._post_header()) + + # Transforma o retorno em etree + try: + retorno = etree.parse(StringIO(retorno)) + return retorno + except TypeError: + pass def consultar_cadastro(self, instancia): - pass + #post = '/nfeweb/services/cadconsultacadastro.asmx' + post = '/nfeweb/services/nfeconsulta.asmx' def inutilizar_faixa_numeracao(self, faixa): pass + def _cabecalho_soap(self): + u"""Monta o XML do cabeçalho da requisição SOAP""" + + raiz = etree.Element('cabecMsg', xmlns=NAMESPACE_NFE, versao="1.02") + etree.SubElement(raiz, 'versaoDados').text = self._versao + + return etree.tostring(raiz, encoding='utf-8', xml_declaration=True) + + def _construir_xml_soap(self, metodo, tag_metodo, cabecalho, dados): + u"""Mota o XML para o envio via SOAP""" + + raiz = etree.Element('{%s}Envelope'%NAMESPACE_SOAP, nsmap={'soap': NAMESPACE_SOAP}) + + body = etree.SubElement(raiz, '{%s}Body'%NAMESPACE_SOAP) + met = etree.SubElement( + body, tag_metodo, xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/%s"%metodo, + ) + + etree.SubElement(met, 'nfeCabecMsg').text = cabecalho + etree.SubElement(met, 'nfeDadosMsg').text = dados + + return etree.tostring(raiz, encoding='utf-8', xml_declaration=True) + + def _post_header(self): + u"""Retorna um dicionário com os atributos para o cabeçalho da requisição HTTP""" + return { + u'content-type': u'application/soap+xml; charset=utf-8', + u'Accept': u'application/soap+xml; charset=utf-8', + } + + def _post(self, post, xml, header): + # Separa arquivos de certificado para chave e certificado (sozinho) + caminho_chave, caminho_cert = self.certificado.separar_arquivo(senha=self.certificado_senha) + + # Abre a conexão HTTPS + con = HTTPSConnection(self.servidor, key_file=caminho_chave, cert_file=caminho_cert) + + try: + #con.set_debuglevel(100) + + con.request(u'POST', post, xml, header) + + resp = con.getresponse() + + # Tudo certo! + if resp.status == 200: + return resp.read() + finally: + con.close() + diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 22040c3..f519d4d 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -4,30 +4,9 @@ try: except: from sets import Set as set -try: - from lxml import etree -except ImportError: - try: - # Python 2.5 - cElementTree - import xml.etree.cElementTree as etree - except ImportError: - try: - # Python 2.5 - ElementTree - import xml.etree.ElementTree as etree - except ImportError: - try: - # Instalacao normal do cElementTree - import cElementTree as etree - except ImportError: - try: - # Instalacao normal do ElementTree - import elementtree.ElementTree as etree - except ImportError: - raise Exception('Falhou ao importar lxml/ElementTree') - from pynfe.entidades import Emitente, Cliente, Produto, Transportadora, NotaFiscal from pynfe.excecoes import NenhumObjetoEncontrado, MuitosObjetosEncontrados -from pynfe.utils import so_numeros, obter_municipio_por_codigo, obter_pais_por_codigo +from pynfe.utils import etree, so_numeros, obter_municipio_por_codigo, obter_pais_por_codigo from pynfe.utils.flags import CODIGOS_ESTADOS class Serializacao(object): diff --git a/pynfe/utils/__init__.py b/pynfe/utils/__init__.py index ef67cbd..b11ab9f 100644 --- a/pynfe/utils/__init__.py +++ b/pynfe/utils/__init__.py @@ -1,5 +1,31 @@ import os +try: + from lxml import etree +except ImportError: + try: + # Python 2.5 - cElementTree + import xml.etree.cElementTree as etree + except ImportError: + try: + # Python 2.5 - ElementTree + import xml.etree.ElementTree as etree + except ImportError: + try: + # Instalacao normal do cElementTree + import cElementTree as etree + except ImportError: + try: + # Instalacao normal do ElementTree + import elementtree.ElementTree as etree + except ImportError: + raise Exception('Falhou ao importar lxml/ElementTree') + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + import flags from geraldo.utils import memoize diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index f7054ba..77b2acb 100644 --- a/pynfe/utils/flags.py +++ b/pynfe/utils/flags.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- +NAMESPACE_NFE = 'http://www.portalfiscal.inf.br/nfe' +NAMESPACE_SIG = 'http://www.w3.org/2000/09/xmldsig#' +NAMESPACE_SOAP = 'http://www.w3.org/2003/05/soap-envelope' + TIPOS_DOCUMENTO = ( 'CNPJ', 'CPF', diff --git a/tests/03-processamento-03-assinatura.txt b/tests/03-processamento-03-assinatura.txt index 407288a..b564a24 100644 --- a/tests/03-processamento-03-assinatura.txt +++ b/tests/03-processamento-03-assinatura.txt @@ -6,7 +6,7 @@ Carregando Certificado Digital tipo A1 >>> from pynfe.entidades import CertificadoA1 - >>> certificado = CertificadoA1(caminho_arquivo='tests/certificado.pem') + >>> certificado = CertificadoA1(caminho_arquivo='tests/certificado.pfx') Assinando NF-e -------------- diff --git a/tests/03-processamento-04-comunicacao.txt b/tests/03-processamento-04-comunicacao.txt index 864a922..55c781d 100644 --- a/tests/03-processamento-04-comunicacao.txt +++ b/tests/03-processamento-04-comunicacao.txt @@ -3,3 +3,21 @@ PROCESSAMENTO - COMUNICACAO >>> from pynfe.processamento import ComunicacaoSefaz +Carregando certificado digital tipo A1 + + >>> from pynfe.entidades import CertificadoA1 + >>> certificado = CertificadoA1(caminho_arquivo='tests/certificado.pfx') + +Instancia de comunicacao + + >>> comunicacao = ComunicacaoSefaz( + ... #servidor='localhost:8080', + ... servidor='homologacao.nfe.fazenda.sp.gov.br', + ... certificado=certificado, + ... certificado_senha='associacao', + ... ) + +Verifica o status do servico + + >>> comunicacao.status_servico() + diff --git a/tests/04-servidor-soap.txt b/tests/04-servidor-soap.txt index d7d4cfd..6a202b3 100644 --- a/tests/04-servidor-soap.txt +++ b/tests/04-servidor-soap.txt @@ -7,16 +7,9 @@ responstas em formato WSDL, de forma a simular o servidor da SEFAZ. Usando suds - >>> from suds.client import Client - >>> client = Client('http://localhost:8080/ServidorNFEFalso?wsdl', cache=None) - >>> client.set_options(retxml=True) - >>> print client.service.ping('mario', 5) - -Usando soaplib - - >>> #from run_fake_soap_server import ServidorNFEFalso - >>> #from soaplib.client import ServiceClient, make_service_client - >>> #client = make_service_client('http://localhost:8080/', ServidorNFEFalso()) - >>> #print client.ping('Brasil', 5) - ['Brasil', 'Brasil', 'Brasil', 'Brasil', 'Brasil'] + >>> #from suds.client import Client + >>> #client = Client('http://localhost:8080/ServidorNFEFalso?wsdl', cache=None) + >>> #client.set_options(retxml=True) + >>> #bool(client.service.ping('mario', 5)) + True From 536aecc2739d27a2a3e9a241be0cd6a3d36dbe81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Mon, 18 Jan 2010 19:57:40 -0200 Subject: [PATCH 6/7] =?UTF-8?q?Trabalhando=20na=20comunica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/assinatura.py | 57 +++++++++++++++++++++----- pynfe/processamento/comunicacao.py | 4 +- pynfe/processamento/serializacao.py | 6 ++- pynfe/utils/flags.py | 2 + tests/03-processamento-01-serializacao-xml.txt | 2 +- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/pynfe/processamento/assinatura.py b/pynfe/processamento/assinatura.py index 27bb5ad..b9eacb8 100644 --- a/pynfe/processamento/assinatura.py +++ b/pynfe/processamento/assinatura.py @@ -61,16 +61,30 @@ class AssinaturaA1(Assinatura): """Classe abstrata responsavel por efetuar a assinatura do certificado digital no XML informado.""" - def assinar_arquivo(self, caminho_arquivo): + def assinar_arquivo(self, caminho_arquivo, salva=True): # Carrega o XML do arquivo raiz = etree.parse(caminho_arquivo) - return self.assinar_etree(raiz) + + # Efetua a assinatura + xml = self.assinar_etree(raiz, retorna_xml=True) + + raise Exception(xml) + + # Grava XML assinado no arquivo + if salva: + fp = file(caminho_arquivo, 'w') + fp.write(xml) + fp.close() + + return xml def assinar_xml(self, xml): raiz = etree.parse(StringIO(xml)) - return self.assinar_etree(raiz) - def assinar_etree(self, raiz): + # Efetua a assinatura + return self.assinar_etree(raiz, retorna_xml=True) + + def assinar_etree(self, raiz, retorna_xml=False): # Extrai a tag do elemento raiz tipo = extrair_tag(raiz.getroot()) @@ -91,6 +105,24 @@ class AssinaturaA1(Assinatura): URI=raiz.getroot().getchildren()[0].attrib['Id'], nsmap={'sig': NAMESPACE_SIG}, ) + + signed_info = etree.SubElement(signature, '{%s}SignedInfo'%NAMESPACE_SIG) + etree.SubElement(signed_info, 'CanonicalizationMethod', Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315") + etree.SubElement(signed_info, 'SignatureMethod', Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1") + + reference = etree.SubElement(signed_info, '{%s}Reference'%NAMESPACE_SIG, URI=raiz.getroot().getchildren()[0].attrib['Id']) + transforms = etree.SubElement(reference, 'Transforms', URI=raiz.getroot().getchildren()[0].attrib['Id']) + etree.SubElement(transforms, 'Transform', Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature") + etree.SubElement(transforms, 'Transform', Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315") + etree.SubElement(reference, '{%s}DigestMethod'%NAMESPACE_SIG, Algorithm="http://www.w3.org/2000/09/xmldsig#sha1") + digest_value = etree.SubElement(reference, '{%s}DigestValue'%NAMESPACE_SIG) + + signature_value = etree.SubElement(signature, '{%s}SignatureValue'%NAMESPACE_SIG) + + key_info = etree.SubElement(signature, '{%s}KeyInfo'%NAMESPACE_SIG) + x509_data = etree.SubElement(key_info, '{%s}X509Data'%NAMESPACE_SIG) + x509_certificate = etree.SubElement(x509_data, '{%s}X509Certificate'%NAMESPACE_SIG) + raiz.getroot().insert(0, signature) # Acrescenta a tag de doctype (como o lxml nao suporta alteracao do doctype, @@ -108,21 +140,27 @@ class AssinaturaA1(Assinatura): assinador.sign(noh_assinatura) # Coloca na instância Signature os valores calculados - doc.Signature.DigestValue = ctxt.xpathEval(u'//sig:DigestValue')[0].content.replace(u'\n', u'') - doc.Signature.SignatureValue = ctxt.xpathEval(u'//sig:SignatureValue')[0].content.replace(u'\n', u'') + digest_value.text = ctxt.xpathEval(u'//sig:DigestValue')[0].content.replace(u'\n', u'') + signature_value.text = ctxt.xpathEval(u'//sig:SignatureValue')[0].content.replace(u'\n', u'') # Provavelmente retornarão vários certificados, já que o xmlsec inclui a cadeia inteira certificados = ctxt.xpathEval(u'//sig:X509Data/sig:X509Certificate') - doc.Signature.X509Certificate = certificados[len(certificados)-1].content.replace(u'\n', u'') + x509_certificate.text = certificados[len(certificados)-1].content.replace(u'\n', u'') resultado = assinador.status == xmlsec.DSigStatusSucceeded # Limpa objetos da memoria e desativa funções criptográficas self._depois_de_assinar_ou_verificar(doc_xml, ctxt, assinador) - #print etree.tostring(raiz, pretty_print=True, xml_declaration=True, encoding='utf-8') + # Gera o XML para retornar + raise Exception(dir(doc_xml)) + xml = doc_xml.serialize() - return resultado + if retorna_xml: + raise Exception(xml) + return xml + else: + return etree.parse(StringIO(xml)) def _ativar_funcoes_criptograficas(self): # FIXME: descobrir forma de evitar o uso do libxml2 neste processo @@ -175,7 +213,6 @@ class AssinaturaA1(Assinatura): return resultado def _antes_de_assinar_ou_verificar(self, raiz): - raise Exception(dir(raiz)) # Converte etree para string xml = etree.tostring(raiz, xml_declaration=True, encoding='utf-8') diff --git a/pynfe/processamento/comunicacao.py b/pynfe/processamento/comunicacao.py index 27bfbec..750730d 100644 --- a/pynfe/processamento/comunicacao.py +++ b/pynfe/processamento/comunicacao.py @@ -3,7 +3,7 @@ from httplib import HTTPSConnection, HTTPResponse from pynfe.utils import etree, StringIO -from pynfe.utils.flags import NAMESPACE_NFE, NAMESPACE_SOAP +from pynfe.utils.flags import NAMESPACE_NFE, NAMESPACE_SOAP, VERSAO_PADRAO class Comunicacao(object): u"""Classe abstrata responsavel por definir os metodos e logica das classes @@ -21,7 +21,7 @@ class Comunicacao(object): class ComunicacaoSefaz(Comunicacao): u"""Classe de comunicação que segue o padrão definido para as SEFAZ dos Estados.""" - _versao = '1.01' + _versao = VERSAO_PADRAO def transmitir(self, nota_fiscal): pass diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index f519d4d..f36939d 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -7,7 +7,7 @@ except: from pynfe.entidades import Emitente, Cliente, Produto, Transportadora, NotaFiscal from pynfe.excecoes import NenhumObjetoEncontrado, MuitosObjetosEncontrados from pynfe.utils import etree, so_numeros, obter_municipio_por_codigo, obter_pais_por_codigo -from pynfe.utils.flags import CODIGOS_ESTADOS +from pynfe.utils.flags import CODIGOS_ESTADOS, VERSAO_PADRAO class Serializacao(object): """Classe abstrata responsavel por fornecer as funcionalidades basicas para @@ -43,6 +43,8 @@ class Serializacao(object): raise Exception('Metodo nao implementado') class SerializacaoXML(Serializacao): + _versao = VERSAO_PADRAO + def exportar(self, destino=None, retorna_string=False, **kwargs): """Gera o(s) arquivo(s) de Nofa Fiscal eletronica no padrao oficial da SEFAZ e Receita Federal, para ser(em) enviado(s) para o webservice ou para ser(em) @@ -220,7 +222,7 @@ class SerializacaoXML(Serializacao): return raiz def _serializar_notas_fiscal(self, nota_fiscal, tag_raiz='infNFe', retorna_string=True): - raiz = etree.Element(tag_raiz, versao="2.00") + raiz = etree.Element(tag_raiz, versao=self._versao) # Dados da Nota Fiscal ide = etree.SubElement(raiz, 'ide') diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index 77b2acb..515ec18 100644 --- a/pynfe/utils/flags.py +++ b/pynfe/utils/flags.py @@ -4,6 +4,8 @@ NAMESPACE_NFE = 'http://www.portalfiscal.inf.br/nfe' NAMESPACE_SIG = 'http://www.w3.org/2000/09/xmldsig#' NAMESPACE_SOAP = 'http://www.w3.org/2003/05/soap-envelope' +VERSAO_PADRAO = '1.01' + TIPOS_DOCUMENTO = ( 'CNPJ', 'CPF', diff --git a/tests/03-processamento-01-serializacao-xml.txt b/tests/03-processamento-01-serializacao-xml.txt index c80a2ee..1ceb14c 100644 --- a/tests/03-processamento-01-serializacao-xml.txt +++ b/tests/03-processamento-01-serializacao-xml.txt @@ -268,7 +268,7 @@ Serializando por partes NFe52100112345678000190550010000000011518005123 >>> print serializador._serializar_notas_fiscal(nota_fiscal) - + 52 51800512 From 4a6abec30e5bc2141b019c670ea7947b677b77de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Tue, 19 Jan 2010 00:21:42 -0200 Subject: [PATCH 7/7] Ajustes na assinatura --- pynfe/processamento/assinatura.py | 10 +++------- tests/03-processamento-03-assinatura.txt | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pynfe/processamento/assinatura.py b/pynfe/processamento/assinatura.py index b9eacb8..dde2c65 100644 --- a/pynfe/processamento/assinatura.py +++ b/pynfe/processamento/assinatura.py @@ -68,8 +68,6 @@ class AssinaturaA1(Assinatura): # Efetua a assinatura xml = self.assinar_etree(raiz, retorna_xml=True) - raise Exception(xml) - # Grava XML assinado no arquivo if salva: fp = file(caminho_arquivo, 'w') @@ -149,15 +147,13 @@ class AssinaturaA1(Assinatura): resultado = assinador.status == xmlsec.DSigStatusSucceeded - # Limpa objetos da memoria e desativa funções criptográficas - self._depois_de_assinar_ou_verificar(doc_xml, ctxt, assinador) - # Gera o XML para retornar - raise Exception(dir(doc_xml)) xml = doc_xml.serialize() + # Limpa objetos da memoria e desativa funções criptográficas + self._depois_de_assinar_ou_verificar(doc_xml, ctxt, assinador) + if retorna_xml: - raise Exception(xml) return xml else: return etree.parse(StringIO(xml)) diff --git a/tests/03-processamento-03-assinatura.txt b/tests/03-processamento-03-assinatura.txt index b564a24..473d159 100644 --- a/tests/03-processamento-03-assinatura.txt +++ b/tests/03-processamento-03-assinatura.txt @@ -24,7 +24,7 @@ A assinatura deve ser feita em quatro tipos diferentes de origem do XML: - Arquivo - >>> assinatura.assinar_arquivo('tests/saida/nfe-1.xml') + >>> bool(assinatura.assinar_arquivo('tests/saida/nfe-1.xml')) True - String de XML