diff --git a/.gitignore b/.gitignore index cf2be46..11179d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Pycharm .idea/* +# VsCode +.vscode/* + # Apple OS X .DS_Store diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4894d86 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +graft tests +include AUTHORS PLANEJAMENTO LICENCE *.py *.sh diff --git a/README.md b/README.md index eb45520..bacf3cf 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,3 @@ Documentação ----------- - https://github.com/leotada/PyNFe/wiki - http://pynfe.readthedocs.org/pt/latest/ - -backlog: -- renomeado metodo serializar_evento (_serializar_evento) -- removido metoco con.cancelar (utilizar con.evento) -- add evento carta de correção (con.evento) diff --git a/pynfe/entidades/certificado.py b/pynfe/entidades/certificado.py index f2a560c..dc430b4 100644 --- a/pynfe/entidades/certificado.py +++ b/pynfe/entidades/certificado.py @@ -35,11 +35,21 @@ class CertificadoA1(Certificado): e retorna a string. Se caminho for True grava na pasta temporaria e retorna o caminho dos arquivos, senao retorna o objeto. Apos o uso devem ser excluidos com o metodo excluir.""" + try: + with open(self.caminho_arquivo, "rb") as cert_arquivo: + cert_conteudo = cert_arquivo.read() + except (PermissionError, FileNotFoundError) as exc: + raise Exception('Falha ao abrir arquivo do certificado digital A1. Verifique local e permissoes do arquivo.') from exc + except Exception as exc: + raise Exception('Falha ao abrir arquivo do certificado digital A1. Causa desconhecida.') from exc + # Carrega o arquivo .pfx, erro pode ocorrer se a senha estiver errada ou formato invalido. try: - pkcs12 = crypto.load_pkcs12(open(self.caminho_arquivo, "rb").read(), senha) - except Exception as e: - raise Exception('Falha ao carregar certificado digital A1. Verifique local e senha.') + pkcs12 = crypto.load_pkcs12(cert_conteudo, senha) + except crypto.Error as exc: + raise Exception('Falha ao carregar certificado digital A1. Verifique a senha do certificado.') from exc + except Exception as exc: + raise Exception('Falha ao carregar certificado digital A1. Causa desconhecida.') from exc if caminho: cert = crypto.dump_certificate(crypto.FILETYPE_PEM, pkcs12.get_certificate()) diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index ed001bf..4c204ed 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -145,6 +145,9 @@ class NotaFiscal(Entidade): # - Local de entrega diferente do destinatario (Sim/Nao) local_entrega_diferente_destinatario = False + # - Autorizados a baixar XML (lista 1 para * / ManyToManyField) + autorizados_baixar_xml = None + # - Produtos e Servicos (lista 1 para * / ManyToManyField) produtos_e_servicos = None @@ -349,6 +352,7 @@ class NotaFiscal(Entidade): processos_referenciados = None def __init__(self, *args, **kwargs): + self.autorizados_baixar_xml = [] self.notas_fiscais_referenciadas = [] self.produtos_e_servicos = [] self.transporte_volumes = [] @@ -362,6 +366,11 @@ class NotaFiscal(Entidade): def __str__(self): return ' '.join([str(self.modelo), self.serie, self.numero_nf]) + def adicionar_autorizados_baixar_xml(self, **kwargs): + obj = AutorizadosBaixarXML(**kwargs) + self.autorizados_baixar_xml.append(obj) + return obj + def adicionar_nota_fiscal_referenciada(self, **kwargs): u"""Adiciona uma instancia de Nota Fisca referenciada""" obj = NotaFiscalReferenciada(**kwargs) @@ -468,7 +477,7 @@ class NotaFiscal(Entidade): 'uf': CODIGOS_ESTADOS[self.uf], 'ano': self.data_emissao.strftime('%y'), 'mes': self.data_emissao.strftime('%m'), - 'cnpj': so_numeros(self.emitente.cnpj), + 'cnpj': so_numeros(self.emitente.cnpj).zfill(14), 'mod': self.modelo, 'serie': str(self.serie).zfill(3), 'nNF': str(self.numero_nf).zfill(9), @@ -479,7 +488,7 @@ class NotaFiscal(Entidade): 'uf': CODIGOS_ESTADOS[self.uf], 'ano': self.data_emissao.strftime('%y'), 'mes': self.data_emissao.strftime('%m'), - 'cnpj': so_numeros(self.emitente.cnpj), + 'cnpj': so_numeros(self.emitente.cnpj).zfill(14), 'mod': self.modelo, 'serie': str(self.serie).zfill(3), 'nNF': str(self.numero_nf).zfill(9), @@ -550,6 +559,9 @@ class NotaFiscalProduto(Entidade): # - Unidade Tributavel (obrigatorio) unidade_tributavel = str() + # - cBenef + cbenef = str() + # - Quantidade Tributavel (obrigatorio) quantidade_tributavel = Decimal() @@ -1023,3 +1035,6 @@ class NotaFiscalResponsavelTecnico(Entidade): email = str() fone = str() csrt = str() + +class AutorizadosBaixarXML(Entidade): + CPFCNPJ = str() diff --git a/pynfe/entidades/produto.py b/pynfe/entidades/produto.py index 21e3cfb..6d06e96 100644 --- a/pynfe/entidades/produto.py +++ b/pynfe/entidades/produto.py @@ -34,6 +34,8 @@ class Produto(Entidade): # Tabela https://www.confaz.fazenda.gov.br/anexo-i.pdf cest = str() + cbenef = str() + # - Unid. Com. unidade_comercial = str() diff --git a/pynfe/processamento/comunicacao.py b/pynfe/processamento/comunicacao.py index 1cbfe93..663f056 100644 --- a/pynfe/processamento/comunicacao.py +++ b/pynfe/processamento/comunicacao.py @@ -201,9 +201,11 @@ class ComunicacaoSefaz(Comunicacao): :return: """ # UF que utilizam a SVRS - Sefaz Virtual do RS: Para serviço de Consulta Cadastro: AC, RN, PB, SC - lista_svrs = ['AC', 'RJ', 'RN', 'PB', 'SC', 'PI'] + lista_svrs = ['AC', 'RN', 'PB', 'SC', 'PA'] # RS implementa um método diferente na consulta de cadastro + # usa o mesmo url para produção e homologação + # não tem url para NFCE if self.uf.upper() == 'RS': url = NFE['RS']['CADASTRO'] elif self.uf.upper() in lista_svrs: @@ -359,8 +361,7 @@ class ComunicacaoSefaz(Comunicacao): raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') # Estados que utilizam outros ambientes else: - lista_svrs = ['AC', 'RJ', 'RN', 'PB', 'SC', 'SE', 'PI', 'DF', 'ES'] - lista_svan = ['MA','PA'] + lista_svrs = ['AC', 'AL', 'AP', 'DF', 'ES', 'PB', 'PI', 'RJ', 'RN', 'RO', 'RR', 'SC', 'SE', 'TO', 'PA'] if self.uf.upper() in lista_svrs: if self._ambiente == 1: ambiente = 'HTTPS' @@ -374,7 +375,9 @@ class ComunicacaoSefaz(Comunicacao): self.url = NFCE['SVRS'][ambiente] + NFCE['SVRS'][consulta] else: raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') - elif self.uf.upper() in lista_svan: + # unico UF que utiliza SVAN ainda para NF-e + # SVRS para NFC-e + elif self.uf.upper() == 'MA': if self._ambiente == 1: ambiente = 'HTTPS' else: @@ -384,9 +387,11 @@ class ComunicacaoSefaz(Comunicacao): self.url = NFE['SVAN'][ambiente] + NFE['SVAN'][consulta] elif modelo == 'nfce': # nfce Ex: https://homologacao.nfce.fazenda.pr.gov.br/nfce/NFeStatusServico3 - self.url = NFCE['SVAN'][ambiente] + NFCE['SVAN'][consulta] + self.url = NFCE['SVRS'][ambiente] + NFCE['SVRS'][consulta] else: raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') + else: + raise Exception(f"Url não encontrada para {modelo} e {consulta} {self.uf.upper()}") return self.url def _construir_xml_soap(self, metodo, dados, cabecalho=False): diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index e216d5e..5810fe3 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -92,7 +92,10 @@ class SerializacaoXML(Serializacao): raiz = etree.Element(tag_raiz) # Dados do emitente - etree.SubElement(raiz, 'CNPJ').text = so_numeros(emitente.cnpj) + if len(so_numeros(emitente.cnpj)) == 11: + etree.SubElement(raiz, 'CPF').text = so_numeros(emitente.cnpj) + else: + etree.SubElement(raiz, 'CNPJ').text = so_numeros(emitente.cnpj) etree.SubElement(raiz, 'xNome').text = emitente.razao_social etree.SubElement(raiz, 'xFant').text = emitente.nome_fantasia # Endereço @@ -213,6 +216,19 @@ class SerializacaoXML(Serializacao): else: return raiz + def _serializar_autorizados_baixar_xml(self, autorizados_baixar_xml, tag_raiz='autXML', retorna_string=True): + raiz = etree.Element(tag_raiz) + + if len(so_numeros(autorizados_baixar_xml.CPFCNPJ)) == 11: + etree.SubElement(raiz, 'CPF').text = so_numeros(autorizados_baixar_xml.CPFCNPJ) + else: + etree.SubElement(raiz, 'CNPJ').text = so_numeros(autorizados_baixar_xml.CPFCNPJ) + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + def _serializar_produto_servico(self, produto_servico, modelo, tag_raiz='det', retorna_string=True): raiz = etree.Element(tag_raiz) @@ -224,7 +240,10 @@ class SerializacaoXML(Serializacao): etree.SubElement(prod, 'NCM').text = produto_servico.ncm # Codificação opcional que detalha alguns NCM. Formato: duas letras maiúsculas e 4 algarismos. # Se a mercadoria se enquadrar em mais de uma codificação, informar até 8 codificações principais. - #etree.SubElement(prod, 'NVE').text = '' + # etree.SubElement(prod, 'NVE').text = '' + # etree.SubElement(prod, 'CEST').text = produto_service.cest + if produto_servico.cbenef: + etree.SubElement(prod, 'cBenef').text = produto_servico.cbenef etree.SubElement(prod, 'CFOP').text = produto_servico.cfop etree.SubElement(prod, 'uCom').text = produto_servico.unidade_comercial etree.SubElement(prod, 'qCom').text = str(produto_servico.quantidade_comercial or 0) @@ -239,8 +258,18 @@ class SerializacaoXML(Serializacao): etree.SubElement(prod, 'qTrib').text = str(produto_servico.quantidade_tributavel) etree.SubElement(prod, 'vUnTrib').text = '{:.4f}'.format(produto_servico.valor_unitario_tributavel or 0) + # frete + if produto_servico.total_frete: + etree.SubElement(prod, 'vFrete').text = '{:.2f}'.format(produto_servico.total_frete) + # seguro + if produto_servico.total_seguro: + etree.SubElement(prod, 'vSeg').text = '{:.2f}'.format(produto_servico.total_seguro) + # desconto if produto_servico.desconto: etree.SubElement(prod, 'vDesc').text = '{:.2f}'.format(produto_servico.desconto) + # outras despesas acessórias + if produto_servico.outras_despesas_acessorias: + etree.SubElement(prod, 'vOutro').text = '{:.2f}'.format(produto_servico.outras_despesas_acessorias) """ Indica se valor do Item (vProd) entra no valor total da NF-e (vProd) 0=Valor do item (vProd) não compõe o valor total da NF-e @@ -289,6 +318,11 @@ class SerializacaoXML(Serializacao): icms_item = etree.SubElement(icms, 'ICMSSN'+produto_servico.icms_modalidade) etree.SubElement(icms_item, 'orig').text = str(produto_servico.icms_origem) etree.SubElement(icms_item, 'CSOSN').text = produto_servico.icms_csosn + elif produto_servico.icms_modalidade == '51': + icms_item = etree.SubElement(icms, 'ICMS'+produto_servico.icms_modalidade) + etree.SubElement(icms_item, 'orig').text = str(produto_servico.icms_origem) + etree.SubElement(icms_item, 'CST').text = '51' + etree.SubElement(icms_item, 'modBC').text = str(produto_servico.icms_modalidade_determinacao_bc) else: ### OUTROS TIPOS DE ICMS (00,10,20) icms_item = etree.SubElement(icms, 'ICMS'+produto_servico.icms_modalidade) @@ -331,12 +365,18 @@ class SerializacaoXML(Serializacao): else: raise NotImplementedError # ipi - # ipi = etree.SubElement(imposto, 'IPI') - # etree.SubElement(ipi, 'clEnq') = produto_servico.ipi_classe_enquadramento # Preenchimento conforme Atos Normativos editados pela Receita Federal (Observação 2) - # ipint = etree.SubElement(ipi, 'IPINT') - # # 01=Entrada tributada com alíquota zero 02=Entrada isenta 03=Entrada não-tributada 04=Entrada imune 05=Entrada com suspensão - # # 51=Saída tributada com alíquota zero 52=Saída isenta 53=Saída não-tributada 54=Saída imune 55=Saída com suspensão - # etree.SubElement(ipint, 'CST') = produto_servico.ipi_codigo_enquadramento + ipint_lista = ('01','02','03','04','05','51','52','53','54','55') + if produto_servico.ipi_codigo_enquadramento in ipint_lista: + ipi = etree.SubElement(imposto, 'IPI') + # Preenchimento conforme Atos Normativos editados pela Receita Federal (Observação 2) + etree.SubElement(ipi, 'cEnq').text = produto_servico.ipi_classe_enquadramento + if produto_servico.ipi_classe_enquadramento == '': + etree.SubElement(ipi, 'cEnq').text = '999' + + ipint = etree.SubElement(ipi, 'IPINT') + # 01=Entrada tributada com alíquota zero 02=Entrada isenta 03=Entrada não-tributada 04=Entrada imune 05=Entrada com suspensão + # 51=Saída tributada com alíquota zero 52=Saída isenta 53=Saída não-tributada 54=Saída imune 55=Saída com suspensão + etree.SubElement(ipint, 'CST').text = produto_servico.ipi_codigo_enquadramento # apenas nfe if modelo == 55: @@ -356,7 +396,7 @@ class SerializacaoXML(Serializacao): pis_item = etree.SubElement(pis, 'PISQtde') etree.SubElement(pis_item, 'CST').text = produto_servico.pis_modalidade etree.SubElement(pis_item, 'qBCProd').text = '{:.4f}'.format(produto_servico.quantidade_comercial) - etree.SubElement(pis_item, 'vAliqProd').text = produto_servico.pis_aliquota_percentual + etree.SubElement(pis_item, 'vAliqProd').text = '{:.4f}'.format(produto_servico.pis_aliquota_percentual or 0) etree.SubElement(pis_item, 'vPIS').text = '{:.2f}'.format(produto_servico.pis_valor_base_calculo or 0) else: pis_item = etree.SubElement(pis, 'PISOutr') @@ -365,7 +405,7 @@ class SerializacaoXML(Serializacao): etree.SubElement(pis_item, 'pPIS').text = '{:.2f}'.format(produto_servico.pis_aliquota_percentual or 0) if produto_servico.pis_modalidade is not '99': etree.SubElement(pis_item, 'qBCProd').text = '{:.4f}'.format(produto_servico.quantidade_comercial) - etree.SubElement(pis_item, 'vAliqProd').text = produto_servico.pis_aliquota_percentual + etree.SubElement(pis_item, 'vAliqProd').text = '{:.4f}'.format(produto_servico.pis_aliquota_percentual or 0) etree.SubElement(pis_item, 'vPIS').text = '{:.2f}'.format(produto_servico.pis_valor_base_calculo or 0) ## PISST @@ -400,7 +440,7 @@ class SerializacaoXML(Serializacao): etree.SubElement(cofins_item, 'vBC').text = '{:.2f}'.format(produto_servico.cofins_valor_base_calculo or 0) etree.SubElement(cofins_item, 'pCOFINS').text = '{:.2f}'.format(produto_servico.cofins_aliquota_percentual or 0) if produto_servico.cofins_modalidade is not '99': - etree.SubElement(cofins_item, 'vAliqProd').text = '{:.2f}'.format(produto_servico.cofins_aliquota_percentual or 0) + etree.SubElement(cofins_item, 'vAliqProd').text = '{:.4f}'.format(produto_servico.cofins_aliquota_percentual or 0) etree.SubElement(cofins_item, 'vCOFINS').text = '{:.2f}'.format(produto_servico.cofins_valor or 0) ## COFINSST @@ -531,6 +571,10 @@ class SerializacaoXML(Serializacao): tag_raiz='entrega', )) + # Autorizados a baixar o XML + for num, item in enumerate(nota_fiscal.autorizados_baixar_xml): + raiz.append(self._serializar_autorizados_baixar_xml(item, retorna_string=False)) + # Itens for num, item in enumerate(nota_fiscal.produtos_e_servicos): det = self._serializar_produto_servico(item, modelo=nota_fiscal.modelo, retorna_string=False) @@ -674,8 +718,10 @@ class SerializacaoXML(Serializacao): e = etree.SubElement(raiz, 'infEvento', Id=evento.identificador) etree.SubElement(e, 'cOrgao').text = CODIGOS_ESTADOS[evento.uf.upper()] etree.SubElement(e, 'tpAmb').text = str(self._ambiente) - etree.SubElement(e, 'CNPJ').text = evento.cnpj # Empresas somente terão CNPJ - #etree.SubElement(e, 'CPF').text = '' + if len(so_numeros(evento.cnpj)) == 11: + etree.SubElement(e, 'CPF').text = evento.cnpj + else: + etree.SubElement(e, 'CNPJ').text = evento.cnpj etree.SubElement(e, 'chNFe').text = evento.chave etree.SubElement(e, 'dhEvento').text = evento.data_emissao.strftime('%Y-%m-%dT%H:%M:%S') + tz etree.SubElement(e, 'tpEvento').text = evento.tp_evento diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index d030c3d..dd18879 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -322,7 +322,7 @@ NFE = { 'CHAVE': 'sefaz.ce.gov.br/nfe4/services/NFeConsultaProtocolo4?WSDL', 'INUTILIZACAO': 'sefaz.ce.gov.br/nfe4/services/NFeInutilizacao4?WSDL', 'EVENTOS': 'sefaz.ce.gov.br/nfe4/services/NFeRecepcaoEvento4?WSDL', - 'CADASTRO': 'nfe.sefaz.ce.gov.br/nfe4/services/CadConsultaCadastro4?wsdl', + 'CADASTRO': 'sefaz.ce.gov.br/nfe4/services/CadConsultaCadastro4?wsdl', 'DOWNLOAD': 'sefaz.ce.gov.br/nfe2/services/NfeDownloadNF?wsdl', 'HTTPS': 'https://nfe.', 'HOMOLOGACAO': 'https://nfeh.' @@ -334,7 +334,7 @@ NFE = { 'CHAVE': 'sefaz.pe.gov.br/nfe-service/services/NFeConsultaProtocolo4', 'INUTILIZACAO': 'sefaz.pe.gov.br/nfe-service/services/NFeInutilizacao4', 'EVENTOS': 'sefaz.pe.gov.br/nfe-service/services/NFeRecepcaoEvento4', - # 'CADASTRO': 'sefaz.pe.gov.br/nfe-service/services/CadConsultaCadastro2', + 'CADASTRO': 'sefaz.pe.gov.br/nfe-service/services/CadConsultaCadastro4?wsdl', 'HTTPS': 'https://nfe.', 'HOMOLOGACAO': 'https://nfehomolog.' }, @@ -353,10 +353,10 @@ NFE = { 'STATUS': 'nfe.fazenda.mg.gov.br/nfe2/services/NFeStatusServico4', 'AUTORIZACAO': 'nfe.fazenda.mg.gov.br/nfe2/services/NFeAutorizacao4', 'RECIBO': 'nfe.fazenda.mg.gov.br/nfe2/services/NFeRetAutorizacao4', - 'CHAVE': 'nfe.fazenda.mg.gov.br/nfe2/services/NFeConsulta4', + 'CHAVE': 'nfe.fazenda.mg.gov.br/nfe2/services/NFeConsultaProtocolo4', 'INUTILIZACAO': 'nfe.fazenda.mg.gov.br/nfe2/services/NFeInutilizacao4', 'EVENTOS': 'nfe.fazenda.mg.gov.br/nfe2/services/NFeRecepcaoEvento4', - 'CADASTRO': 'nfe.fazenda.mg.gov.br/nfe2/services/cadconsultacadastro2', + 'CADASTRO': 'nfe.fazenda.mg.gov.br/nfe2/services/CadConsultaCadastro4', 'HTTPS': 'https://', 'HOMOLOGACAO': 'https://h' }, @@ -390,7 +390,7 @@ NFE = { 'CHAVE': 'sefazrs.rs.gov.br/ws/NfeConsulta/NfeConsulta4.asmx', 'INUTILIZACAO': 'sefazrs.rs.gov.br/ws/nfeinutilizacao/nfeinutilizacao4.asmx', 'EVENTOS': 'sefazrs.rs.gov.br/ws/recepcaoevento/recepcaoevento4.asmx', - 'CADASTRO': 'cad.sefazrs.rs.gov.br/ws/cadconsultacadastro/cadconsultacadastro2.asmx', + 'CADASTRO': 'https://cad.sefazrs.rs.gov.br/ws/cadconsultacadastro/cadconsultacadastro4.asmx', 'DOWNLOAD': 'sefazrs.rs.gov.br/ws/nfeDownloadNF/nfeDownloadNF.asmx', 'DESTINADAS': 'sefazrs.rs.gov.br/ws/nfeConsultaDest/nfeConsultaDest.asmx', 'HTTPS': 'https://nfe.', @@ -447,7 +447,7 @@ NFE = { 'CHAVE': 'svrs.rs.gov.br/ws/NfeConsulta/NfeConsulta4.asmx', 'INUTILIZACAO': 'svrs.rs.gov.br/ws/nfeinutilizacao/nfeinutilizacao4.asmx', 'EVENTOS': 'svrs.rs.gov.br/ws/recepcaoevento/recepcaoevento4.asmx', - 'CADASTRO': 'https://cad.svrs.rs.gov.br/ws/cadconsultacadastro/cadconsultacadastro2.asmx', + 'CADASTRO': 'https://cad.svrs.rs.gov.br/ws/cadconsultacadastro/cadconsultacadastro4.asmx', 'HTTPS': 'https://nfe.', 'HOMOLOGACAO': 'https://nfe-homologacao.' }, diff --git a/requirements-nfse.txt b/requirements-nfse.txt index 582da17..15f3243 100644 --- a/requirements-nfse.txt +++ b/requirements-nfse.txt @@ -1,3 +1,3 @@ # Opcional para NFS-e suds-jurko -pyxb=1.2.4 +pyxb==1.2.4 diff --git a/setup.py b/setup.py index 94eab7e..43da2e2 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,28 @@ #!/usr/bin/env python -from setuptools import setup, find_packages -try: # for pip >= 10 - from pip._internal.req import parse_requirements as parse -except ImportError: # for pip <= 9.0.3 - from pip.req import parse_requirements as parse +import setuptools -requirements = lambda f: [str(i.req) for i in parse(f, session=False)] - -setup( +setuptools.setup( name='PyNFe', version='0.4', - packages=find_packages(), + author='TadaSoftware', + author_email='tadasoftware@gmail.com', + url='https://github.com/TadaSoftware', + packages=setuptools.find_packages(exclude=['tests', 'tests.*']), package_data={ 'pynfe': ['data/**/*.txt', 'data/**/*.xsd', 'danfe/**/*.ttf'], }, - install_requires=requirements('requirements.txt'), + install_requires=[ + 'pyopenssl', + 'requests', + 'lxml', + 'signxml', + ], + extras_require={ + 'nfse': [ + 'suds-jurko', + 'pyxb==1.2.4', + ], + }, zip_safe=False, + python_requires='>=3.6', )