From 847935f611e14dcc43105878de0571245c20d352 Mon Sep 17 00:00:00 2001 From: Junior Tada Date: Wed, 11 Nov 2015 00:40:36 -0200 Subject: [PATCH] =?UTF-8?q?Inicio=20implanta=C3=A7=C3=A3o=20de=20NFS-e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/entidades/__init__.py | 1 + pynfe/entidades/notafiscal.py | 26 ++++++++++ pynfe/entidades/servico.py | 21 ++++++++ pynfe/processamento/__init__.py | 1 + pynfe/processamento/assinatura.py | 38 +++++++++++++++ pynfe/processamento/serializacao.py | 95 ++++++++++++++++++++++++++++++++++++- pynfe/utils/flags.py | 2 + pynfe/utils/webservices.py | 30 ++++++++++++ 8 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 pynfe/entidades/servico.py diff --git a/pynfe/entidades/__init__.py b/pynfe/entidades/__init__.py index f4fa7d4..f2c8464 100644 --- a/pynfe/entidades/__init__.py +++ b/pynfe/entidades/__init__.py @@ -7,4 +7,5 @@ from .lotes import LoteNotaFiscal from .fonte_dados import _fonte_dados from .certificado import CertificadoA1 from .evento import EventoCancelarNota +from .servico import Servico diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index 0d276a7..5a00190 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -905,3 +905,29 @@ class NotaFiscalEntregaRetirada(Entidade): # - Telefone endereco_telefone = str() +class NotaFiscalServico(Entidade): + + # Empresa que implementa o webservice + autorizador = str() # betha + # id do rps + identificador = str() + # tag competencia + data_emissao = None + # Serviço executado pelo prestador + servico = None + # Emitente da NFS-e + emitente = None + # Cliente para quem a NFS-e será emitida + cliente = None + # Optante Simples Nacional + simples = int() # 1-Sim; 2-Não + # Incentivo Fiscal + incentivo = int() # 1-Sim; 2-Não + + def __init__(self, *args, **kwargs): + + super(NotaFiscalServico, self).__init__(*args, **kwargs) + + def __str__(self): + return ' '.join([str(self.identificador)]) + diff --git a/pynfe/entidades/servico.py b/pynfe/entidades/servico.py new file mode 100644 index 0000000..e9679bb --- /dev/null +++ b/pynfe/entidades/servico.py @@ -0,0 +1,21 @@ +""" + @author: Junior Tada, Leonardo Tada +""" + +from .base import Entidade +from decimal import Decimal + +class Servico(Entidade): + + valor_servico = Decimal() + iss_retido = Decimal() + """ http://www1.receita.fazenda.gov.br/sistemas/nfse/tabelas-de-codigos.htm + Lista com códigos dos serviços + """ + item_lista = str() + discriminacao = str() + exigibilidade = int() + codigo_municipio = str() + + def __str__(self): + return self.discriminacao \ No newline at end of file diff --git a/pynfe/processamento/__init__.py b/pynfe/processamento/__init__.py index a28647d..e63097d 100644 --- a/pynfe/processamento/__init__.py +++ b/pynfe/processamento/__init__.py @@ -1,4 +1,5 @@ from .serializacao import SerializacaoXML +from .serializacao import SerializacaoNfse from .validacao import Validacao from .assinatura import AssinaturaA1 from .comunicacao import ComunicacaoSefaz diff --git a/pynfe/processamento/assinatura.py b/pynfe/processamento/assinatura.py index 2b16130..9cd702b 100644 --- a/pynfe/processamento/assinatura.py +++ b/pynfe/processamento/assinatura.py @@ -62,4 +62,42 @@ class AssinaturaA1(Assinatura): return xml except Exception as e: raise e + + def assinarNfse(self, xml, retorna_string=False): + try: + # No raiz do XML de saida + tag = 'InfDeclaracaoPrestacaoServico'; # tag que será assinada + raiz = etree.Element('Signature', xmlns='http://www.w3.org/2000/09/xmldsig#') + siginfo = etree.SubElement(raiz, 'SignedInfo') + etree.SubElement(siginfo, 'CanonicalizationMethod', Algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315') + etree.SubElement(siginfo, 'SignatureMethod', Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1') + # Tenta achar a tag infNFe + + ref = etree.SubElement(siginfo, 'Reference', URI='#'+xml.xpath('Rps/InfDeclaracaoPrestacaoServico')[0].attrib['Id']) + + trans = etree.SubElement(ref, 'Transforms') + etree.SubElement(trans, 'Transform', Algorithm='http://www.w3.org/2000/09/xmldsig#enveloped-signature') + etree.SubElement(trans, 'Transform', Algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315') + etree.SubElement(ref, 'DigestMethod', Algorithm='http://www.w3.org/2000/09/xmldsig#sha1') + etree.SubElement(ref, 'DigestValue') + etree.SubElement(raiz, 'SignatureValue') + keyinfo = etree.SubElement(raiz, 'KeyInfo') + etree.SubElement(keyinfo, 'X509Data') + + rps = xml.xpath('Rps')[0] + rps.append(raiz) + + # Escreve no arquivo depois de remover caracteres especiais e parse string + with open('nfse.xml', 'w') as arquivo: + arquivo.write(remover_acentos(etree.tostring(xml, encoding="unicode", pretty_print=False))) + + subprocess.call(['xmlsec1', '--sign', '--pkcs12', self.certificado, '--pwd', self.senha, '--crypto', 'openssl', '--output', 'funfa.xml', '--id-attr:Id', tag, 'nfse.xml']) + xml = etree.parse('funfa.xml').getroot() + + if retorna_string: + return etree.tostring(xml, encoding="unicode", pretty_print=False) + else: + return xml + except Exception as e: + raise e diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 5073e7a..462d54d 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -6,7 +6,7 @@ from pynfe.entidades import NotaFiscal from pynfe.utils import etree, so_numeros, obter_municipio_por_codigo, \ obter_pais_por_codigo, obter_municipio_e_codigo, \ formatar_decimal, remover_acentos, obter_uf_por_codigo, obter_codigo_por_municipio -from pynfe.utils.flags import CODIGOS_ESTADOS, VERSAO_PADRAO, NAMESPACE_NFE +from pynfe.utils.flags import CODIGOS_ESTADOS, VERSAO_PADRAO, NAMESPACE_NFE, NAMESPACE_BETHA class Serializacao(object): """Classe abstrata responsavel por fornecer as funcionalidades basicas para @@ -576,6 +576,99 @@ class SerializacaoXML(Serializacao): else: return raiz +class SerializacaoNfse(Serializacao): + + def exportar(self): + pass + + def importar(self): + pass + + def _serializar_emitente(self, emitente, tag_raiz='Prestador', retorna_string=True): + raiz = etree.Element(tag_raiz) + documento = etree.SubElement(raiz, 'CpfCnpj') + etree.SubElement(documento, 'Cnpj').text = emitente.cnpj + etree.SubElement(raiz, 'InscricaoMunicipal').text = emitente.inscricao_municipal + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_cliente(self, cliente, tag_raiz='Tomador', retorna_string=True): + raiz = etree.Element(tag_raiz) + identificacao = etree.SubElement(raiz, 'IdentificacaoTomador') + documento = etree.SubElement(identificacao, 'CpfCnpj') + etree.SubElement(documento, cliente.tipo_documento).text = cliente.numero_documento # Apenas Cnpj ?? + etree.SubElement(identificacao, 'InscricaoMunicipal').text = cliente.inscricao_municipal # obrigatório?? + etree.SubElement(raiz, 'RazaoSocial').text = cliente.razao_social + endereco = etree.SubElement(raiz, 'Endereco') + etree.SubElement(endereco, 'Endereco').text = cliente.endereco_logradouro + etree.SubElement(endereco, 'Numero').text = cliente.endereco_numero + if cliente.endereco_complemento: + etree.SubElement(endereco, 'Complemento').text = cliente.endereco_complemento + etree.SubElement(endereco, 'Bairro').text = cliente.endereco_bairro + etree.SubElement(endereco, 'CodigoMunicipio').text = obter_codigo_por_municipio( + cliente.endereco_municipio, cliente.endereco_uf) + etree.SubElement(endereco, 'Uf').text = cliente.endereco_uf + etree.SubElement(endereco, 'CodigoPais').text = cliente.endereco_pais + etree.SubElement(endereco, 'Cep').text = so_numeros(cliente.endereco_cep) + contato = etree.SubElement(raiz, 'Contato') + etree.SubElement(contato, 'Telefone').text = cliente.endereco_telefone + etree.SubElement(contato, 'Email').text = cliente.email + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_servico(self, servico, tag_raiz='Servico', retorna_string=True): + raiz = etree.Element(tag_raiz) + valores = etree.SubElement(raiz, 'Valores') + etree.SubElement(valores, 'ValorServicos').text = str('{:.2f}').format(servico.valor_servico) + etree.SubElement(raiz, 'IssRetido').text = str('{:.2f}').format(servico.iss_retido) + #etree.SubElement(raiz, 'ResponsavelRetencao').text = '' + etree.SubElement(raiz, 'ItemListaServico').text = servico.item_lista + #etree.SubElement(raiz, 'CodigoCnae').text = '' + #etree.SubElement(raiz, 'CodigoTributacaoMunicipio').text = '' + etree.SubElement(raiz, 'Discriminacao').text = servico.discriminacao + etree.SubElement(raiz, 'CodigoMunicipio').text = servico.codigo_municipio + #etree.SubElement(raiz, 'CodigoPais').text = '' + etree.SubElement(raiz, 'ExigibilidadeISS').text = str(servico.exigibilidade) + etree.SubElement(raiz, 'MunicipioIncidencia').text = servico.codigo_municipio + #etree.SubElement(raiz, 'NumeroProcesso').text = '' + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_gerar(self, nfse, tag_raiz='GerarNfseEnvio', retorna_string=False): + + if nfse.autorizador == 'betha': + raiz = etree.Element(tag_raiz, xmlns=NAMESPACE_BETHA) + # TODO - implementar outros sistemas autorizadores + else: + raiz = etree.Element(tag_raiz) + rps = etree.SubElement(raiz, 'Rps') + info = etree.SubElement(rps, 'InfDeclaracaoPrestacaoServico', Id=nfse.identificador) + etree.SubElement(info, 'Competencia').text = nfse.data_emissao.strftime('%Y-%m-%d') + + # Servico + info.append(self._serializar_servico(nfse.servico, retorna_string=False)) + # Emitente/Prestador + info.append(self._serializar_emitente(nfse.emitente, retorna_string=False)) + # Cliente/Tomador + info.append(self._serializar_cliente(nfse.cliente, retorna_string=False)) + + etree.SubElement(info, 'OptanteSimplesNacional').text = str(nfse.simples) # 1-Sim; 2-Não + etree.SubElement(info, 'IncentivoFiscal').text = str(nfse.incentivo) # 1-Sim; 2-Não + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + class SerializacaoPipes(Serializacao): """Serialização utilizada pela SEFAZ-SP para a importação de notas.""" diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index 4ca34db..f1bb65d 100644 --- a/pynfe/utils/flags.py +++ b/pynfe/utils/flags.py @@ -7,6 +7,8 @@ NAMESPACE_XSI = 'http://www.w3.org/2001/XMLSchema-instance' NAMESPACE_XSD = 'http://www.w3.org/2001/XMLSchema' NAMESPACE_METODO = 'http://www.portalfiscal.inf.br/nfe/wsdl/' +NAMESPACE_BETHA = 'http://www.betha.com.br/e-nota-contribuinte-ws' + VERSAO_PADRAO = '3.10' VERSAO_QRCODE = '100' diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index 00af8a1..453ee7b 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -420,4 +420,34 @@ NFE = { 'HTTPS': 'https://nfe.', 'HOMOLOGACAO': 'https://nfe-homologacao.' }, +} + +# Nfs-e +NFSE = { + # + 'BETHA': { + 'STATUS':'', + 'AUTORIZACAO':'GerarNfse', + 'CANCELAR':'CancelarNfse', + 'CONSULTA_RPS':'ConsultarNfsePorRps', + 'CONSULTA_FAIXA':'ConsultarNfseFaixa', + 'CONSULTA_SERVICO':'ConsultarNfseServicoPrestado', + 'CONSULTA_SERVICO_TOMADO':'ConsultarNfseServicoTomado', + 'SUBSTITUIR':'SubstituirNfse', + 'HTTPS':'http://e-gov.betha.com.br/e-nota-contribuinte-ws/nfseWS?wsdl', + 'HOMOLOGACAO':'http://e-gov.betha.com.br/e-nota-contribuinte-test-ws/nfseWS?wsdl' + }, + # + 'GINFES':{ + 'STATUS':'', + 'AUTORIZACAO':'GerarNfse', + 'CANCELAR':'CancelarNfse', + 'CONSULTA_RPS':'ConsultarNfsePorRps', + 'CONSULTA_FAIXA':'ConsultarNfseFaixa', + 'CONSULTA_SERVICO':'ConsultarNfseServicoPrestado', + 'CONSULTA_SERVICO_TOMADO':'ConsultarNfseServicoTomado', + 'SUBSTITUIR':'SubstituirNfse', + 'HTTPS':'https://producao.ginfes.com.br/ServiceGinfesImpl?wsdl', + 'HOMOLOGACAO':'https://homologacao.ginfes.com.br/ServiceGinfesImpl?wsdl' + } } \ No newline at end of file