10 changed files with 288 additions and 5 deletions
-
24pytrustnfe/Certificado.py
-
38pytrustnfe/HttpClient.py
-
54pytrustnfe/servicos/Comunicacao.py
-
5pytrustnfe/servicos/comunicacao.py
-
13pytrustnfe/test/XMLs/recibo_envio_1.xml
-
13pytrustnfe/test/XMLs/recibo_envio_2.xml
-
21pytrustnfe/test/XMLs/recibo_protocolo_sucesso_1.xml
-
21pytrustnfe/test/XMLs/recibo_protocolo_sucesso_2.xml
-
65pytrustnfe/test/test_certificado.py
-
39pytrustnfe/test/test_comunicacao.py
@ -0,0 +1,24 @@ |
|||||
|
#coding=utf-8 |
||||
|
''' |
||||
|
Created on Jun 16, 2015 |
||||
|
|
||||
|
@author: danimar |
||||
|
''' |
||||
|
import os.path |
||||
|
from OpenSSL import crypto |
||||
|
|
||||
|
|
||||
|
def converte_pfx_pem(caminho, senha): |
||||
|
if not os.path.isfile(caminho): |
||||
|
raise Exception('Certificado não existe') |
||||
|
stream = open(caminho, 'rb').read() |
||||
|
try: |
||||
|
certificado = crypto.load_pkcs12(stream, senha) |
||||
|
|
||||
|
privada = crypto.dump_privatekey(crypto.FILETYPE_PEM, certificado.get_privatekey()) |
||||
|
certificado = crypto.dump_certificate(crypto.FILETYPE_PEM, certificado.get_certificate()) |
||||
|
except Exception as e: |
||||
|
if len(e.message) == 1 and len(e.message[0])==3 and e.message[0][2] == 'mac verify failure': |
||||
|
raise Exception('Senha inválida') |
||||
|
raise |
||||
|
return privada, certificado |
||||
@ -0,0 +1,38 @@ |
|||||
|
#coding=utf-8 |
||||
|
''' |
||||
|
Created on Jun 16, 2015 |
||||
|
|
||||
|
@author: danimar |
||||
|
''' |
||||
|
from httplib import HTTPSConnection, HTTPResponse |
||||
|
|
||||
|
class HttpClient(object): |
||||
|
|
||||
|
def __init__(self, url, chave_pem, certificado_pem): |
||||
|
self.url = url |
||||
|
self.chave_pem = chave_pem |
||||
|
self.certificado_pem = certificado_pem |
||||
|
|
||||
|
def _headers(self): |
||||
|
return { |
||||
|
u'Content-type': u'application/soap+xml; charset=utf-8', |
||||
|
u'Accept': u'application/soap+xml; charset=utf-8' |
||||
|
} |
||||
|
|
||||
|
def post_xml(self, post, xml): |
||||
|
|
||||
|
conexao = HTTPSConnection(self.url, '443', key_file=self.chave_pem, |
||||
|
cert_file=self.certificado_pem) |
||||
|
|
||||
|
try: |
||||
|
conexao.request(u'POST', post, xml, self._headers()) |
||||
|
response = conexao.getresponse() |
||||
|
if response.status == '200': |
||||
|
return response.read() |
||||
|
except Exception as e: |
||||
|
print str(e) |
||||
|
finally: |
||||
|
conexao.close() |
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,54 @@ |
|||||
|
#coding=utf-8 |
||||
|
''' |
||||
|
Created on Jun 14, 2015 |
||||
|
|
||||
|
@author: danimar |
||||
|
''' |
||||
|
|
||||
|
from lxml import objectify |
||||
|
from uuid import uuid4 |
||||
|
from pytrustnfe.HttpClient import HttpClient |
||||
|
from pytrustnfe.Certificado import converte_pfx_pem |
||||
|
|
||||
|
import logging |
||||
|
from logilab.common.registry import objectify_predicate |
||||
|
logging.basicConfig(level=logging.INFO) |
||||
|
logging.getLogger('suds.client').setLevel(logging.DEBUG) |
||||
|
logging.getLogger('suds.transport').setLevel(logging.DEBUG) |
||||
|
|
||||
|
|
||||
|
class Comunicacao(object): |
||||
|
|
||||
|
def __init__(self, certificado, senha): |
||||
|
self.certificado = certificado |
||||
|
self.senha = senha |
||||
|
|
||||
|
|
||||
|
def _preparar_temp_pem(self): |
||||
|
chave_temp = '/tmp/' + uuid4().hex |
||||
|
certificado_temp = '/tmp/' + uuid4().hex |
||||
|
|
||||
|
chave, certificado = converte_pfx_pem(self.certificado, self.senha) |
||||
|
arq_temp = open(chave_temp, 'w') |
||||
|
arq_temp.write(chave) |
||||
|
arq_temp.close() |
||||
|
|
||||
|
arq_temp = open(certificado_temp, 'w') |
||||
|
arq_temp.write(certificado) |
||||
|
arq_temp.close() |
||||
|
|
||||
|
return chave_temp, certificado_temp |
||||
|
|
||||
|
def envio_nfe(self): |
||||
|
chave, certificado = self._preparar_temp_pem() |
||||
|
|
||||
|
c = HttpClient('cad.svrs.rs.gov.br', chave, certificado) |
||||
|
|
||||
|
xml_retorno = c.post_xml('/ws/cadconsultacadastro/cadconsultacadastro2.asmx', '') |
||||
|
|
||||
|
obj = objectify.fromstring(xml_retorno) |
||||
|
|
||||
|
return xml_retorno, obj |
||||
|
|
||||
|
|
||||
|
|
||||
@ -1,5 +0,0 @@ |
|||||
''' |
|
||||
Created on Jun 14, 2015 |
|
||||
|
|
||||
@author: danimar |
|
||||
''' |
|
||||
@ -0,0 +1,13 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<retEnviNFe versao="2.00" xmlns="http://www.portalfiscal.inf.br/nfe"> |
||||
|
<tpAmb>2</tpAmb> |
||||
|
<verAplic>SVRS20140728145415</verAplic> |
||||
|
<cStat>103</cStat> |
||||
|
<xMotivo>Lote recebido com sucesso</xMotivo> |
||||
|
<cUF>42</cUF> |
||||
|
<dhRecbto>2014-08-18T10:32:32</dhRecbto> |
||||
|
<infRec> |
||||
|
<nRec>423002149000085</nRec> |
||||
|
<tMed>1</tMed> |
||||
|
</infRec> |
||||
|
</retEnviNFe> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<retEnviNFe versao="2.00" xmlns="http://www.portalfiscal.inf.br/nfe"> |
||||
|
<tpAmb>2</tpAmb> |
||||
|
<verAplic>SVRS20140728145415</verAplic> |
||||
|
<cStat>103</cStat> |
||||
|
<xMotivo>Lote recebido com sucesso</xMotivo> |
||||
|
<cUF>42</cUF> |
||||
|
<dhRecbto>2014-08-18T17:33:28</dhRecbto> |
||||
|
<infRec> |
||||
|
<nRec>423002149008908</nRec> |
||||
|
<tMed>1</tMed> |
||||
|
</infRec> |
||||
|
</retEnviNFe> |
||||
@ -0,0 +1,21 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<retConsReciNFe versao="2.00" xmlns="http://www.portalfiscal.inf.br/nfe"> |
||||
|
<tpAmb>2</tpAmb> |
||||
|
<verAplic>SVRS20140729095237</verAplic> |
||||
|
<nRec>423002149000085</nRec> |
||||
|
<cStat>104</cStat> |
||||
|
<xMotivo>Lote processado</xMotivo> |
||||
|
<cUF>42</cUF> |
||||
|
<protNFe versao="2.00"> |
||||
|
<infProt Id="ID342140000660576"> |
||||
|
<tpAmb>2</tpAmb> |
||||
|
<verAplic>SVRS20140729095237</verAplic> |
||||
|
<chNFe>42140803657739000169550020000000011000000018</chNFe> |
||||
|
<dhRecbto>2014-08-18T10:32:32</dhRecbto> |
||||
|
<nProt>342140000660576</nProt> |
||||
|
<digVal>ladI/iyYJbQx6QW5ihtlBMR1UUY=</digVal> |
||||
|
<cStat>100</cStat> |
||||
|
<xMotivo>Autorizado o uso da NF-e</xMotivo> |
||||
|
</infProt> |
||||
|
</protNFe> |
||||
|
</retConsReciNFe> |
||||
@ -0,0 +1,21 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<retConsReciNFe versao="2.00" xmlns="http://www.portalfiscal.inf.br/nfe"> |
||||
|
<tpAmb>2</tpAmb> |
||||
|
<verAplic>SVRS20140729095237</verAplic> |
||||
|
<nRec>423002149008908</nRec> |
||||
|
<cStat>104</cStat> |
||||
|
<xMotivo>Lote processado</xMotivo> |
||||
|
<cUF>42</cUF> |
||||
|
<protNFe versao="2.00"> |
||||
|
<infProt Id="ID342140000666829"> |
||||
|
<tpAmb>2</tpAmb> |
||||
|
<verAplic>SVRS20140729095237</verAplic> |
||||
|
<chNFe>42140803657739000169550020000000021000000023</chNFe> |
||||
|
<dhRecbto>2014-08-18T17:33:28</dhRecbto> |
||||
|
<nProt>342140000666829</nProt> |
||||
|
<digVal>oQsitGdZl1I66NNzpMn8Wf6mI7c=</digVal> |
||||
|
<cStat>100</cStat> |
||||
|
<xMotivo>Autorizado o uso da NF-e</xMotivo> |
||||
|
</infProt> |
||||
|
</protNFe> |
||||
|
</retConsReciNFe> |
||||
@ -0,0 +1,65 @@ |
|||||
|
#coding=utf-8 |
||||
|
''' |
||||
|
Created on Jun 14, 2015 |
||||
|
|
||||
|
@author: danimar |
||||
|
''' |
||||
|
import unittest |
||||
|
import os, os.path |
||||
|
from pytrustnfe.Certificado import converte_pfx_pem |
||||
|
|
||||
|
CHAVE = '-----BEGIN PRIVATE KEY-----\n' \ |
||||
|
'MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJONRp6l1y2ojgv8\n' \ |
||||
|
'tP3AOLW0vjWQqiPseBLM7YAxbzz5R7LYlWHC0ZJ4uIvd4Cvc6AuoNJoeuhzFcwHx\n' \ |
||||
|
'PL0TcFuW+5up1ktUohwaJ+/zKrMODCKt0gvif302yqasMnwLh9mGZQIkLkHPOX8p\n' \ |
||||
|
'ZQDC4dlqwOyYDi0f+bRd5C7aWx3RAgMBAAECgYADqASP+dwTLZIXifOSNikxl4D/\n' \ |
||||
|
'Is6UhU+UZ6+a9Z6kDClSrTtGaOV4k7U/AgiEDb1STKDBEPHbtKjc63Vt2gV2teem\n' \ |
||||
|
'ohU0Giv+gD42uuwy2DM31OfYrpR46mzOK9JrpQc78b36ealL3AWJ1gyBbbcOWbAb\n' \ |
||||
|
'KmP742V7pcD07EEp4QJBAM/e7M8VdLgOyaQzH9KHekU6fJlI4vy1UwgRUwx3/1W6\n' \ |
||||
|
'zlBYo1qXfc7NSVG8ZaSrJwW4rPn393u31CpXv+oc/OMCQQC1txS6nxM9+p/641HX\n' \ |
||||
|
'CHXiWJRn0Wv7rT1FyF2dHO+OQOkCCnHCsGDMf3bacTNb7iyaPbXEDac8od5uF/3h\n' \ |
||||
|
'aUy7AkBDPGoAeYItXqseL2Mlp6iG5+oRcp/o+YWH4IKqT84JHslI98KutL1+vKvw\n'\ |
||||
|
'gi2mW63djeR1Xh1wqP85SvTKduHdAkAIJLlIF8Lr/yRWQQO06EsoJqIX+Pmm4L+j\n'\ |
||||
|
'NfSECvztWhlXHxK0D+V2pKu15GbR0t2q1+Micx4wiGyIcIjPJkHrAkAvlbXGFcGT\n'\ |
||||
|
'pk9bQ8nl7EYqlvVn1TejzTLfBhBYOse/xT/NI4Kwjkan9R+EJ1cOc9EE8gm1W3jv\n'\ |
||||
|
'fMw/Bh2wC5kj\n'\ |
||||
|
'-----END PRIVATE KEY-----\n' |
||||
|
|
||||
|
CERTIFICADO = '-----BEGIN CERTIFICATE-----\n'\ |
||||
|
'MIICMTCCAZqgAwIBAgIQfYOsIEVuAJ1FwwcTrY0t1DANBgkqhkiG9w0BAQUFADBX\n'\ |
||||
|
'MVUwUwYDVQQDHkwAewA1ADkARgAxAEUANAA2ADEALQBEAEQARQA1AC0ANABEADIA\n'\ |
||||
|
'RgAtAEEAMAAxAEEALQA4ADMAMwAyADIAQQA5AEUAQgA4ADMAOAB9MB4XDTE1MDYx\n'\ |
||||
|
'NTA1NDc1N1oXDTE2MDYxNDExNDc1N1owVzFVMFMGA1UEAx5MAHsANQA5AEYAMQBF\n'\ |
||||
|
'ADQANgAxAC0ARABEAEUANQAtADQARAAyAEYALQBBADAAMQBBAC0AOAAzADMAMgAy\n'\ |
||||
|
'AEEAOQBFAEIAOAAzADgAfTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAk41G\n'\ |
||||
|
'nqXXLaiOC/y0/cA4tbS+NZCqI+x4EsztgDFvPPlHstiVYcLRkni4i93gK9zoC6g0\n'\ |
||||
|
'mh66HMVzAfE8vRNwW5b7m6nWS1SiHBon7/Mqsw4MIq3SC+J/fTbKpqwyfAuH2YZl\n'\ |
||||
|
'AiQuQc85fyllAMLh2WrA7JgOLR/5tF3kLtpbHdECAwEAATANBgkqhkiG9w0BAQUF\n'\ |
||||
|
'AAOBgQArdh+RyT6VxKGsXk1zhHsgwXfToe6GpTF4W8PHI1+T0WIsNForDhvst6nm\n'\ |
||||
|
'QtgAhuZM9rxpOJuNKc+pM29EixpAiZZiRMCSWEItNyEVdUIi+YnKBcAHd88TwO86\n'\ |
||||
|
'd126MWQ2O8cu5W1VoDp7hYBYKOnLbYi11/StO+0rzK+oPYAvIw==\n'\ |
||||
|
'-----END CERTIFICATE-----\n' |
||||
|
|
||||
|
class test_assinatura(unittest.TestCase): |
||||
|
|
||||
|
caminho = os.path.dirname(__file__) |
||||
|
|
||||
|
def test_preparar_pfx(self): |
||||
|
dir_pfx = os.path.join(self.caminho, 'teste.pfx') |
||||
|
chave, certificado = converte_pfx_pem(dir_pfx, '123456') |
||||
|
self.assertEqual(chave, CHAVE, 'Chave gerada inválida') |
||||
|
self.assertEqual(certificado, CERTIFICADO, 'Certificado gerado inválido') |
||||
|
|
||||
|
def test_pfx_nao_existe(self): |
||||
|
self.assertRaises(Exception, converte_pfx_pem, 'file.pfx', '123456') |
||||
|
|
||||
|
def test_pfx_senha_invalida(self): |
||||
|
dir_pfx = os.path.join(self.caminho, 'teste.pfx') |
||||
|
self.assertRaises(Exception, converte_pfx_pem, dir_pfx, '123') |
||||
|
|
||||
|
def test_pfx_invalido(self): |
||||
|
dir_pfx = os.path.join(self.caminho, 'xml_assinado.xml') |
||||
|
self.assertRaises(Exception, converte_pfx_pem, dir_pfx, '123456') |
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,39 @@ |
|||||
|
#coding=utf-8 |
||||
|
''' |
||||
|
Created on Jun 16, 2015 |
||||
|
|
||||
|
@author: danimar |
||||
|
''' |
||||
|
import mock |
||||
|
import unittest |
||||
|
import os.path |
||||
|
from pytrustnfe.servicos.Comunicacao import Comunicacao |
||||
|
|
||||
|
XML_RETORNO = '<retEnviNFe><cStat>103</cStat>' \ |
||||
|
'<cUF>42</cUF></retEnviNFe>' |
||||
|
|
||||
|
class test_comunicacao(unittest.TestCase): |
||||
|
|
||||
|
caminho = os.path.dirname(__file__) |
||||
|
|
||||
|
def test_envio_nfe(self): |
||||
|
dir_pfx = os.path.join(self.caminho, 'teste.pfx') |
||||
|
|
||||
|
#dir_pfx = '/home/danimar/Desktop/INFOGER.pfx' #Hack |
||||
|
|
||||
|
with mock.patch('pytrustnfe.HttpClient.HTTPSConnection') as HttpsConnection: |
||||
|
conn = HttpsConnection.return_value |
||||
|
retorno = mock.MagicMock() |
||||
|
type(retorno).status = mock.PropertyMock(return_value='200') |
||||
|
retorno.read.return_value = XML_RETORNO |
||||
|
|
||||
|
conn.getresponse.return_value = retorno |
||||
|
|
||||
|
com = Comunicacao(dir_pfx, '123456') |
||||
|
xml, objeto = com.envio_nfe() |
||||
|
|
||||
|
self.assertEqual(xml, XML_RETORNO, 'Envio de NF-e com problemas - xml de retorno inválido') |
||||
|
self.assertEqual(objeto.cUF, 42, 'Envio de NF-e com problemas - objeto de retorno inválido') |
||||
|
self.assertEqual(objeto.cStat, 103, 'Envio de NF-e com problemas - objeto de retorno inválido') |
||||
|
|
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue