From 1d38a8cfd623c17a70dc39524a956d2ffd79e8c7 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Thu, 25 Jun 2020 10:57:48 -0400 Subject: [PATCH 01/29] =?UTF-8?q?feat:=20Criada=20a=20comunica=C3=A7=C3=A3?= =?UTF-8?q?o=20com=20o=20webservice=20do=20MDF-e.=20Separa=C3=A7=C3=A3o=20?= =?UTF-8?q?das=20classes=20de=20NFe,=20NFSe=20e=20MDFe.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/entidades/evento.py | 111 ++++++- pynfe/processamento/__init__.py | 5 +- pynfe/processamento/comunicacao.py | 647 +----------------------------------- pynfe/processamento/mdfe.py | 214 ++++++++++++ pynfe/processamento/nfe.py | 437 ++++++++++++++++++++++++ pynfe/processamento/nfse.py | 234 +++++++++++++ pynfe/processamento/resposta.py | 28 ++ pynfe/processamento/serializacao.py | 104 +++++- pynfe/utils/__init__.py | 14 +- pynfe/utils/flags.py | 5 + pynfe/utils/webservices.py | 15 + 11 files changed, 1175 insertions(+), 639 deletions(-) create mode 100644 pynfe/processamento/mdfe.py create mode 100644 pynfe/processamento/nfe.py create mode 100644 pynfe/processamento/nfse.py create mode 100644 pynfe/processamento/resposta.py diff --git a/pynfe/entidades/evento.py b/pynfe/entidades/evento.py index ffe7410..473f810 100644 --- a/pynfe/entidades/evento.py +++ b/pynfe/entidades/evento.py @@ -4,8 +4,10 @@ @author: Junior Tada, Leonardo Tada """ +from decimal import Decimal from .base import Entidade + class Evento(Entidade): # - Identificador da TAG a ser assinada, a regra de formação do Id é: “ID” + tpEvento + chave da NF-e + nSeqEvento id = str() @@ -114,4 +116,111 @@ class EventoManifestacaoDest(Evento): # - Informar a justificativa porque a operação não foi realizada, este campo deve ser informado somente no evento de Operação não Realizada. (min 15 max 255 caracteres) justificativa = str() - \ No newline at end of file + + +class EventoEncerramento(Evento): + + def __init__(self, *args, **kwargs): + super(EventoEncerramento, self).__init__(*args, **kwargs) + # - Código do evento = 110112 + self.tp_evento = '110112' + # - "Encerramento" + self.descricao = 'Encerramento' + + # - Informar o número do Protocolo de Autorização da MDF-e a ser Encerrada + protocolo = str() + # - Data e hora do evento no formato AAAA-MM-DDThh:mm:ssTZD + dtenc = None + # - uf de onde a manifesto foi encerrado + cuf = str() + # - minicipio onde o manifesto foi encerrado + cmun = str() + + +class EventoInclusaoCondutor(Evento): + + def __init__(self, *args, **kwargs): + super(EventoInclusaoCondutor, self).__init__(*args, **kwargs) + # - Código do evento = 110114 + self.tp_evento = '110114' + # - "Encerramento" + self.descricao = 'Inclusão Condutor' + + # - Nome do motorista + nome_motorista = str() + # - CPF do motorista + cpf_motorista = str() + + +class EventoInclusaoDFe(Evento): + + def __init__(self, *args, **kwargs): + super(EventoInclusaoDFe, self).__init__(*args, **kwargs) + # - Código do evento = 110115 + self.tp_evento = '110115' + # - "Inclusao DF-e" + self.descricao = 'Inclusao DF-e' + + # - Informar o número do Protocolo de Autorização da MDF-e a ser Incluida nova NF-e + protocolo = str() + # - Código IBGE do Município de Carregamento + cmun_carrega = str() + # - Nome do Município de Carregamento + xmun_carrega = str() + # - Código IBGE do Município de Descarga + cmun_descarga = str() + # - Nome do Município de Descarga + xmun_descarga = str() + # - Chave de Acesso da NF-e a ser incluída no MDFe + chave_nfe = str() + + +class EventoInclusaoPagamento(Evento): + + def __init__(self, *args, **kwargs): + super(EventoInclusaoPagamento, self).__init__(*args, **kwargs) + # - Código do evento = 110116 + self.tp_evento = '110116' + # - "Pagamento Operacao MDF-e" + self.descricao = 'Pagamento Operacao MDF-e' + + # - Informar o número do Protocolo de Autorização da MDF-e a ser Incluida nova NF-e + protocolo = str() + + # - Quantidade de viagens + qtd_viagens = str() + # - Número da viagem + nro_viagens = str() + + # Informações do pagamento + # - Nome do Contratante + nome_contratante = str() + # - CPF/CNPJ do Contratante + cpfcnpj_contratante = str() + + # Componentes do Pagamento + # - Tipo do pagamento + tpComp = str() + # - Valor + vComp = Decimal() + + # - Valor total do contrato + vContrato = Decimal() + # - Tipo do pagamento (0=a vista e 1=a prazo) + indPag = str() + + # Se o pagamento for a prazo + # - Numero da parcela + nParcela = str() + # - Data vencimento + dVenc = None + # - Valor da parcela + vParcela = Decimal() + + # Informações bancárias + # - CNPJ da Instituição de Pagamento eletrônico do Frete + CNPJIPEF = str() + # - Código do Banco + codBanco = str() + # - Código da Agência + codAgencia = str() diff --git a/pynfe/processamento/__init__.py b/pynfe/processamento/__init__.py index e63097d..7dfb5f6 100644 --- a/pynfe/processamento/__init__.py +++ b/pynfe/processamento/__init__.py @@ -2,5 +2,8 @@ from .serializacao import SerializacaoXML from .serializacao import SerializacaoNfse from .validacao import Validacao from .assinatura import AssinaturaA1 -from .comunicacao import ComunicacaoSefaz +from pynfe.entidades.certificado import CertificadoA1 +from .nfe import ComunicacaoNFe +from .mdfe import ComunicacaoMDFe +from .nfse import ComunicacaoNfse from .danfe import DanfeNfce diff --git a/pynfe/processamento/comunicacao.py b/pynfe/processamento/comunicacao.py index 2a9bd41..41cc426 100644 --- a/pynfe/processamento/comunicacao.py +++ b/pynfe/processamento/comunicacao.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -import re import ssl import datetime -import requests + from pynfe.utils import etree, so_numeros from pynfe.utils.flags import ( NAMESPACE_NFE, @@ -15,14 +14,13 @@ from pynfe.utils.flags import ( NAMESPACE_METODO ) from pynfe.utils.webservices import NFE, NFCE, NFSE -from pynfe.entidades.certificado import CertificadoA1 from .assinatura import AssinaturaA1 class Comunicacao(object): """ Classe abstrata responsavel por definir os metodos e logica das classes - de comunicação com os webservices da NF-e. + de comunicação com os webservices. """ _ambiente = 1 # 1 = Produção, 2 = Homologação @@ -30,637 +28,22 @@ class Comunicacao(object): certificado = None certificado_senha = None url = None + _versao = False + _assinatura = AssinaturaA1 + _namespace = False + _header = False + _envio_mensagem = False + _namespace_metodo = False + _accept = False + _soap_action = False + _ws_url = False + _namespace_soap = NAMESPACE_SOAP + _namespace_xsi = NAMESPACE_XSI + _namespace_xsd = NAMESPACE_XSD + _soap_version = 'soap' def __init__(self, uf, certificado, certificado_senha, homologacao=False): self.uf = uf self.certificado = certificado self.certificado_senha = certificado_senha self._ambiente = 2 if homologacao else 1 - - -class ComunicacaoSefaz(Comunicacao): - """Classe de comunicação que segue o padrão definido para as SEFAZ dos Estados.""" - - _versao = VERSAO_PADRAO - _assinatura = AssinaturaA1 - - def autorizacao(self, modelo, nota_fiscal, id_lote=1, ind_sinc=1): - """ - Método para realizar autorização da nota de acordo com o modelo - :param modelo: Modelo - :param nota_fiscal: XML assinado - :param id_lote: Id do lote - numero autoincremental gerado pelo sistema - :param ind_sinc: Indicador de sincrono e assincrono, 0 para assincrono, 1 para sincrono - :return: Uma tupla que em caso de sucesso, retorna xml com nfe e protocolo de autorização. Caso contrário, - envia todo o soap de resposta da Sefaz para decisão do usuário. - """ - # url do serviço - url = self._get_url(modelo=modelo, consulta='AUTORIZACAO') - - # Monta XML do corpo da requisição - raiz = etree.Element('enviNFe', xmlns=NAMESPACE_NFE, versao=VERSAO_PADRAO) - etree.SubElement(raiz, 'idLote').text = str(id_lote) # numero autoincremental gerado pelo sistema - etree.SubElement(raiz, 'indSinc').text = str(ind_sinc) # 0 para assincrono, 1 para sincrono - raiz.append(nota_fiscal) - - # Monta XML para envio da requisição - xml = self._construir_xml_soap('NFeAutorizacao4', raiz) - # Faz request no Servidor da Sefaz - retorno = self._post(url, xml) - - # Em caso de sucesso, retorna xml com nfe e protocolo de autorização. - # Caso contrário, envia todo o soap de resposta da Sefaz para decisão do usuário. - if retorno.status_code == 200: - # namespace - ns = {'ns': NAMESPACE_NFE} - # Procuta status no xml - try: - prot = etree.fromstring(retorno.text) - except ValueError: - # em SP retorno.text apresenta erro - prot = etree.fromstring(retorno.content) - if ind_sinc == 1: - try: - # Protocolo com envio OK - try: - inf_prot = prot[0][0] # root protNFe - except IndexError: - # Estados como GO vem com a tag header - inf_prot = prot[1][0] - - lote_status = inf_prot.xpath("ns:retEnviNFe/ns:cStat", namespaces=ns)[0].text - # Lote processado - if lote_status == '104': - prot_nfe = inf_prot.xpath("ns:retEnviNFe/ns:protNFe", namespaces=ns)[0] - status = prot_nfe.xpath('ns:infProt/ns:cStat', namespaces=ns)[0].text - # autorizado usa da NF-e - # retorna xml final (protNFe+NFe) - if status == '100': - raiz = etree.Element('nfeProc', xmlns=NAMESPACE_NFE, versao=VERSAO_PADRAO) - raiz.append(nota_fiscal) - raiz.append(prot_nfe) - return 0, raiz - except IndexError: - # Protocolo com algum erro no Envio - return 1, retorno, nota_fiscal - else: - # Retorna id do protocolo para posterior consulta em caso de sucesso. - rec = prot[0][0] - status = rec.xpath("ns:retEnviNFe/ns:cStat", namespaces=ns)[0].text - # Lote Recebido com Sucesso! - if status == '103': - nrec = rec.xpath("ns:retEnviNFe/ns:infRec/ns:nRec", namespaces=ns)[0].text - return 0, nrec, nota_fiscal - return 1, retorno, nota_fiscal - - def consulta_recibo(self, modelo, numero): - """ - Este método oferece a consulta do resultado do processamento de um lote de NF-e. - O aplicativo do Contribuinte deve ser construído de forma a aguardar um tempo mínimo de - 15 segundos entre o envio do Lote de NF-e para processamento e a consulta do resultado - deste processamento, evitando a obtenção desnecessária do status de erro 105 - "Lote em - Processamento". - :param modelo: Modelo da nota - :param numero: Número da nota - :return: - """ - - # url do serviço - url = self._get_url(modelo=modelo, consulta='RECIBO') - - # Monta XML do corpo da requisição - raiz = etree.Element('consReciNFe', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) - etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) - etree.SubElement(raiz, 'nRec').text = numero - - # Monta XML para envio da requisição - xml = self._construir_xml_soap('NFeRetAutorizacao4', raiz) - return self._post(url, xml) - - def consulta_nota(self, modelo, chave): - """ - Este método oferece a consulta da situação da NF-e/NFC-e na Base de Dados do Portal - da Secretaria de Fazenda Estadual. - :param modelo: Modelo da nota - :param chave: Chave da nota - :return: - """ - # url do serviço - url = self._get_url(modelo=modelo, consulta='CHAVE') - # Monta XML do corpo da requisição - raiz = etree.Element('consSitNFe', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) - etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) - etree.SubElement(raiz, 'xServ').text = 'CONSULTAR' - etree.SubElement(raiz, 'chNFe').text = chave - # Monta XML para envio da requisição - xml = self._construir_xml_soap('NFeConsultaProtocolo4', raiz) - return self._post(url, xml) - - def consulta_distribuicao(self, cnpj=None, cpf=None, chave=None, nsu=0): - """ - O XML do pedido de distribuição suporta três tipos de consultas que são definidas de acordo com a tag - informada no XML. As tags são distNSU, consNSU e consChNFe. - a) distNSU – Distribuição de Conjunto de DF-e a Partir do NSU Informado - b) consNSU – Consulta DF-e Vinculado ao NSU Informado - c) consChNFe – Consulta de NF-e por Chave de Acesso Informada - :param cnpj: CNPJ do interessado - :param cpf: CPF do interessado - :param chave: Chave da NF-e a ser consultada - :param nsu: Ultimo nsu ou nsu específico para ser consultado. - :return: - """ - # url - url = self._get_url_an(consulta='DISTRIBUICAO') - # Monta XML para envio da requisição - raiz = etree.Element('distDFeInt', versao='1.01', xmlns=NAMESPACE_NFE) - etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) - if self.uf: - etree.SubElement(raiz, 'cUFAutor').text = CODIGOS_ESTADOS[self.uf.upper()] - if cnpj: - etree.SubElement(raiz, 'CNPJ').text = cnpj - else: - etree.SubElement(raiz, 'CPF').text = cpf - if not chave: - distNSU = etree.SubElement(raiz, 'distNSU') - etree.SubElement(distNSU, 'ultNSU').text = str(nsu).zfill(15) - if chave: - consChNFe = etree.SubElement(raiz, 'consChNFe') - etree.SubElement(consChNFe, 'chNFe').text = chave - #Monta XML para envio da requisição - xml = self._construir_xml_soap('NFeDistribuicaoDFe', raiz) - - - return self._post(url, xml) - - def consulta_cadastro(self, modelo, cnpj): - """ - Consulta de cadastro - :param modelo: Modelo da nota - :param cnpj: CNPJ da empresa - :return: - """ - # UF que utilizam a SVRS - Sefaz Virtual do RS: Para serviço de Consulta Cadastro: AC, RN, PB, SC - lista_svrs = ['AC', 'RN', 'PB', 'SC'] - - # RS implementa um método diferente na consulta de cadastro - if self.uf.upper() == 'RS': - url = NFE['RS']['CADASTRO'] - elif self.uf.upper() in lista_svrs: - url = NFE['SVRS']['CADASTRO'] - elif self.uf.upper() == 'SVC-RS': - url = NFE['SVC-RS']['CADASTRO'] - else: - url = self._get_url(modelo=modelo, consulta='CADASTRO') - - raiz = etree.Element('ConsCad', versao='2.00', xmlns=NAMESPACE_NFE) - info = etree.SubElement(raiz, 'infCons') - etree.SubElement(info, 'xServ').text = 'CONS-CAD' - etree.SubElement(info, 'UF').text = self.uf.upper() - etree.SubElement(info, 'CNPJ').text = cnpj - # etree.SubElement(info, 'CPF').text = cpf - - # Monta XML para envio da requisição - xml = self._construir_xml_soap('CadConsultaCadastro4', raiz) - # Chama método que efetua a requisição POST no servidor SOAP - return self._post(url, xml) - - def evento(self, modelo, evento, id_lote=1): - """ - Envia um evento de nota fiscal (cancelamento e carta de correção) - :param modelo: Modelo da nota - :param evento: Eventro - :param id_lote: Id do lote - :return: - """ - - # url do serviço - try: - # manifestacao url é do AN - if evento[0][5].text.startswith('2'): - url = self._get_url_an(consulta='EVENTOS') - else: - url = self._get_url(modelo=modelo, consulta='EVENTOS') - except Exception: - url = self._get_url(modelo=modelo, consulta='EVENTOS') - - # Monta XML do corpo da requisição - raiz = etree.Element('envEvento', versao='1.00', xmlns=NAMESPACE_NFE) - etree.SubElement(raiz, 'idLote').text = str(id_lote) # numero autoincremental gerado pelo sistema - raiz.append(evento) - xml = self._construir_xml_soap('NFeRecepcaoEvento4', raiz) - return self._post(url, xml) - - def status_servico(self, modelo): - """ - Verifica status do servidor da receita. - :param modelo: modelo é a string com tipo de serviço que deseja consultar, Ex: nfe ou nfce - :return: - """ - url = self._get_url(modelo, 'STATUS') - # Monta XML do corpo da requisição - raiz = etree.Element('consStatServ', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) - etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) - etree.SubElement(raiz, 'cUF').text = CODIGOS_ESTADOS[self.uf.upper()] - etree.SubElement(raiz, 'xServ').text = 'STATUS' - xml = self._construir_xml_soap('NFeStatusServico4', raiz) - return self._post(url, xml) - - def inutilizacao(self, modelo, cnpj, numero_inicial, numero_final, justificativa='', ano=None, serie='1'): - """ - Serviço destinado ao atendimento de solicitações de inutilização de numeração. - :param modelo: Modelo da nota - :param cnpj: CNPJda empresa - :param numero_inicial: Número inicial - :param numero_final: Número final - :param justificativa: Justificativa - :param ano: Ano - :param serie: Série - :return: - """ - - # url do servico - url = self._get_url(modelo=modelo, consulta='INUTILIZACAO') - - # Valores default - ano = str(ano or datetime.date.today().year)[-2:] - uf = CODIGOS_ESTADOS[self.uf.upper()] - cnpj = so_numeros(cnpj) - - # Identificador da TAG a ser assinada formada com Código da UF + Ano (2 posições) + - # CNPJ + modelo + série + nro inicial e nro final precedida do literal “ID” - id_unico = 'ID%(uf)s%(ano)s%(cnpj)s%(modelo)s%(serie)s%(num_ini)s%(num_fin)s' % { - 'uf': uf, - 'ano': ano, - 'cnpj': cnpj, - 'modelo': '55' if modelo == 'nfe' else '65', # 55=NF-e; 65=NFC-e; - 'serie': serie.zfill(3), - 'num_ini': str(numero_inicial).zfill(9), - 'num_fin': str(numero_final).zfill(9), - } - - # Monta XML do corpo da requisição # FIXME - raiz = etree.Element('inutNFe', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) - inf_inut = etree.SubElement(raiz, 'infInut', Id=id_unico) - etree.SubElement(inf_inut, 'tpAmb').text = str(self._ambiente) - etree.SubElement(inf_inut, 'xServ').text = 'INUTILIZAR' - etree.SubElement(inf_inut, 'cUF').text = uf - etree.SubElement(inf_inut, 'ano').text = ano - etree.SubElement(inf_inut, 'CNPJ').text = cnpj - etree.SubElement(inf_inut, 'mod').text = '55' if modelo == 'nfe' else '65' # 55=NF-e; 65=NFC-e - etree.SubElement(inf_inut, 'serie').text = serie - etree.SubElement(inf_inut, 'nNFIni').text = str(numero_inicial) - etree.SubElement(inf_inut, 'nNFFin').text = str(numero_final) - etree.SubElement(inf_inut, 'xJust').text = justificativa - - # assinatura - a1 = AssinaturaA1(self.certificado, self.certificado_senha) - xml = a1.assinar(raiz) - - # Monta XML para envio da requisição - xml = self._construir_xml_soap('NFeInutilizacao4', xml) - # Faz request no Servidor da Sefaz e retorna resposta - return self._post(url, xml) - - def _get_url_an(self, consulta): - # producao - if self._ambiente == 1: - if consulta == 'DISTRIBUICAO': - ambiente = 'https://www1.' - else: - ambiente = 'https://www.' - # homologacao - else: - ambiente = 'https://hom.' - - self.url = ambiente + NFE['AN'][consulta] - return self.url - - def _get_url(self, modelo, consulta): - """ Retorna a url para comunicação com o webservice """ - # estado que implementam webservices proprios - lista = ['PR', 'MS', 'SP', 'AM', 'CE', 'BA', 'GO', 'MG', 'MT', 'PE', 'RS'] - if self.uf.upper() in lista: - if self._ambiente == 1: - ambiente = 'HTTPS' - else: - ambiente = 'HOMOLOGACAO' - if modelo == 'nfe': - # nfe Ex: https://nfe.fazenda.pr.gov.br/nfe/NFeStatusServico3 - self.url = NFE[self.uf.upper()][ambiente] + NFE[self.uf.upper()][consulta] - elif modelo == 'nfce': - # PE e BA são as únicas UF'sque possuem NFE proprio e SVRS para NFCe - if self.uf.upper() == 'PE' or self.uf.upper() == 'BA': - self.url = NFCE['SVRS'][ambiente] + NFCE['SVRS'][consulta] - else: - # nfce Ex: https://homologacao.nfce.fazenda.pr.gov.br/nfce/NFeStatusServico3 - self.url = NFCE[self.uf.upper()][ambiente] + NFCE[self.uf.upper()][consulta] - else: - raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') - # Estados que utilizam outros ambientes - else: - lista_svrs = ['AC', 'AL', 'AP', 'DF', 'ES', 'PB', 'PI', 'RJ', 'RN', 'RO', 'RR', 'SC', 'SE', 'TO'] - if self.uf.upper() in lista_svrs: - if self._ambiente == 1: - ambiente = 'HTTPS' - else: - ambiente = 'HOMOLOGACAO' - if modelo == 'nfe': - # nfe Ex: https://nfe.fazenda.pr.gov.br/nfe/NFeStatusServico3 - self.url = NFE['SVRS'][ambiente] + NFE['SVRS'][consulta] - elif modelo == 'nfce': - # nfce Ex: https://homologacao.nfce.fazenda.pr.gov.br/nfce/NFeStatusServico3 - self.url = NFCE['SVRS'][ambiente] + NFCE['SVRS'][consulta] - else: - raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') - # 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: - ambiente = 'HOMOLOGACAO' - if modelo == 'nfe': - # nfe Ex: https://nfe.fazenda.pr.gov.br/nfe/NFeStatusServico3 - 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['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): - """Mota o XML para o envio via SOAP""" - raiz = etree.Element('{%s}Envelope' % NAMESPACE_SOAP, nsmap={ - 'xsi': NAMESPACE_XSI, 'xsd': NAMESPACE_XSD,'soap': NAMESPACE_SOAP}) - body = etree.SubElement(raiz, '{%s}Body' % NAMESPACE_SOAP) - ## distribuição tem um corpo de xml diferente - if metodo == 'NFeDistribuicaoDFe': - x = etree.SubElement(body, 'nfeDistDFeInteresse', xmlns=NAMESPACE_METODO+metodo) - a = etree.SubElement(x, 'nfeDadosMsg') - else: - a = etree.SubElement(body, 'nfeDadosMsg', xmlns=NAMESPACE_METODO+metodo) - a.append(dados) - return raiz - - def _post_header(self): - """Retorna um dicionário com os atributos para o cabeçalho da requisição HTTP""" - # PE é a única UF que exige SOAPAction no header - response = { - 'content-type': 'application/soap+xml; charset=utf-8;', - 'Accept': 'application/soap+xml; charset=utf-8;', - } - if self.uf.upper() == 'PE': - response["SOAPAction"] = "" - return response - - def _post(self, url, xml): - certificado_a1 = CertificadoA1(self.certificado) - chave, cert = certificado_a1.separar_arquivo(self.certificado_senha, caminho=True) - chave_cert = (cert, chave) - # Abre a conexão HTTPS - try: - xml_declaration = '' - - # limpa xml com caracteres bugados para infNFeSupl em NFC-e - xml = re.sub( - '(.*?)', - lambda x: x.group(0).replace('<', '<').replace('>', '>').replace('&', ''), - etree.tostring(xml, encoding='unicode').replace('\n', '') - ) - xml = xml_declaration + xml - # Faz o request com o servidor - result = requests.post(url, xml, headers=self._post_header(), cert=chave_cert, verify=False) - result.encoding = 'utf-8' - return result - except requests.exceptions.RequestException as e: - raise e - finally: - certificado_a1.excluir() - - -class ComunicacaoNfse(Comunicacao): - """ Classe de comunicação que segue o padrão definido para as SEFAZ dos Municípios. """ - - _versao = '' - _namespace = '' - - def __init__(self, certificado, certificado_senha, autorizador, homologacao=False): - self.certificado = certificado - self.certificado_senha = certificado_senha - self._ambiente = 2 if homologacao else 1 - self.autorizador = autorizador.upper() - if self.autorizador == 'GINFES': - self._namespace = 'http://www.ginfes.com.br/cabecalho_v03.xsd' - self._versao = '3' - elif self.autorizador == 'BETHA': - self._namespace = NAMESPACE_BETHA - self._versao = '2.02' - else: - raise Exception('Autorizador não encontrado!') - - def autorizacao(self, nota): - # url do serviço - url = self._get_url() - if self.autorizador == 'BETHA': - # xml - xml = etree.tostring(nota, encoding='unicode', pretty_print=False) - # comunica via wsdl - return self._post(url, xml, 'gerar') - else: - raise Exception('Este método só esta implementado no autorizador betha.') - - def enviar_lote(self, xml): - # url do serviço - url = self._get_url() - if self.autorizador == 'GINFES': - # xml - xml = '' + xml - # comunica via wsdl - return self._post_https(url, xml, 'enviar_lote') - else: - raise Exception('Este método só esta implementado no autorizador ginfes.') - - def consultar(self, xml): - # url do serviço - url = self._get_url() - if self.autorizador == 'GINFES': - # xml - xml = '' + xml - # comunica via wsdl - return self._post_https(url, xml, 'consulta') - else: - raise Exception('Este método só esta implementado no autorizador ginfes.') - - def consultar_rps(self, xml): - # url do serviço - url = self._get_url() - if self.autorizador == 'BETHA': - # comunica via wsdl - return self._post(url, xml, 'consultaRps') - elif self.autorizador == 'GINFES': - return self._post_https(url, xml, 'consultaRps') - # TODO outros autorizadres - else: - raise Exception('Autorizador não encontrado!') - - def consultar_faixa(self, xml): - # url do serviço - url = self._get_url() - if self.autorizador == 'BETHA': - # comunica via wsdl - return self._post(url, xml, 'consultaFaixa') - else: - raise Exception('Este método só esta implementado no autorizador betha.') - - def consultar_lote(self, xml): - # url do serviço - url = self._get_url() - if self.autorizador == 'GINFES': - # xml - xml = '' + xml - # comunica via wsdl - return self._post_https(url, xml, 'consulta_lote') - else: - raise Exception('Este método só esta implementado no autorizador ginfes.') - - def consultar_situacao_lote(self, xml): - # url do serviço - url = self._get_url() - if self.autorizador == 'GINFES': - # comunica via wsdl - return self._post_https(url, xml, 'consulta_situacao_lote') - else: - raise Exception('Este método só esta implementado no autorizador ginfes.') - - def cancelar(self, xml): - # url do serviço - url = self._get_url() - # Betha - if self.autorizador == 'BETHA': - # comunica via wsdl - return self._post(url, xml, 'cancelar') - # Ginfes - elif self.autorizador == 'GINFES': - # comunica via wsdl com certificado - return self._post_https(url, xml, 'cancelar') - # TODO outros autorizadres - else: - raise Exception('Autorizador não encontrado!') - - def _cabecalho(self, retorna_string=True): - """ Monta o XML do cabeçalho da requisição wsdl - Namespaces padrão homologação (Ginfes) """ - - xml_declaration = '' - # cabecalho = '3' - # cabecalho - raiz = etree.Element('{%s}cabecalho'%self._namespace, nsmap={'ns2':self._namespace, 'xsi':NAMESPACE_XSI}, versao=self._versao) - etree.SubElement(raiz, 'versaoDados').text = self._versao - - if retorna_string: - cabecalho = etree.tostring(raiz, encoding='unicode', pretty_print=False).replace('\n','') - cabecalho = xml_declaration + cabecalho - return cabecalho - else: - return raiz - - def _cabecalho2(self, retorna_string=True): - """ Monta o XML do cabeçalho da requisição wsdl - Namespaces que funcionaram em produção (Ginfes)""" - - xml_declaration = '' - - # cabecalho - raiz = etree.Element('cabecalho', xmlns=self._namespace, versao=self._versao) - etree.SubElement(raiz, 'versaoDados').text = self._versao - - if retorna_string: - cabecalho = etree.tostring(raiz, encoding='unicode', pretty_print=False).replace('\n', '') - cabecalho = xml_declaration + cabecalho - return cabecalho - else: - return raiz - - def _cabecalho_ginfes(self): - """ Retorna o XML do cabeçalho gerado pelo xsd""" - from pynfe.processamento.autorizador_nfse import SerializacaoGinfes - return SerializacaoGinfes().cabecalho() - - def _get_url(self): - """ Retorna a url para comunicação com o webservice """ - if self._ambiente == 1: - ambiente = 'HTTPS' - else: - ambiente = 'HOMOLOGACAO' - if self.autorizador in NFSE: - self.url = NFSE[self.autorizador][ambiente] - else: - raise Exception('Autorizador nao encontrado!') - return self.url - - def _post(self, url, xml, metodo): - """ Comunicação wsdl (http) sem certificado digital """ - # cabecalho - cabecalho = self._cabecalho() - # comunicacao wsdl - try: - from suds.client import Client - cliente = Client(url) - # gerar nfse - if metodo == 'gerar': - return cliente.service.GerarNfse(cabecalho, xml) - elif metodo == 'consultaRps': - return cliente.service.ConsultarNfsePorRps(cabecalho, xml) - elif metodo == 'consultaFaixa': - return cliente.service.ConsultarNfseFaixa(cabecalho, xml) - elif metodo == 'cancelar': - return cliente.service.CancelarNfse(cabecalho, xml) - # TODO outros metodos - else: - raise Exception('Método não implementado no autorizador.') - except Exception as e: - raise e - - def _post_https(self, url, xml, metodo): - """ Comunicação wsdl (https) utilizando certificado do usuário """ - # cabecalho - cabecalho = self._cabecalho() - # comunicacao wsdl - try: - from suds.client import Client - from pynfe.utils.https_nfse import HttpAuthenticated - - certificadoA1 = CertificadoA1(self.certificado) - chave, cert = certificadoA1.separar_arquivo(self.certificado_senha, caminho=True) - - cliente = Client(url, transport = HttpAuthenticated(key=chave, cert=cert, endereco=url)) - - # gerar nfse - if metodo == 'gerar': - return cliente.service.GerarNfse(cabecalho, xml) - elif metodo == 'enviar_lote': - return cliente.service.RecepcionarLoteRpsV3(cabecalho, xml) - elif metodo == 'consulta': - return cliente.service.ConsultarNfseV3(cabecalho, xml) - elif metodo == 'consulta_lote': - return cliente.service.ConsultarLoteRpsV3(cabecalho, xml) - elif metodo == 'consulta_situacao_lote': - return cliente.service.ConsultarSituacaoLoteRpsV3(cabecalho, xml) - elif metodo == 'consultaRps': - return cliente.service.ConsultarNfsePorRpsV3(cabecalho, xml) - elif metodo == 'consultaFaixa': - return cliente.service.ConsultarNfseFaixa(cabecalho, xml) - elif metodo == 'cancelar': - # versão 2 - return cliente.service.CancelarNfse(xml) - # versão 3 - # return cliente.service.CancelarNfseV3(cabecalho, xml) - # TODO outros metodos - else: - raise Exception('Método não implementado no autorizador.') - except Exception as e: - raise e diff --git a/pynfe/processamento/mdfe.py b/pynfe/processamento/mdfe.py new file mode 100644 index 0000000..ab5e153 --- /dev/null +++ b/pynfe/processamento/mdfe.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +import time +import re +import requests +import collections +from io import StringIO + + +from pynfe.utils.flags import ( + NAMESPACE_MDFE, + MODELO_MDFE, + VERSAO_MDFE, + NAMESPACE_MDFE_METODO, + NAMESPACE_SOAP, + NAMESPACE_XSI, + NAMESPACE_XSD, + CODIGOS_ESTADOS +) +from pynfe.utils.webservices import MDFE +from pynfe.entidades.certificado import CertificadoA1 +from pynfe.utils import etree, extrai_id_srtxml +from .comunicacao import Comunicacao +from .resposta import analisar_retorno + +MDFE_SITUACAO_JA_ENVIADO = ('100', '101', '132') + + +class ComunicacaoMDFe(Comunicacao): + + _modelo = MODELO_MDFE + _namespace = NAMESPACE_MDFE + _versao = VERSAO_MDFE + _header = 'mdfeCabecMsg' + _envio_mensagem = 'mdfeDadosMsg' + _retorno_mensagem = 'mdfeRecepcaoResult' + _namespace_metodo = NAMESPACE_MDFE_METODO + + _accept = True + _soap_action = False + _namespace_soap = NAMESPACE_SOAP + _namespace_xsi = NAMESPACE_XSI + _namespace_xsd = NAMESPACE_XSD + _soap_version = 'soap12' + _edoc_situacao_ja_enviado = MDFE_SITUACAO_JA_ENVIADO + _edoc_situacao_arquivo_recebido_com_sucesso = '103' + _edoc_situacao_em_processamento = '105' + _edoc_situacao_servico_em_operacao = '107' + + consulta_servico_ao_enviar = True + maximo_tentativas_consulta_recibo = 5 + + def status_servico(self): + url = self._get_url('STATUS') + # Monta XML do corpo da requisição + raiz = etree.Element('consStatServMDFe', versao=self._versao, xmlns=NAMESPACE_MDFE) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + etree.SubElement(raiz, 'xServ').text = 'STATUS' + xml = self._construir_xml_soap('MDFeStatusServico', raiz) + return self._post(url, xml) + + def consulta(self, chave): + url = self._get_url('CONSULTA') + # Monta XML do corpo da requisição + raiz = etree.Element('consSitMDFe', versao=self._versao, xmlns=NAMESPACE_MDFE) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + etree.SubElement(raiz, 'xServ').text = 'CONSULTAR' + etree.SubElement(raiz, 'chMDFe').text = chave + # Monta XML para envio da requisição + xml = self._construir_xml_soap('MDFeConsulta', raiz) + return self._post(url, xml) + + def consulta_nao_encerrados(self, cpfcnpj): + url = self._get_url('NAO_ENCERRADOS') + # Monta XML do corpo da requisição + # raiz = etree.Element('consMDFeNaoEnc', xmlns=NAMESPACE_MDFE, versao=self._versao) + attr = collections.OrderedDict() + attr['xmlns'] = NAMESPACE_MDFE + attr['versao'] = self._versao + raiz = etree.Element('consMDFeNaoEnc', attr) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + etree.SubElement(raiz, 'xServ').text = 'CONSULTAR NÃO ENCERRADOS' + if len(cpfcnpj) == 11: + etree.SubElement(raiz, 'CPF').text = cpfcnpj.zfill(11) + else: + etree.SubElement(raiz, 'CNPJ').text = cpfcnpj.zfill(14) + # Monta XML para envio da requisição + xml = self._construir_xml_soap('MDFeConsNaoEnc', raiz) + return self._post(url, xml) + + def consulta_recibo(self, numero): + url = self._get_url('RET_RECEPCAO') + # Monta XML do corpo da requisição + raiz = etree.Element('consReciMDFe', versao=self._versao, xmlns=NAMESPACE_MDFE) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + etree.SubElement(raiz, 'nRec').text = numero.zfill(15) + # Monta XML para envio da requisição + xml = self._construir_xml_soap('MDFeRetRecepcao', raiz) + return self._post(url, xml) + + def evento(self, evento): + """ + Envia eventos do MDFe como: + Encerramento + Cancelamento + Inclusao Condutor + Inclusao DF-e + Pagamento Operacao MDF-e + :param evento: Nome do Evento + :return: + """ + # url do serviço + url = self._get_url('EVENTOS') + # Monta XML do corpo da requisição + xml = self._construir_xml_soap('MDFeRecepcaoEvento', evento) + return self._post(url, xml) + + def _construir_xml_soap(self, metodo, dados): + """Mota o XML para o envio via SOAP""" + + ns = collections.OrderedDict() + ns['xsi'] = self._namespace_xsi + ns['xsd'] = self._namespace_xsd + ns[self._soap_version] = self._namespace_soap + raiz = etree.Element( + '{%s}Envelope' % self._namespace_soap, + nsmap=ns + ) + + if self._header: + cabecalho = self._cabecalho_soap(metodo) + c = etree.SubElement(raiz, '{%s}Header' % self._namespace_soap) + c.append(cabecalho) + + body = etree.SubElement(raiz, '{%s}Body' % self._namespace_soap) + + a = etree.SubElement( + body, + self._envio_mensagem, + xmlns=self._namespace_metodo+metodo + ) + a.append(dados) + return raiz + + def _post_header(self, soap_webservice_method=False): + """Retorna um dicionário com os atributos para o cabeçalho da requisição HTTP""" + header = { + b'content-type': b'text/xml; charset=utf-8;', + } + + # PE é a únca UF que exige SOAPAction no header + if soap_webservice_method: + header[b'SOAPAction'] = \ + (self._namespace_metodo + soap_webservice_method).encode('utf-8') + + if self._accept: + header[b'Accept'] = b'application/soap+xml; charset=utf-8;' + + return header + + def _post(self, url, xml): + certificado_a1 = CertificadoA1(self.certificado) + chave, cert = certificado_a1.separar_arquivo(self.certificado_senha, caminho=True) + chave_cert = (cert, chave) + # Abre a conexão HTTPS + try: + xml_declaration = '' + + # limpa xml com caracteres bugados para infNFeSupl em NFC-e + xml = re.sub( + '(.*?)', + lambda x: x.group(0).replace('<', '<').replace('>', '>').replace('&', ''), + etree.tostring(xml, encoding='unicode').replace('\n', '') + ) + xml = xml_declaration + xml + + # print(xml) + # print('-' * 20) + + # Faz o request com o servidor + result = requests.post( + url, + xml, + headers=self._post_header(), + cert=chave_cert, + verify=False + ) + result.encoding = 'utf-8' + return result + except requests.exceptions.RequestException as e: + raise e + finally: + certificado_a1.excluir() + + def _cabecalho_soap(self, metodo): + """Monta o XML do cabeçalho da requisição SOAP""" + + raiz = etree.Element( + self._header, + xmlns=self._namespace_metodo + metodo + ) + etree.SubElement(raiz, 'cUF').text = CODIGOS_ESTADOS[self.uf.upper()] + etree.SubElement(raiz, 'versaoDados').text = '3.00' + return raiz + + def _get_url(self, consulta): + # producao + if self._ambiente == 1: + ambiente = MDFE['SVRS']['HTTPS'] + # homologacao + else: + ambiente = MDFE['SVRS']['HOMOLOGACAO'] + + self.url = ambiente + MDFE['SVRS'][consulta] + return self.url diff --git a/pynfe/processamento/nfe.py b/pynfe/processamento/nfe.py new file mode 100644 index 0000000..b15da6c --- /dev/null +++ b/pynfe/processamento/nfe.py @@ -0,0 +1,437 @@ +# -*- coding: utf-8 -*- +import datetime +import re +import requests + +from pynfe.utils import etree, so_numeros +from pynfe.utils.flags import ( + NAMESPACE_NFE, + VERSAO_PADRAO, + CODIGOS_ESTADOS, + NAMESPACE_METODO, + NAMESPACE_SOAP, + NAMESPACE_XSI, + NAMESPACE_XSD, +) + +from pynfe.entidades.certificado import CertificadoA1 +from pynfe.utils.webservices import NFE, NFCE +from .assinatura import AssinaturaA1 +from .comunicacao import Comunicacao + + +class ComunicacaoNFe(Comunicacao): + """Classe de comunicação que segue o padrão definido para as SEFAZ dos Estados.""" + + _versao = VERSAO_PADRAO + _assinatura = AssinaturaA1 + _namespace = NAMESPACE_NFE + _header = False + _envio_mensagem = 'nfeDadosMsg' + _namespace_metodo = NAMESPACE_METODO + _accept = False + _namespace_soap = NAMESPACE_SOAP + _namespace_xsi = NAMESPACE_XSI + _namespace_xsd = NAMESPACE_XSD + _soap_version = 'soap' + + def autorizacao(self, modelo, nota_fiscal, id_lote=1, ind_sinc=1): + """ + Método para realizar autorização da nota de acordo com o modelo + :param modelo: Modelo + :param nota_fiscal: XML assinado + :param id_lote: Id do lote - numero autoincremental gerado pelo sistema + :param ind_sinc: Indicador de sincrono e assincrono, 0 para assincrono, 1 para sincrono + :return: Uma tupla que em caso de sucesso, retorna xml com nfe e protocolo de autorização. Caso contrário, + envia todo o soap de resposta da Sefaz para decisão do usuário. + """ + # url do serviço + url = self._get_url(modelo=modelo, consulta='AUTORIZACAO') + + # Monta XML do corpo da requisição + raiz = etree.Element('enviNFe', xmlns=NAMESPACE_NFE, versao=VERSAO_PADRAO) + etree.SubElement(raiz, 'idLote').text = str(id_lote) # numero autoincremental gerado pelo sistema + etree.SubElement(raiz, 'indSinc').text = str(ind_sinc) # 0 para assincrono, 1 para sincrono + raiz.append(nota_fiscal) + + # Monta XML para envio da requisição + xml = self._construir_xml_soap('NFeAutorizacao4', raiz) + # Faz request no Servidor da Sefaz + retorno = self._post(url, xml) + + # Em caso de sucesso, retorna xml com nfe e protocolo de autorização. + # Caso contrário, envia todo o soap de resposta da Sefaz para decisão do usuário. + if retorno.status_code == 200: + # namespace + ns = {'ns': NAMESPACE_NFE} + # Procuta status no xml + try: + prot = etree.fromstring(retorno.text) + except ValueError: + # em SP retorno.text apresenta erro + prot = etree.fromstring(retorno.content) + if ind_sinc == 1: + try: + # Protocolo com envio OK + try: + inf_prot = prot[0][0] # root protNFe + except IndexError: + # Estados como GO vem com a tag header + inf_prot = prot[1][0] + + lote_status = inf_prot.xpath("ns:retEnviNFe/ns:cStat", namespaces=ns)[0].text + # Lote processado + if lote_status == '104': + prot_nfe = inf_prot.xpath("ns:retEnviNFe/ns:protNFe", namespaces=ns)[0] + status = prot_nfe.xpath('ns:infProt/ns:cStat', namespaces=ns)[0].text + # autorizado usa da NF-e + # retorna xml final (protNFe+NFe) + if status == '100': + raiz = etree.Element('nfeProc', xmlns=NAMESPACE_NFE, versao=VERSAO_PADRAO) + raiz.append(nota_fiscal) + raiz.append(prot_nfe) + return 0, raiz + except IndexError: + # Protocolo com algum erro no Envio + return 1, retorno, nota_fiscal + else: + # Retorna id do protocolo para posterior consulta em caso de sucesso. + rec = prot[0][0] + status = rec.xpath("ns:retEnviNFe/ns:cStat", namespaces=ns)[0].text + # Lote Recebido com Sucesso! + if status == '103': + nrec = rec.xpath("ns:retEnviNFe/ns:infRec/ns:nRec", namespaces=ns)[0].text + return 0, nrec, nota_fiscal + return 1, retorno, nota_fiscal + + def consulta_recibo(self, modelo, numero): + """ + Este método oferece a consulta do resultado do processamento de um lote de NF-e. + O aplicativo do Contribuinte deve ser construído de forma a aguardar um tempo mínimo de + 15 segundos entre o envio do Lote de NF-e para processamento e a consulta do resultado + deste processamento, evitando a obtenção desnecessária do status de erro 105 - "Lote em + Processamento". + :param modelo: Modelo da nota + :param numero: Número da nota + :return: + """ + + # url do serviço + url = self._get_url(modelo=modelo, consulta='RECIBO') + + # Monta XML do corpo da requisição + raiz = etree.Element('consReciNFe', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + etree.SubElement(raiz, 'nRec').text = numero + + # Monta XML para envio da requisição + xml = self._construir_xml_soap('NFeRetAutorizacao4', raiz) + return self._post(url, xml) + + def consulta_nota(self, modelo, chave): + """ + Este método oferece a consulta da situação da NF-e/NFC-e na Base de Dados do Portal + da Secretaria de Fazenda Estadual. + :param modelo: Modelo da nota + :param chave: Chave da nota + :return: + """ + # url do serviço + url = self._get_url(modelo=modelo, consulta='CHAVE') + # Monta XML do corpo da requisição + raiz = etree.Element('consSitNFe', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + etree.SubElement(raiz, 'xServ').text = 'CONSULTAR' + etree.SubElement(raiz, 'chNFe').text = chave + # Monta XML para envio da requisição + xml = self._construir_xml_soap('NFeConsultaProtocolo4', raiz) + return self._post(url, xml) + + def consulta_distribuicao(self, cnpj=None, cpf=None, chave=None, nsu=0): + """ + O XML do pedido de distribuição suporta três tipos de consultas que são definidas de acordo com a tag + informada no XML. As tags são distNSU, consNSU e consChNFe. + a) distNSU – Distribuição de Conjunto de DF-e a Partir do NSU Informado + b) consNSU – Consulta DF-e Vinculado ao NSU Informado + c) consChNFe – Consulta de NF-e por Chave de Acesso Informada + :param cnpj: CNPJ do interessado + :param cpf: CPF do interessado + :param chave: Chave da NF-e a ser consultada + :param nsu: Ultimo nsu ou nsu específico para ser consultado. + :return: + """ + # url + url = self._get_url_an(consulta='DISTRIBUICAO') + # Monta XML para envio da requisição + raiz = etree.Element('distDFeInt', versao='1.01', xmlns=NAMESPACE_NFE) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + if self.uf: + etree.SubElement(raiz, 'cUFAutor').text = CODIGOS_ESTADOS[self.uf.upper()] + if cnpj: + etree.SubElement(raiz, 'CNPJ').text = cnpj + else: + etree.SubElement(raiz, 'CPF').text = cpf + if not chave: + distNSU = etree.SubElement(raiz, 'distNSU') + etree.SubElement(distNSU, 'ultNSU').text = str(nsu).zfill(15) + if chave: + consChNFe = etree.SubElement(raiz, 'consChNFe') + etree.SubElement(consChNFe, 'chNFe').text = chave + #Monta XML para envio da requisição + xml = self._construir_xml_soap('NFeDistribuicaoDFe', raiz) + return self._post(url, xml) + + def consulta_cadastro(self, modelo, cnpj): + """ + Consulta de cadastro + :param modelo: Modelo da nota + :param cnpj: CNPJ da empresa + :return: + """ + # UF que utilizam a SVRS - Sefaz Virtual do RS: Para serviço de Consulta Cadastro: AC, RN, PB, SC + lista_svrs = ['AC', 'RN', 'PB', 'SC'] + + # RS implementa um método diferente na consulta de cadastro + if self.uf.upper() == 'RS': + url = NFE['RS']['CADASTRO'] + elif self.uf.upper() in lista_svrs: + url = NFE['SVRS']['CADASTRO'] + elif self.uf.upper() == 'SVC-RS': + url = NFE['SVC-RS']['CADASTRO'] + else: + url = self._get_url(modelo=modelo, consulta='CADASTRO') + + raiz = etree.Element('ConsCad', versao='2.00', xmlns=NAMESPACE_NFE) + info = etree.SubElement(raiz, 'infCons') + etree.SubElement(info, 'xServ').text = 'CONS-CAD' + etree.SubElement(info, 'UF').text = self.uf.upper() + etree.SubElement(info, 'CNPJ').text = cnpj + # etree.SubElement(info, 'CPF').text = cpf + + # Monta XML para envio da requisição + xml = self._construir_xml_soap('CadConsultaCadastro4', raiz) + # Chama método que efetua a requisição POST no servidor SOAP + return self._post(url, xml) + + def evento(self, modelo, evento, id_lote=1): + """ + Envia um evento de nota fiscal (cancelamento e carta de correção) + :param modelo: Modelo da nota + :param evento: Eventro + :param id_lote: Id do lote + :return: + """ + + # url do serviço + try: + # manifestacao url é do AN + if evento[0][5].text.startswith('2'): + url = self._get_url_an(consulta='EVENTOS') + else: + url = self._get_url(modelo=modelo, consulta='EVENTOS') + except Exception: + url = self._get_url(modelo=modelo, consulta='EVENTOS') + + # Monta XML do corpo da requisição + raiz = etree.Element('envEvento', versao='1.00', xmlns=NAMESPACE_NFE) + etree.SubElement(raiz, 'idLote').text = str(id_lote) # numero autoincremental gerado pelo sistema + raiz.append(evento) + xml = self._construir_xml_soap('NFeRecepcaoEvento4', raiz) + return self._post(url, xml) + + def status_servico(self): + """ + Verifica status do servidor da receita. + :param modelo: modelo é a string com tipo de serviço que deseja consultar, Ex: nfe ou nfce + :return: + """ + url = self._get_url('mdfe', 'STATUS') + # Monta XML do corpo da requisição + raiz = etree.Element('consStatServ', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) + etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) + etree.SubElement(raiz, 'cUF').text = CODIGOS_ESTADOS[self.uf.upper()] + etree.SubElement(raiz, 'xServ').text = 'STATUS' + xml = self._construir_xml_soap('NFeStatusServico4', raiz) + return self._post(url, xml) + + def inutilizacao(self, modelo, cnpj, numero_inicial, numero_final, justificativa='', ano=None, serie='1'): + """ + Serviço destinado ao atendimento de solicitações de inutilização de numeração. + :param modelo: Modelo da nota + :param cnpj: CNPJda empresa + :param numero_inicial: Número inicial + :param numero_final: Número final + :param justificativa: Justificativa + :param ano: Ano + :param serie: Série + :return: + """ + + # url do servico + url = self._get_url(modelo=modelo, consulta='INUTILIZACAO') + + # Valores default + ano = str(ano or datetime.date.today().year)[-2:] + uf = CODIGOS_ESTADOS[self.uf.upper()] + cnpj = so_numeros(cnpj) + + # Identificador da TAG a ser assinada formada com Código da UF + Ano (2 posições) + + # CNPJ + modelo + série + nro inicial e nro final precedida do literal “ID” + id_unico = 'ID%(uf)s%(ano)s%(cnpj)s%(modelo)s%(serie)s%(num_ini)s%(num_fin)s' % { + 'uf': uf, + 'ano': ano, + 'cnpj': cnpj, + 'modelo': '55' if modelo == 'nfe' else '65', # 55=NF-e; 65=NFC-e; + 'serie': serie.zfill(3), + 'num_ini': str(numero_inicial).zfill(9), + 'num_fin': str(numero_final).zfill(9), + } + + # Monta XML do corpo da requisição # FIXME + raiz = etree.Element('inutNFe', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) + inf_inut = etree.SubElement(raiz, 'infInut', Id=id_unico) + etree.SubElement(inf_inut, 'tpAmb').text = str(self._ambiente) + etree.SubElement(inf_inut, 'xServ').text = 'INUTILIZAR' + etree.SubElement(inf_inut, 'cUF').text = uf + etree.SubElement(inf_inut, 'ano').text = ano + etree.SubElement(inf_inut, 'CNPJ').text = cnpj + etree.SubElement(inf_inut, 'mod').text = '55' if modelo == 'nfe' else '65' # 55=NF-e; 65=NFC-e + etree.SubElement(inf_inut, 'serie').text = serie + etree.SubElement(inf_inut, 'nNFIni').text = str(numero_inicial) + etree.SubElement(inf_inut, 'nNFFin').text = str(numero_final) + etree.SubElement(inf_inut, 'xJust').text = justificativa + + # assinatura + a1 = AssinaturaA1(self.certificado, self.certificado_senha) + xml = a1.assinar(raiz) + + # Monta XML para envio da requisição + xml = self._construir_xml_soap('NFeInutilizacao4', xml) + # Faz request no Servidor da Sefaz e retorna resposta + return self._post(url, xml) + + def _get_url_an(self, consulta): + # producao + if self._ambiente == 1: + if consulta == 'DISTRIBUICAO': + ambiente = 'https://www1.' + else: + ambiente = 'https://www.' + # homologacao + else: + ambiente = 'https://hom.' + + self.url = ambiente + NFE['AN'][consulta] + return self.url + + def _get_url(self, modelo, consulta): + """ Retorna a url para comunicação com o webservice """ + # estado que implementam webservices proprios + lista = ['PR', 'MS', 'SP', 'AM', 'CE', 'BA', 'GO', 'MG', 'MT', 'PE', 'RS'] + if self.uf.upper() in lista: + if self._ambiente == 1: + ambiente = 'HTTPS' + else: + ambiente = 'HOMOLOGACAO' + if modelo == 'nfe': + # nfe Ex: https://nfe.fazenda.pr.gov.br/nfe/NFeStatusServico3 + self.url = NFE[self.uf.upper()][ambiente] + NFE[self.uf.upper()][consulta] + elif modelo == 'nfce': + # PE e BA são as únicas UF'sque possuem NFE proprio e SVRS para NFCe + if self.uf.upper() == 'PE' or self.uf.upper() == 'BA': + self.url = NFCE['SVRS'][ambiente] + NFCE['SVRS'][consulta] + else: + # nfce Ex: https://homologacao.nfce.fazenda.pr.gov.br/nfce/NFeStatusServico3 + self.url = NFCE[self.uf.upper()][ambiente] + NFCE[self.uf.upper()][consulta] + else: + raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') + # Estados que utilizam outros ambientes + else: + lista_svrs = ['AC', 'AL', 'AP', 'DF', 'ES', 'PB', 'PI', 'RJ', 'RN', 'RO', 'RR', 'SC', 'SE', 'TO'] + if self.uf.upper() in lista_svrs: + if self._ambiente == 1: + ambiente = 'HTTPS' + else: + ambiente = 'HOMOLOGACAO' + if modelo == 'nfe': + # nfe Ex: https://nfe.fazenda.pr.gov.br/nfe/NFeStatusServico3 + self.url = NFE['SVRS'][ambiente] + NFE['SVRS'][consulta] + elif modelo == 'nfce': + # nfce Ex: https://homologacao.nfce.fazenda.pr.gov.br/nfce/NFeStatusServico3 + self.url = NFCE['SVRS'][ambiente] + NFCE['SVRS'][consulta] + else: + raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') + # 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: + ambiente = 'HOMOLOGACAO' + if modelo == 'nfe': + # nfe Ex: https://nfe.fazenda.pr.gov.br/nfe/NFeStatusServico3 + 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['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): + """Mota o XML para o envio via SOAP""" + + raiz = etree.Element( + '{%s}Envelope' % NAMESPACE_SOAP, + nsmap={ + 'xsi': NAMESPACE_XSI, + 'xsd': NAMESPACE_XSD, + 'soap': NAMESPACE_SOAP + } + ) + body = etree.SubElement(raiz, '{%s}Body' % NAMESPACE_SOAP) + # distribuição tem um corpo de xml diferente + if metodo == 'NFeDistribuicaoDFe': + x = etree.SubElement(body, 'nfeDistDFeInteresse', xmlns=NAMESPACE_METODO+metodo) + a = etree.SubElement(x, 'nfeDadosMsg') + else: + a = etree.SubElement(body, 'nfeDadosMsg', xmlns=NAMESPACE_METODO+metodo) + a.append(dados) + return raiz + + def _post_header(self): + """Retorna um dicionário com os atributos para o cabeçalho da requisição HTTP""" + # PE é a única UF que exige SOAPAction no header + response = { + 'content-type': 'application/soap+xml; charset=utf-8;', + 'Accept': 'application/soap+xml; charset=utf-8;', + } + if self.uf.upper() == 'PE': + response["SOAPAction"] = "" + return response + + def _post(self, url, xml): + certificado_a1 = CertificadoA1(self.certificado) + chave, cert = certificado_a1.separar_arquivo(self.certificado_senha, caminho=True) + chave_cert = (cert, chave) + # Abre a conexão HTTPS + try: + xml_declaration = '' + + # limpa xml com caracteres bugados para infNFeSupl em NFC-e + xml = re.sub( + '(.*?)', + lambda x: x.group(0).replace('<', '<').replace('>', '>').replace('&', ''), + etree.tostring(xml, encoding='unicode').replace('\n', '') + ) + xml = xml_declaration + xml + # Faz o request com o servidor + result = requests.post(url, xml, headers=self._post_header(), cert=chave_cert, verify=False) + result.encoding = 'utf-8' + return result + except requests.exceptions.RequestException as e: + raise e + finally: + certificado_a1.excluir() diff --git a/pynfe/processamento/nfse.py b/pynfe/processamento/nfse.py new file mode 100644 index 0000000..5d75a52 --- /dev/null +++ b/pynfe/processamento/nfse.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +from pynfe.utils import etree +from pynfe.utils.flags import ( + NAMESPACE_XSI, + NAMESPACE_BETHA, +) +from pynfe.utils.webservices import NFSE +from pynfe.entidades.certificado import CertificadoA1 +from .comunicacao import Comunicacao + + +class ComunicacaoNfse(Comunicacao): + """ Classe de comunicação que segue o padrão definido para as SEFAZ dos Municípios. """ + + _versao = '' + _namespace = '' + + def __init__(self, certificado, certificado_senha, autorizador, homologacao=False): + self.certificado = certificado + self.certificado_senha = certificado_senha + self._ambiente = 2 if homologacao else 1 + self.autorizador = autorizador.upper() + if self.autorizador == 'GINFES': + self._namespace = 'http://www.ginfes.com.br/cabecalho_v03.xsd' + self._versao = '3' + elif self.autorizador == 'BETHA': + self._namespace = NAMESPACE_BETHA + self._versao = '2.02' + else: + raise Exception('Autorizador não encontrado!') + + def autorizacao(self, nota): + # url do serviço + url = self._get_url() + if self.autorizador == 'BETHA': + # xml + xml = etree.tostring(nota, encoding='unicode', pretty_print=False) + # comunica via wsdl + return self._post(url, xml, 'gerar') + else: + raise Exception('Este método só esta implementado no autorizador betha.') + + def enviar_lote(self, xml): + # url do serviço + url = self._get_url() + if self.autorizador == 'GINFES': + # xml + xml = '' + xml + # comunica via wsdl + return self._post_https(url, xml, 'enviar_lote') + else: + raise Exception('Este método só esta implementado no autorizador ginfes.') + + def consultar(self, xml): + # url do serviço + url = self._get_url() + if self.autorizador == 'GINFES': + # xml + xml = '' + xml + # comunica via wsdl + return self._post_https(url, xml, 'consulta') + else: + raise Exception('Este método só esta implementado no autorizador ginfes.') + + def consultar_rps(self, xml): + # url do serviço + url = self._get_url() + if self.autorizador == 'BETHA': + # comunica via wsdl + return self._post(url, xml, 'consultaRps') + elif self.autorizador == 'GINFES': + return self._post_https(url, xml, 'consultaRps') + # TODO outros autorizadres + else: + raise Exception('Autorizador não encontrado!') + + def consultar_faixa(self, xml): + # url do serviço + url = self._get_url() + if self.autorizador == 'BETHA': + # comunica via wsdl + return self._post(url, xml, 'consultaFaixa') + else: + raise Exception('Este método só esta implementado no autorizador betha.') + + def consultar_lote(self, xml): + # url do serviço + url = self._get_url() + if self.autorizador == 'GINFES': + # xml + xml = '' + xml + # comunica via wsdl + return self._post_https(url, xml, 'consulta_lote') + else: + raise Exception('Este método só esta implementado no autorizador ginfes.') + + def consultar_situacao_lote(self, xml): + # url do serviço + url = self._get_url() + if self.autorizador == 'GINFES': + # comunica via wsdl + return self._post_https(url, xml, 'consulta_situacao_lote') + else: + raise Exception('Este método só esta implementado no autorizador ginfes.') + + def cancelar(self, xml): + # url do serviço + url = self._get_url() + # Betha + if self.autorizador == 'BETHA': + # comunica via wsdl + return self._post(url, xml, 'cancelar') + # Ginfes + elif self.autorizador == 'GINFES': + # comunica via wsdl com certificado + return self._post_https(url, xml, 'cancelar') + # TODO outros autorizadres + else: + raise Exception('Autorizador não encontrado!') + + def _cabecalho(self, retorna_string=True): + """ Monta o XML do cabeçalho da requisição wsdl + Namespaces padrão homologação (Ginfes) """ + + xml_declaration = '' + # cabecalho = '3' + # cabecalho + raiz = etree.Element('{%s}cabecalho'%self._namespace, nsmap={'ns2':self._namespace, 'xsi':NAMESPACE_XSI}, versao=self._versao) + etree.SubElement(raiz, 'versaoDados').text = self._versao + + if retorna_string: + cabecalho = etree.tostring(raiz, encoding='unicode', pretty_print=False).replace('\n','') + cabecalho = xml_declaration + cabecalho + return cabecalho + else: + return raiz + + def _cabecalho2(self, retorna_string=True): + """ Monta o XML do cabeçalho da requisição wsdl + Namespaces que funcionaram em produção (Ginfes)""" + + xml_declaration = '' + + # cabecalho + raiz = etree.Element('cabecalho', xmlns=self._namespace, versao=self._versao) + etree.SubElement(raiz, 'versaoDados').text = self._versao + + if retorna_string: + cabecalho = etree.tostring(raiz, encoding='unicode', pretty_print=False).replace('\n', '') + cabecalho = xml_declaration + cabecalho + return cabecalho + else: + return raiz + + def _cabecalho_ginfes(self): + """ Retorna o XML do cabeçalho gerado pelo xsd""" + from pynfe.processamento.autorizador_nfse import SerializacaoGinfes + return SerializacaoGinfes().cabecalho() + + def _get_url(self): + """ Retorna a url para comunicação com o webservice """ + if self._ambiente == 1: + ambiente = 'HTTPS' + else: + ambiente = 'HOMOLOGACAO' + if self.autorizador in NFSE: + self.url = NFSE[self.autorizador][ambiente] + else: + raise Exception('Autorizador nao encontrado!') + return self.url + + def _post(self, url, xml, metodo): + """ Comunicação wsdl (http) sem certificado digital """ + # cabecalho + cabecalho = self._cabecalho() + # comunicacao wsdl + try: + from suds.client import Client + cliente = Client(url) + # gerar nfse + if metodo == 'gerar': + return cliente.service.GerarNfse(cabecalho, xml) + elif metodo == 'consultaRps': + return cliente.service.ConsultarNfsePorRps(cabecalho, xml) + elif metodo == 'consultaFaixa': + return cliente.service.ConsultarNfseFaixa(cabecalho, xml) + elif metodo == 'cancelar': + return cliente.service.CancelarNfse(cabecalho, xml) + # TODO outros metodos + else: + raise Exception('Método não implementado no autorizador.') + except Exception as e: + raise e + + def _post_https(self, url, xml, metodo): + """ Comunicação wsdl (https) utilizando certificado do usuário """ + # cabecalho + cabecalho = self._cabecalho() + # comunicacao wsdl + try: + from suds.client import Client + from pynfe.utils.https_nfse import HttpAuthenticated + + certificadoA1 = CertificadoA1(self.certificado) + chave, cert = certificadoA1.separar_arquivo(self.certificado_senha, caminho=True) + + cliente = Client(url, transport = HttpAuthenticated(key=chave, cert=cert, endereco=url)) + + # gerar nfse + if metodo == 'gerar': + return cliente.service.GerarNfse(cabecalho, xml) + elif metodo == 'enviar_lote': + return cliente.service.RecepcionarLoteRpsV3(cabecalho, xml) + elif metodo == 'consulta': + return cliente.service.ConsultarNfseV3(cabecalho, xml) + elif metodo == 'consulta_lote': + return cliente.service.ConsultarLoteRpsV3(cabecalho, xml) + elif metodo == 'consulta_situacao_lote': + return cliente.service.ConsultarSituacaoLoteRpsV3(cabecalho, xml) + elif metodo == 'consultaRps': + return cliente.service.ConsultarNfsePorRpsV3(cabecalho, xml) + elif metodo == 'consultaFaixa': + return cliente.service.ConsultarNfseFaixa(cabecalho, xml) + elif metodo == 'cancelar': + # versão 2 + return cliente.service.CancelarNfse(xml) + # versão 3 + # return cliente.service.CancelarNfseV3(cabecalho, xml) + # TODO outros metodos + else: + raise Exception('Método não implementado no autorizador.') + except Exception as e: + raise e diff --git a/pynfe/processamento/resposta.py b/pynfe/processamento/resposta.py new file mode 100644 index 0000000..40f1529 --- /dev/null +++ b/pynfe/processamento/resposta.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import re +from pynfe.utils import etree + + +class RetornoSoap(object): + + def __init__(self, webservice, retorno, resposta): + self.webservice = webservice + self.resposta = resposta + self.retorno = retorno + + +def analisar_retorno(webservice, retorno, classe_resposta): + + # retorno.raise_for_status() + # print(retorno.text) + + match = re.search('(.*?)', retorno.text) + + if match: + resultado = etree.tostring(etree.fromstring(match.group(1))[0]) + # classe_resposta.Validate_simpletypes_ = False + # resposta = classe_resposta.parseString(resultado) + resposta = resultado + # resposta = retorno.text + + return RetornoSoap(webservice, retorno, resposta) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index c571654..9f0ddc2 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1,9 +1,19 @@ # -*- coding: utf-8 -*- 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, \ +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, NAMESPACE_SIG, VERSAO_QRCODE +) +from pynfe.utils.flags import ( + CODIGOS_ESTADOS, + VERSAO_PADRAO, + VERSAO_MDFE, + NAMESPACE_NFE, + NAMESPACE_MDFE, + NAMESPACE_SIG, + VERSAO_QRCODE +) from pynfe.utils.webservices import NFCE import base64 import hashlib @@ -674,7 +684,6 @@ class SerializacaoXML(Serializacao): raiz.append(self._serializar_responsavel_tecnico( nota_fiscal.responsavel_tecnico[0], retorna_string=False)) - if retorna_string: return etree.tostring(raiz, encoding="unicode", pretty_print=True) else: @@ -712,6 +721,93 @@ class SerializacaoXML(Serializacao): else: return raiz + def serializar_evento_mdfe(self, evento, tag_raiz='eventoMDFe', retorna_string=False): + tz = datetime.now().astimezone().strftime('%z') + tz = "{}:{}".format(tz[:-2], tz[-2:]) + raiz = etree.Element(tag_raiz, versao=VERSAO_MDFE, xmlns=NAMESPACE_MDFE) + 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) + 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, 'chMDFe').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 + etree.SubElement(e, 'nSeqEvento').text = str(evento.n_seq_evento) + det = etree.SubElement(e, 'detEvento', versaoEvento=VERSAO_MDFE) + if evento.descricao == 'Encerramento': + encerramento = etree.SubElement(det, 'evEncMDFe') + etree.SubElement(encerramento, 'descEvento').text = evento.descricao + etree.SubElement(encerramento, 'nProt').text = evento.protocolo + etree.SubElement(encerramento, 'dtEnc').text = evento.dtenc.strftime('%Y-%m-%d') + etree.SubElement(encerramento, 'cUF').text = evento.cuf + etree.SubElement(encerramento, 'cMun').text = evento.cmun + elif evento.descricao == 'Inclusão Condutor': + inclusao = etree.SubElement(det, 'evIncCondutorMDFe') + etree.SubElement(inclusao, 'descEvento').text = evento.descricao + condutor = etree.SubElement(inclusao, 'condutor') + etree.SubElement(condutor, 'xNome').text = evento.nome_motorista + etree.SubElement(condutor, 'CPF').text = evento.cpf_motorista + elif evento.descricao == 'Inclusao DF-e': + inclusao = etree.SubElement(det, 'evIncDFeMDFe') + etree.SubElement(inclusao, 'descEvento').text = evento.descricao + etree.SubElement(inclusao, 'nProt').text = evento.protocolo + etree.SubElement(inclusao, 'cMunCarrega').text = evento.cmun_carrega + etree.SubElement(inclusao, 'xMunCarrega').text = evento.xmun_carrega + infDoc = etree.SubElement(inclusao, 'infDoc') + etree.SubElement(infDoc, 'cMunDescarga').text = evento.cmun_descarga + etree.SubElement(infDoc, 'xMunDescarga').text = evento.xmun_descarga + etree.SubElement(infDoc, 'chNFe').text = evento.chave_nfe + elif evento.descricao == 'Pagamento Operacao MDF-e': + pagamento = etree.SubElement(det, 'evPagtoOperMDFe') + etree.SubElement(pagamento, 'descEvento').text = evento.descricao + etree.SubElement(pagamento, 'nProt').text = evento.protocolo + + # Viagens + infViagens = etree.SubElement(pagamento, 'infViagens') + etree.SubElement(infViagens, 'qtdViagens').text = evento.qtd_viagens.zfill(5) + etree.SubElement(infViagens, 'nroViagem').text = evento.nro_viagens.zfill(5) + + # Informações do pagamento + infPag = etree.SubElement(pagamento, 'infPag') + etree.SubElement(infPag, 'xNome').text = evento.nome_contratante + if len(evento.cpfcnpj_contratante) == 11: + etree.SubElement(infPag, 'CPF').text = evento.cpfcnpj_contratante + else: + etree.SubElement(infPag, 'CNPJ').text = evento.cpfcnpj_contratante + + # Componentes de Pagamento do Frete + Comp = etree.SubElement(infPag, 'Comp') + etree.SubElement(Comp, 'tpComp').text = evento.tpComp.zfill(2) + etree.SubElement(Comp, 'vComp').text = '{:.2f}'.format(evento.vComp) + + # Continuação das Informações do pagamento + etree.SubElement(infPag, 'vContrato').text = '{:.2f}'.format(evento.vContrato) + etree.SubElement(infPag, 'indPag').text = evento.indPag + + # Se indPag == 1 (0=A vista e 1=A prazo) + if evento.indPag != '': + if int(evento.indPag) == 1: + infPrazo = etree.SubElement(infPag, 'infPrazo') + etree.SubElement(infPrazo, 'nParcela').text = evento.nParcela.zfill(3) + etree.SubElement(infPrazo, 'dVenc').text = evento.dVenc.strftime('%Y-%m-%d') + etree.SubElement(infPrazo, 'vParcela').text = '{:.2f}'.format(evento.vParcela) + + # Informações bancárias + infBanc = etree.SubElement(infPag, 'infBanc') + if evento.CNPJIPEF != '': + etree.SubElement(infBanc, 'CNPJIPEF').text = evento.CNPJIPEF.zfill(14) + else: + etree.SubElement(infBanc, 'codBanco').text = evento.codBanco + etree.SubElement(infBanc, 'codAgencia').text = evento.codAgencia + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + class SerializacaoQrcode(object): """ Classe que gera e serializa o qrcode de NFC-e no xml """ diff --git a/pynfe/utils/__init__.py b/pynfe/utils/__init__.py index 2aeeab6..a971aa1 100644 --- a/pynfe/utils/__init__.py +++ b/pynfe/utils/__init__.py @@ -3,13 +3,17 @@ import os import codecs from unicodedata import normalize +import re try: from lxml import etree except ImportError: raise Exception('Falhou ao importar lxml/ElementTree') -from io import StringIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO try: from . import flags @@ -146,3 +150,11 @@ def obter_uf_por_codigo(codigo_uf): def remover_acentos(txt): return normalize('NFKD', txt).encode('ASCII','ignore').decode('ASCII') + + +def extrai_id_srtxml(edoc): + result = '' + match = re.search('Id=[^0-9]+(\d+)"', edoc) + if match: + result = match.group(1) + return result \ No newline at end of file diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index 084389d..d43df33 100644 --- a/pynfe/utils/flags.py +++ b/pynfe/utils/flags.py @@ -10,6 +10,11 @@ NAMESPACE_METODO = 'http://www.portalfiscal.inf.br/nfe/wsdl/' NAMESPACE_SOAP_NFSE = 'http://schemas.xmlsoap.org/soap/envelope/' NAMESPACE_BETHA = 'http://www.betha.com.br/e-nota-contribuinte-ws' +NAMESPACE_MDFE = 'http://www.portalfiscal.inf.br/mdfe' +NAMESPACE_MDFE_METODO = 'http://www.portalfiscal.inf.br/mdfe/wsdl/' +MODELO_MDFE = '58' +VERSAO_MDFE = '3.00' + VERSAO_PADRAO = '4.00' VERSAO_QRCODE = '2' diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index 49c6029..acc1585 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -498,3 +498,18 @@ NFSE = { 'HOMOLOGACAO':'https://homologacao.ginfes.com.br/ServiceGinfesImpl?wsdl' } } + +# MDF-e +MDFE = { + # unico autorizador de MDF-e + 'SVRS': { + 'RECEPCAO': 'mdferecepcao/MDFeRecepcao.asmx', + 'RET_RECEPCAO': 'mdferetrecepcao/MDFeRetRecepcao.asmx', + 'EVENTOS': 'mdferecepcaoevento/MDFeRecepcaoEvento.asmx', + 'CONSULTA': 'mdfeconsulta/MDFeConsulta.asmx', + 'STATUS': 'mdfestatusservico/MDFeStatusServico.asmx', + 'NAO_ENCERRADOS': 'mdfeconsnaoenc/MDFeConsNaoEnc.asmx', + 'HTTPS': 'https://mdfe.svrs.rs.gov.br/ws/', + 'HOMOLOGACAO': 'https://mdfe-homologacao.svrs.rs.gov.br/ws/' + } +} From 1e0719cf868ba2ed3c83d51748f64ffc8d45bc65 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Fri, 3 Jul 2020 13:51:05 -0400 Subject: [PATCH 02/29] =?UTF-8?q?Adicionada=20as=20urls=20do=20QRCode=20e?= =?UTF-8?q?=20da=20transmiss=C3=A3o=20s=C3=ADncrona?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/utils/webservices.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index acc1585..31f87ae 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -504,12 +504,14 @@ MDFE = { # unico autorizador de MDF-e 'SVRS': { 'RECEPCAO': 'mdferecepcao/MDFeRecepcao.asmx', + 'RECEPCAO_SINC': 'mdferecepcao/MDFeRecepcaoSinc.asmx', 'RET_RECEPCAO': 'mdferetrecepcao/MDFeRetRecepcao.asmx', 'EVENTOS': 'mdferecepcaoevento/MDFeRecepcaoEvento.asmx', 'CONSULTA': 'mdfeconsulta/MDFeConsulta.asmx', 'STATUS': 'mdfestatusservico/MDFeStatusServico.asmx', 'NAO_ENCERRADOS': 'mdfeconsnaoenc/MDFeConsNaoEnc.asmx', 'HTTPS': 'https://mdfe.svrs.rs.gov.br/ws/', - 'HOMOLOGACAO': 'https://mdfe-homologacao.svrs.rs.gov.br/ws/' + 'HOMOLOGACAO': 'https://mdfe-homologacao.svrs.rs.gov.br/ws/', + 'QRCODE': 'https://dfe-portal.svrs.rs.gov.br/mdfe/qrCode' } } From a00071c25d9e26c9da9b5884846e958e68509bb4 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Fri, 3 Jul 2020 13:51:21 -0400 Subject: [PATCH 03/29] =?UTF-8?q?Cria=C3=A7=C3=A3o=20dos=20status=20do=20m?= =?UTF-8?q?dfe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/utils/flags.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pynfe/utils/flags.py b/pynfe/utils/flags.py index d43df33..f132148 100644 --- a/pynfe/utils/flags.py +++ b/pynfe/utils/flags.py @@ -74,6 +74,17 @@ NF_STATUS = ( 'Cancelada', ) +MDFE_STATUS = ( + 'Em Digitacao', + 'Validada', + 'Assinada', + 'Em processamento', + 'Autorizada', + 'Rejeitada', + 'Cancelada', + 'Encerrada', +) + NF_TIPOS_DOCUMENTO = ( (0, 'Entrada'), (1, 'Saida'), From d796de62e14e5605bb41ccd4b6c9b96a1fa97e60 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Fri, 3 Jul 2020 13:51:51 -0400 Subject: [PATCH 04/29] =?UTF-8?q?Cria=C3=A7=C3=A3o=20do=20m=C3=A9todo=20de?= =?UTF-8?q?=20envio/transmiss=C3=A3o=20de=20mdfe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/mdfe.py | 86 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/pynfe/processamento/mdfe.py b/pynfe/processamento/mdfe.py index ab5e153..b2d7256 100644 --- a/pynfe/processamento/mdfe.py +++ b/pynfe/processamento/mdfe.py @@ -4,7 +4,7 @@ import re import requests import collections from io import StringIO - +import base64 from pynfe.utils.flags import ( NAMESPACE_MDFE, @@ -43,12 +43,92 @@ class ComunicacaoMDFe(Comunicacao): _soap_version = 'soap12' _edoc_situacao_ja_enviado = MDFE_SITUACAO_JA_ENVIADO _edoc_situacao_arquivo_recebido_com_sucesso = '103' + _edoc_situacao_lote_processado = '104' _edoc_situacao_em_processamento = '105' _edoc_situacao_servico_em_operacao = '107' consulta_servico_ao_enviar = True maximo_tentativas_consulta_recibo = 5 + def autorizacao(self, manifesto, id_lote=1, ind_sinc=1): + """ + Método para realizar autorização do manifesto + :param manifesto: XML assinado + :param id_lote: Id do lote - numero autoincremental gerado pelo sistema + :param ind_sinc: Indicador de sincrono e assincrono, 0 para assincrono, 1 para sincrono + :return: Uma tupla que em caso de sucesso, retorna xml com manifesto e protocolo de autorização. + Caso contrário, envia todo o soap de resposta da Sefaz para decisão do usuário. + """ + # url do serviço + if ind_sinc == 0: + url = self._get_url(consulta='RECEPCAO') + elif ind_sinc == 1: + url = self._get_url(consulta='RECEPCAO_SINC') + else: + raise f'ind_sinc deve ser 0=assincrono ou 1=sincrono' + + # Monta XML do corpo da requisição + raiz = etree.Element('enviMDFe', xmlns=NAMESPACE_MDFE, versao=VERSAO_MDFE) + etree.SubElement(raiz, 'idLote').text = str(id_lote) # numero autoincremental gerado pelo sistema + etree.SubElement(raiz, 'indSinc').text = str(ind_sinc) # 0 para assincrono, 1 para sincrono + raiz.append(manifesto) + + # Monta XML para envio da requisição + if ind_sinc == 0: + xml = self._construir_xml_soap('MDFeRecepcao', raiz) + elif ind_sinc == 1: + xml = self._construir_xml_soap('MDFeRecepcaoSinc', raiz) + + # Faz request no Servidor da Sefaz + retorno = self._post(url, xml) + + # Em caso de sucesso, retorna xml com o mdfe e protocolo de autorização. + # Caso contrário, envia todo o soap de resposta da Sefaz para decisão do usuário. + if retorno.status_code == 200: + # namespace + ns = {'ns': NAMESPACE_MDFE} + # Procuta status no xml + try: + prot = etree.fromstring(retorno.text) + except ValueError: + # em SP retorno.text apresenta erro + prot = etree.fromstring(retorno.content) + + if ind_sinc == 1: + try: + # Protocolo com envio OK + try: + inf_prot = prot[0][0] # root protMDFe + except IndexError: + # Estados como GO vem com a tag header + inf_prot = prot[1][0] + + lote_status = inf_prot.xpath("ns:retEnviMDFe/ns:cStat", namespaces=ns)[0].text + # Lote processado + if lote_status == self._edoc_situacao_lote_processado: + prot_mdfe = inf_prot.xpath("ns:retEnviMDFe/ns:protMDFe", namespaces=ns)[0] + status = prot_mdfe.xpath('ns:infProt/ns:cStat', namespaces=ns)[0].text + + # autorizado uso do MDF-e + # retorna xml final (protMDFe + MDFe) + if status in self._edoc_situacao_ja_enviado: # if status == '100': + raiz = etree.Element('mdfeProc', xmlns=NAMESPACE_MDFE, versao=VERSAO_MDFE) + raiz.append(manifesto) + raiz.append(prot_mdfe) + return 0, raiz + except IndexError: + # Protocolo com algum erro no Envio + return 1, retorno, manifesto + else: + # Retorna id do protocolo para posterior consulta em caso de sucesso. + rec = prot[0][0] + status = rec.xpath("ns:retEnviMDFe/ns:cStat", namespaces=ns)[0].text + # Lote Recebido com Sucesso! + if status == self._edoc_situacao_arquivo_recebido_com_sucesso: + nrec = rec.xpath("ns:retEnviMDFe/ns:infRec/ns:nRec", namespaces=ns)[0].text + return 0, nrec, manifesto + return 1, retorno, manifesto + def status_servico(self): url = self._get_url('STATUS') # Monta XML do corpo da requisição @@ -138,6 +218,10 @@ class ComunicacaoMDFe(Comunicacao): self._envio_mensagem, xmlns=self._namespace_metodo+metodo ) + + # if metodo == 'MDFeRecepcaoSinc': + # body_base64 = base64.b16encode(a).decode() + a.append(dados) return raiz From c50fd8c293b5330f765f1eea070746aaeccee868 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Fri, 3 Jul 2020 13:52:16 -0400 Subject: [PATCH 05/29] =?UTF-8?q?Cria=C3=A7=C3=A3o=20das=20entidades=20do?= =?UTF-8?q?=20Manifesto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/entidades/__init__.py | 2 +- pynfe/entidades/manifesto.py | 444 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 pynfe/entidades/manifesto.py diff --git a/pynfe/entidades/__init__.py b/pynfe/entidades/__init__.py index f2c8464..c00f4dd 100644 --- a/pynfe/entidades/__init__.py +++ b/pynfe/entidades/__init__.py @@ -3,9 +3,9 @@ from .produto import Produto from .cliente import Cliente from .transportadora import Transportadora from .notafiscal import NotaFiscal +from .manifesto import Manifesto 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/manifesto.py b/pynfe/entidades/manifesto.py new file mode 100644 index 0000000..62ef81b --- /dev/null +++ b/pynfe/entidades/manifesto.py @@ -0,0 +1,444 @@ +# -*- coding: utf-8 -*- +import random + +from .base import Entidade +from pynfe import get_version +from pynfe.utils.flags import MDFE_STATUS, CODIGO_BRASIL, CODIGOS_ESTADOS + +from pynfe.utils import so_numeros + +from decimal import Decimal + + +class Manifesto(Entidade): + status = MDFE_STATUS[0] + + # - UF - converter para codigos em CODIGOS_ESTADOS + uf = str() + + # tpAmb + + # - Tipo Emitente + # 1=Transportadora; 2=Carga própria; 3=CTe Globalizado + tipo_emitente = int() + + # - Tipo transportador - 0=nenhum; 1=etc; 2=tac; 3=ctc + tipo_transportador = int() + + # Manifesto fixo 58 + # - Modelo (formato: NN) + modelo = 58 + + # - Serie (obrigatorio - formato: NNN) + serie = str() + + # - Numero MDFe (obrigatorio) + numero_mdfe = str() + + # - Código numérico aleatório que compõe a chave de acesso + codigo_numerico_aleatorio = str() + + # - Digito verificador do codigo numerico aleatorio + dv_codigo_numerico_aleatorio = str() + + # - Tipo do modal de transporte + # 1=Rodoviario; 2=Aereo; 3=Aquaviario; 4=Ferroviario + modal = 1 + + # - Data da Emissao (obrigatorio) + data_emissao = None + + # - Forma de emissao (obrigatorio - seleciona de lista) - NF_FORMAS_EMISSAO + forma_emissao = str() + + # - Processo de emissão da NF-e (obrigatorio - seleciona de lista) - NF_PROCESSOS_EMISSAO + processo_emissao = 0 + + # - Versao do processo de emissão do MDF-e + versao_processo_emissao = get_version() + + # - UF inicio. Exemplo SP, MT, PR + UFIni = str() + + # - UF final. Exemplo SP, MT, PR + UFFim = str() + + # - Digest value da NF-e (somente leitura) + digest_value = None + + # - Protocolo (somente leitura) + protocolo = str() + + # - Data (somente leitura) + data = None + + # - Municípios carregamento (lista 1 para * / ManyToManyField) + municipio_carrega = None + + # - Percurso da viagem (lista 1 para * / ManyToManyField) + percurso = None + + # Data inicial da viagem + dhIniViagem = None + + # - Emitente (lista 1 para * / ManyToManyField) + emitente = None + + # - Modal rodoviario (lista 1 para * / ManyToManyField) + modal_rodoviario = None + + # - Documentos vinculados NFe ou CTe (lista 1 para * / ManyToManyField) + documentos = None + + # - Seguradora (lista 1 para * / ManyToManyField) + seguradora = None + + # - Produto predominante + produto = None + + # - Resumo dos Totais do MDF-e + totais = None + + # - Lacres + lacres = None + + # - Informacoes Adicionais + # - Informacoes adicionais de interesse do fisco + informacoes_adicionais_interesse_fisco = str() + + # - Informacoes complementares de interesse do contribuinte + informacoes_complementares_interesse_contribuinte = str() + + def __init__(self, *args, **kwargs): + self.municipio_carrega = [] + self.percurso = [] + self.modal_rodoviario = [] + self.documentos = [] + self.seguradora = [] + self.produto = [] + self.lacres = [] + self.responsavel_tecnico = [] + super(Manifesto, self).__init__(*args, **kwargs) + + def __str__(self): + return ' '.join([str(self.modelo), self.serie, self.numero_mdfe]) + + def adicionar_municipio_carrega(self, **kwargs): + obj = ManifestoMunicipioCarrega(**kwargs) + self.municipio_carrega.append(obj) + return obj + + def adicionar_percurso(self, **kwargs): + obj = ManifestoPercurso(**kwargs) + self.percurso.append(obj) + return obj + + def adicionar_modal_rodoviario(self, **kwargs): + obj = ManifestoRodoviario(**kwargs) + self.modal_rodoviario.append(obj) + return obj + + def adicionar_documentos(self, **kwargs): + obj = ManifestoDocumentos(**kwargs) + self.documentos.append(obj) + return obj + + def adicionar_seguradora(self, **kwargs): + obj = ManifestoSeguradora(**kwargs) + self.seguradora.append(obj) + return obj + + def adicionar_produto(self, **kwargs): + obj = ManifestoProduto(**kwargs) + self.produto.append(obj) + return obj + + def adicionar_totais(self, **kwargs): + obj = ManifestoTotais(**kwargs) + self.totais.append(obj) + return obj + + def adicionar_lacres(self, **kwargs): + obj = ManifestoLacres(**kwargs) + self.lacres.append(obj) + return obj + + def adicionar_responsavel_tecnico(self, **kwargs): + """ Adiciona uma instancia de Responsavel Tecnico """ + obj = ManifestoResponsavelTecnico(**kwargs) + self.responsavel_tecnico.append(obj) + return obj + + def _codigo_numerico_aleatorio(self): + self.codigo_numerico_aleatorio = str(random.randint(0, 99999999)).zfill(8) + return self.codigo_numerico_aleatorio + + def _dv_codigo_numerico(self, key): + assert len(key) == 43 + + weights = [2, 3, 4, 5, 6, 7, 8, 9] + weights_size = len(weights) + key_numbers = [int(k) for k in key] + key_numbers.reverse() + + key_sum = 0 + for i, key_number in enumerate(key_numbers): + # cycle though weights + i = i % weights_size + key_sum += key_number * weights[i] + + remainder = key_sum % 11 + if remainder == 0 or remainder == 1: + self.dv_codigo_numerico_aleatorio = '0' + return '0' + self.dv_codigo_numerico_aleatorio = str(11 - remainder) + return str(self.dv_codigo_numerico_aleatorio) + + @property + # @memoize + def identificador_unico(self): + # Monta 'Id' da tag raiz + # Ex.: MDFe35080599999090910270580010000000011518005123 + key = "%(uf)s%(ano)s%(mes)s%(cnpj)s%(mod)s%(serie)s%(nMDF)s%(tpEmis)s%(cMDF)s"%{ + 'uf': CODIGOS_ESTADOS[self.uf], + 'ano': self.data_emissao.strftime('%y'), + 'mes': self.data_emissao.strftime('%m'), + 'cnpj': so_numeros(self.emitente.cpfcnpj).zfill(14), + 'mod': self.modelo, + 'serie': str(self.serie).zfill(3), + 'nMDF': str(self.numero_mdfe).zfill(9), + 'tpEmis': str(self.forma_emissao), + 'cMDF': self._codigo_numerico_aleatorio(), + } + return "MDFe%(uf)s%(ano)s%(mes)s%(cnpj)s%(mod)s%(serie)s%(nMDF)s%(tpEmis)s%(cMDF)s%(cDV)s"%{ + 'uf': CODIGOS_ESTADOS[self.uf], + 'ano': self.data_emissao.strftime('%y'), + 'mes': self.data_emissao.strftime('%m'), + 'cnpj': so_numeros(self.emitente.cpfcnpj).zfill(14), + 'mod': self.modelo, + 'serie': str(self.serie).zfill(3), + 'nMDF': str(self.numero_mdfe).zfill(9), + 'tpEmis': str(self.forma_emissao), + 'cMDF': str(self.codigo_numerico_aleatorio), + 'cDV': self._dv_codigo_numerico(key), + } + + +class ManifestoMunicipioCarrega(Entidade): + # - Codigo municipio + cMunCarrega = str() + + # - Nome do municipio + xMunCarrega = str() + + +class ManifestoPercurso(Entidade): + # - Nome da UF (2 digitos) + UFPer = str() + + +class ManifestoRodoviario(Entidade): + rntrc = str() + ciot = None + pedagio = None + contratante = None + pagamento = None + veiculo_tracao = None + veiculo_reboque = None + + +class ManifestoCIOT(Entidade): + numero_ciot = str() + cpfcnpj = str() + + +class ManifestoPedagio(Entidade): + cnpj_fornecedor = str() + cpfcnpj_pagador = str() + numero_compra = str() + valor_pedagio = Decimal() + + +class ManifestoContratante(Entidade): + nome = str() + cpfcnpj = str() + + +class ManifestoVeiculoTracao(Entidade): + cInt = str() + placa = str() + RENAVAM = str() + tara = str() + capKG = str() + capM3 = str() + proprietario = None + condutor = None + tpRod = str() + tpCar = str() + UF = str() + + +class ManifestoVeiculoReboque(Entidade): + cInt = str() + placa = str() + RENAVAM = str() + tara = str() + capKG = str() + capM3 = str() + proprietario = None + tpCar = str() + UF = str() + + +class ManifestoCondutor(Entidade): + nome_motorista = str() + cpf_motorista = str() + + +class ManifestoDocumentos(Entidade): + + # Código do municipio de descarga + cMunDescarga = str() + # Nome do municipio de descarga + xMunDescarga = str() + + # Documentos vinculados + documentos_nfe = None + documentos_cte = None + + +class ManifestoDocumentosNFe(Entidade): + chave_acesso_nfe = str() + + +class ManifestoDocumentosCTe(Entidade): + chave_acesso_cte = str() + + +class ManifestoSeguradora(Entidade): + + # infResp - Responsavel seguro + # 1=Emitente; 2=Tomador + responsavel_seguro = str() + # - CNPJ do responsavel + cnpj_responsavel = str() + + # infSeg - Seguradora + # - Nome da seguradora + nome_seguradora = str() + # - CNPJ seguradora + cnpj_seguradora = str() + + # Apolice do Seguro + numero_apolice = str() + + # Lista de Averbacoes + averbacoes = None + + +class ManifestoAverbacao(Entidade): + # Numero da Averbacao + numero = str() + + +class ManifestoProduto(Entidade): + + # Tipo de carga + # 01=GranelSolido + # 02=GranelLiquido + # 03=Frigorificada + # 04=Conteinerizada + # 05=CargaGeral + # 06=Neogranel + # 07=PerigosaGranelSolido + # 08=PerigosaGranelLiquido + # 09=PerigosaCargaFrigorificada + # 10=PerigosaConteinerizada + # 11=PerigosaCargaGeral + tipo_carga = str() + + nome_produto = str() + cean = str() + ncm = str() + + +class ManifestoEmitente(Entidade): + # Dados do Emitente + + # - CPF ou CNPJ (obrigatorio) + cpfcnpj = str() + + # - Inscricao Estadual (obrigatorio) + inscricao_estadual = str() + + # - Nome/Razao Social (obrigatorio) + razao_social = str() + + # - Nome Fantasia + nome_fantasia = str() + + # Endereco + # - Logradouro (obrigatorio) + endereco_logradouro = str() + + # - Numero (obrigatorio) + endereco_numero = str() + + # - Complemento + endereco_complemento = str() + + # - Bairro (obrigatorio) + endereco_bairro = str() + + # - Codigo Municipio (opt) + endereco_cod_municipio = str() + + # - Municipio (obrigatorio) + endereco_municipio = str() + + # - CEP + endereco_cep = str() + + # - UF (obrigatorio) + endereco_uf = str() + + # - Telefone + endereco_telefone = str() + + # - Email + endereco_email = str() + + def __str__(self): + return self.cpfcnpj + + +class ManifestoTotais(Entidade): + + # Quantidade total de CT-e relacionados no Manifesto + qCTe = int() + + # Quantidade total de NF-e relacionadas no Manifesto + qNFe = int() + + # Valor total da carga / mercadorias transportadas + vCarga = Decimal() + + # - Código da unidade de medida do Peso Bruto da Carga / Mercadorias transportadas + # Unidades: 01 – KG; 02 - TON + cUnid = str() + + # - Peso Bruto Total da Carga / Mercadorias transportadas + qCarga = Decimal() + + +class ManifestoLacres(Entidade): + nLacre = str() + + +class ManifestoResponsavelTecnico(Entidade): + # NT 2018/003 + cnpj = str() + contato = str() + email = str() + fone = str() + csrt = str() From 4b48fae55f17b66fdfffe2a252b7d6558bbf40d8 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Fri, 3 Jul 2020 13:52:27 -0400 Subject: [PATCH 06/29] =?UTF-8?q?Cria=C3=A7=C3=A3o=20classe=20de=20seriali?= =?UTF-8?q?za=C3=A7=C3=A3o=20do=20MDFe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 462 +++++++++++++++++++++++++++++++++++- 1 file changed, 457 insertions(+), 5 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 9f0ddc2..d4f89b4 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1,9 +1,15 @@ # -*- coding: utf-8 -*- -from pynfe.entidades import NotaFiscal +from pynfe.entidades import NotaFiscal, Manifesto 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 + 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, @@ -14,7 +20,7 @@ from pynfe.utils.flags import ( NAMESPACE_SIG, VERSAO_QRCODE ) -from pynfe.utils.webservices import NFCE +from pynfe.utils.webservices import NFCE, MDFE import base64 import hashlib from datetime import datetime @@ -909,6 +915,39 @@ class SerializacaoQrcode(object): return nfe +class SerializacaoQrcodeMDFe(object): + """ Classe que gera e serializa o qrcode do MDF-e no xml """ + def gerar_qrcode(self, xml, return_qr=False): + + # Procura atributos no xml + ns = {'ns': NAMESPACE_MDFE} + + # Tag Raiz MDFe Ex: + mdfe = xml + chave = mdfe[0].attrib['Id'].replace('MDFe', '') + tpamb = mdfe.xpath('ns:infMDFe/ns:ide/ns:tpAmb/text()', namespaces=ns)[0] + + url_padrao = MDFE['SVRS']['QRCODE'] + qrcode = f'{url_padrao}?chMDFe={chave}&tpAmb={tpamb}' + + # adiciona tag infMDFeSupl com qrcode + infMDFeSupl = etree.Element('infMDFeSupl') + etree.SubElement(infMDFeSupl, 'qrCodMDFe').text = f'' + + mdfe.insert(1, infMDFeSupl) + + # correção da tag qrCodMDFe + tmdfe = etree.tostring(mdfe, encoding='unicode') + etree.tostring(mdfe.find('.//qrCodMDFe'), encoding='unicode') \ + .replace('\n','').replace('<','<').replace('>','>').replace('amp;','') + mdfe = etree.fromstring(tmdfe) + + if return_qr: + return mdfe, qrcode.strip() + else: + return mdfe + + class SerializacaoNfse(object): def __init__(self, autorizador): "Recebe uma string com o nome do autorizador." @@ -968,3 +1007,416 @@ class SerializacaoNfse(object): return SerializacaoBetha().cancelar(nfse) else: raise Exception('Autorizador não suportado para cancelamento!') + + +class SerializacaoMDFe(Serializacao): + """ Classe de serialização do arquivo xml """ + + _versao = VERSAO_MDFE + + def exportar(self, destino=None, retorna_string=False, limpar=True, **kwargs): + """Gera o(s) arquivo(s) do Manifesto de Documento Fiscais Eletrônicos no padrao oficial + da SEFAZ e Receita Federal, para ser(em) enviado(s) para o webservice ou para ser(em) + armazenado(s) em cache local. + @param destino - + @param retorna_string - Retorna uma string para debug. + @param limpar - Limpa a fonte de dados para não gerar xml com dados duplicados. + """ + try: + # No raiz do XML de saida + raiz = etree.Element('MDFe', xmlns=NAMESPACE_MDFE) + + # Carrega lista de Manifestos + manifestos = self._fonte_dados.obter_lista(_classe=Manifesto, **kwargs) + + for mdfe in manifestos: + raiz.append(self._serializar_manifesto(mdfe, retorna_string=False)) + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=False) + else: + return raiz + except Exception as e: + raise e + finally: + if limpar: + self._fonte_dados.limpar_dados() + + def importar(self, origem): + """Cria as instancias do PyNFe a partir de arquivos XML no formato padrao da + SEFAZ e Receita Federal.""" + + raise Exception('Metodo nao implementado') + + def _serializar_emitente(self, emitente, tag_raiz='emit', retorna_string=True): + raiz = etree.Element(tag_raiz) + + # Dados do emitente + if len(so_numeros(emitente.cpfcnpj)) == 11: + etree.SubElement(raiz, 'CPF').text = so_numeros(emitente.cpfcnpj) + else: + etree.SubElement(raiz, 'CNPJ').text = so_numeros(emitente.cpfcnpj) + etree.SubElement(raiz, 'IE').text = emitente.inscricao_estadual + etree.SubElement(raiz, 'xNome').text = emitente.razao_social + etree.SubElement(raiz, 'xFant').text = emitente.nome_fantasia + # Endereço + endereco = etree.SubElement(raiz, 'enderEmit') + etree.SubElement(endereco, 'xLgr').text = emitente.endereco_logradouro + etree.SubElement(endereco, 'nro').text = emitente.endereco_numero + if emitente.endereco_complemento: + etree.SubElement(endereco, 'xCpl').text = emitente.endereco_complemento + etree.SubElement(endereco, 'xBairro').text = emitente.endereco_bairro + etree.SubElement(endereco, 'cMun').text = obter_codigo_por_municipio( + emitente.endereco_municipio, emitente.endereco_uf) + etree.SubElement(endereco, 'xMun').text = emitente.endereco_municipio + etree.SubElement(endereco, 'CEP').text = so_numeros(emitente.endereco_cep) + etree.SubElement(endereco, 'UF').text = emitente.endereco_uf + if emitente.endereco_telefone: + etree.SubElement(endereco, 'fone').text = emitente.endereco_telefone + etree.SubElement(endereco, 'email').text = emitente.endereco_email + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_municipio_carrega(self, municipio_carrega, tag_raiz='infMunCarrega', retorna_string=True): + raiz = etree.Element(tag_raiz) + etree.SubElement(raiz, 'cMunCarrega').text = str(municipio_carrega.cMunCarrega) + etree.SubElement(raiz, 'xMunCarrega').text = str(municipio_carrega.xMunCarrega) + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_percurso(self, percurso, tag_raiz='infPercurso', retorna_string=True): + raiz = etree.Element(tag_raiz) + etree.SubElement(raiz, 'UFPer').text = percurso.UFPer + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_modal_rodoviario(self, modal_rodoviario, tag_raiz='infModal', retorna_string=True): + + """ + + rodo + infANTT + infCIOT + valePed + infContratante + infPag + veicTracao + prop + condutor + veicReboque + prop + """ + raiz = etree.Element(tag_raiz, versaoModal=self._versao) + rodo = etree.SubElement(raiz, 'rodo') + + infANTT = etree.SubElement(rodo, 'infANTT') + etree.SubElement(infANTT, 'RNTRC').text = modal_rodoviario.rntrc + + # CIOT + if len(modal_rodoviario.ciot) > 0: + for num, item in enumerate(modal_rodoviario.ciot): + infCIOT = etree.SubElement(infANTT, 'infCIOT') + etree.SubElement(infCIOT, 'CIOT').text = item.numero_ciot + if len(item.cpfcnpj) == 11: + etree.SubElement(infCIOT, 'CPF').text = item.cpfcnpj + elif len(item.cpfcnpj) == 14: + etree.SubElement(infCIOT, 'CNPJ').text = item.cpfcnpj + + # Vale Pedágio + if len(modal_rodoviario.pedagio) > 0: + valePed = etree.SubElement(infANTT, 'valePed') + for num, item in enumerate(modal_rodoviario.pedagio): + disp = etree.SubElement(valePed, 'disp') + etree.SubElement(disp, 'CNPJForn').text = item.cnpj_fornecedor + if len(item.cpfcnpj_pagador) == 11: + etree.SubElement(disp, 'CPFPg').text = item.cpfcnpj_pagador + elif len(item.cpfcnpj_pagador) == 14: + etree.SubElement(disp, 'CNPJPg').text = item.cpfcnpj_pagador + etree.SubElement(disp, 'nCompra').text = item.numero_compra + etree.SubElement(disp, 'vValePed').text = '{:.2f}'.format(item.valor_pedagio or 0) # Valor do ICMS + + # Contratantes + if len(modal_rodoviario.contratante) > 0: + for num, item in enumerate(modal_rodoviario.contratante): + infContratante = etree.SubElement(infANTT, 'infContratante') + etree.SubElement(infContratante, 'xNome').text = item.nome + if len(item.cpfcnpj) == 11: + etree.SubElement(infContratante, 'CPF').text = item.cpfcnpj + elif len(item.cpfcnpj) == 14: + etree.SubElement(infContratante, 'CNPJ').text = item.cpfcnpj + + # Veículo Tração + veicTracao = etree.SubElement(rodo, 'veicTracao') + etree.SubElement(veicTracao, 'cInt').text = modal_rodoviario.veiculo_tracao.cInt + etree.SubElement(veicTracao, 'placa').text = modal_rodoviario.veiculo_tracao.placa + etree.SubElement(veicTracao, 'RENAVAM').text = modal_rodoviario.veiculo_tracao.RENAVAM + etree.SubElement(veicTracao, 'tara').text = modal_rodoviario.veiculo_tracao.tara + etree.SubElement(veicTracao, 'capKG').text = modal_rodoviario.veiculo_tracao.capKG + etree.SubElement(veicTracao, 'capM3').text = modal_rodoviario.veiculo_tracao.capM3 + + # Propritario do veículo Tração + if modal_rodoviario.veiculo_tracao.proprietario: + prop = etree.SubElement(veicTracao, 'prop') + + if len(modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj) == 11: + etree.SubElement(prop, 'CPF').text = modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj + elif len(modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj) == 14: + etree.SubElement(prop, 'CNPJ').text = modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj + + etree.SubElement(prop, 'RNTRC').text = modal_rodoviario.veiculo_tracao.proprietario.rntrc + etree.SubElement(prop, 'xNome').text = modal_rodoviario.veiculo_tracao.proprietario.nome + if modal_rodoviario.veiculo_tracao.proprietario.inscricao_estudual != None: + etree.SubElement(prop, 'IE').text = modal_rodoviario.veiculo_tracao.proprietario.inscricao_estudual + etree.SubElement(prop, 'UF').text = modal_rodoviario.veiculo_tracao.proprietario.uf + # tpProp: 0=TACAgregado; 1=TACIndependente; 2=Outros + etree.SubElement(prop, 'tpProp').text = modal_rodoviario.veiculo_tracao.proprietario.tipo + + # condutor 1-n + if len(modal_rodoviario.veiculo_tracao.condutor) > 0: + for num, item_condutor in enumerate(modal_rodoviario.veiculo_tracao.condutor): + condutor = etree.SubElement(veicTracao, 'condutor') + etree.SubElement(condutor, 'xNome').text = item_condutor.nome_motorista + etree.SubElement(condutor, 'CPF').text = item_condutor.cpf_motorista + # fim-condutor + + etree.SubElement(veicTracao, 'tpRod').text = modal_rodoviario.veiculo_tracao.tpRod + etree.SubElement(veicTracao, 'tpCar').text = modal_rodoviario.veiculo_tracao.tpCar + etree.SubElement(veicTracao, 'UF').text = modal_rodoviario.veiculo_tracao.UF + # fim-veicTracao + + # Veículos reboque 1-n + if len(modal_rodoviario.veiculo_reboque) > 0: + for num, item_reboque in enumerate(modal_rodoviario.veiculo_reboque): + veicReboque = etree.SubElement(rodo, 'veicReboque') + etree.SubElement(veicReboque, 'cInt').text = item_reboque.cInt + etree.SubElement(veicReboque, 'placa').text = item_reboque.placa + etree.SubElement(veicReboque, 'RENAVAM').text = item_reboque.RENAVAM + etree.SubElement(veicReboque, 'tara').text = item_reboque.tara + etree.SubElement(veicReboque, 'capKG').text = item_reboque.capKG + etree.SubElement(veicReboque, 'capM3').text = item_reboque.capM3 + + # Propritario do veículo Reboque + if item_reboque.proprietario: + prop = etree.SubElement(veicReboque, 'prop') + + if len(item_reboque.proprietario.cpfcnpj) == 11: + etree.SubElement(prop, 'CPF').text = item_reboque.proprietario.cpfcnpj + elif len(item_reboque.proprietario.cpfcnpj) == 14: + etree.SubElement(prop, 'CNPJ').text = item_reboque.proprietario.cpfcnpj + + etree.SubElement(prop, 'RNTRC').text = item_reboque.proprietario.rntrc + etree.SubElement(prop, 'xNome').text = item_reboque.proprietario.nome + if item_reboque.proprietario.inscricao_estudual != None: + etree.SubElement(prop, 'IE').text = item_reboque.proprietario.inscricao_estudual + etree.SubElement(prop, 'UF').text = item_reboque.proprietario.uf + # tpProp: 0=TACAgregado; 1=TACIndependente; 2=Outros + etree.SubElement(prop, 'tpProp').text = item_reboque.proprietario.tipo + + etree.SubElement(veicReboque, 'tpCar').text = item_reboque.tpCar + etree.SubElement(veicReboque, 'UF').text = item_reboque.UF + # fim-veicReboque + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_documentos(self, documentos, tag_raiz='infDoc', retorna_string=True): + raiz = etree.Element(tag_raiz) + + if len(documentos) <= 0: + raise f'MDFe deve ter uma NFe ou uma CTe vinculadas' + + for num, item in enumerate(documentos): + infMunDescarga = etree.SubElement(raiz, 'infMunDescarga') + etree.SubElement(infMunDescarga, 'cMunDescarga').text = item.cMunDescarga + etree.SubElement(infMunDescarga, 'xMunDescarga').text = item.xMunDescarga + + if len(item.documentos_nfe) > 0: + for num, item_doc in enumerate(item.documentos_nfe): + infNFe = etree.SubElement(infMunDescarga, 'infNFe') + etree.SubElement(infNFe, 'chNFe').text = item_doc.chave_acesso_nfe + + elif len(documentos.documentos_cte) > 0: + for num, item_doc in enumerate(item.documentos_cte): + infCTe = etree.SubElement(infMunDescarga, 'infCTe') + etree.SubElement(infCTe, 'chCTe').text = item_doc.chave_acesso_cte + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_seguradora(self, seguradora, tag_raiz='seg', retorna_string=True): + raiz = etree.Element(tag_raiz) + + infResp = etree.SubElement(raiz, 'infResp') + etree.SubElement(infResp, 'respSeg').text = seguradora.responsavel_seguro + etree.SubElement(infResp, 'CNPJ').text = seguradora.cnpj_responsavel + + infSeg = etree.SubElement(raiz, 'infSeg') + etree.SubElement(infSeg, 'xSeg').text = seguradora.nome_seguradora + etree.SubElement(infSeg, 'CNPJ').text = seguradora.cnpj_seguradora + + etree.SubElement(raiz, 'nApol').text = seguradora.numero_apolice + + if len(seguradora.averbacoes) > 0: + for num, item in enumerate(seguradora.averbacoes): + etree.SubElement(raiz, 'nAver').text = item.numero + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_produto(self, produto, tag_raiz='prodPred', retorna_string=True): + raiz = etree.Element(tag_raiz) + etree.SubElement(raiz, 'tpCarga').text = produto.tipo_carga + etree.SubElement(raiz, 'xProd').text = produto.nome_produto + etree.SubElement(raiz, 'cEAN').text = produto.cean + etree.SubElement(raiz, 'NCM').text = produto.ncm + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_totais(self, totais, tag_raiz='tot', retorna_string=True): + raiz = etree.Element(tag_raiz) + + if totais.qCTe > 0: + etree.SubElement(raiz, 'qCTe').text = str(totais.qCTe) + elif totais.qNFe > 0: + etree.SubElement(raiz, 'qNFe').text = str(totais.qNFe) + + etree.SubElement(raiz, 'vCarga').text = str(totais.vCarga) + if totais.cUnid == 'KG': + etree.SubElement(raiz, 'cUnid').text = '01' + elif totais.cUnid == 'TON': + etree.SubElement(raiz, 'cUnid').text = '02' + else: + raise f'cUnid deve ser KG ou TON' + etree.SubElement(raiz, 'qCarga').text = str(totais.qCarga) + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_lacres(self, lacres, tag_raiz='lacres', retorna_string=True): + raiz = etree.Element(tag_raiz) + etree.SubElement(raiz, 'nLacre').text = str(lacres.nLacre) + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_responsavel_tecnico(self, responsavel_tecnico, tag_raiz='infRespTec', retorna_string=True): + raiz = etree.Element(tag_raiz) + etree.SubElement(raiz, 'CNPJ').text = responsavel_tecnico.cnpj + etree.SubElement(raiz, 'xContato').text = responsavel_tecnico.contato + etree.SubElement(raiz, 'email').text = responsavel_tecnico.email + etree.SubElement(raiz, 'fone').text = responsavel_tecnico.fone + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz + + def _serializar_manifesto(self, manifesto, tag_raiz='infMDFe', retorna_string=True): + raiz = etree.Element(tag_raiz, versao=self._versao) + + # 'Id' da tag raiz + # Ex.: MDFe35080599999090910270550010000000011518005123 + raiz.attrib['Id'] = manifesto.identificador_unico + + tz = datetime.now().astimezone().strftime('%z') + tz = "{}:{}".format(tz[:-2], tz[-2:]) + + # Dados do Manifesto + ide = etree.SubElement(raiz, 'ide') + etree.SubElement(ide, 'cUF').text = CODIGOS_ESTADOS[manifesto.uf] + etree.SubElement(ide, 'tpAmb').text = str(self._ambiente) + etree.SubElement(ide, 'tpEmit').text = str(manifesto.tipo_emitente) + + # 0=nenhum; 1=etc; 2=tac; 3=ctc + if manifesto.tipo_transportador != 0: + etree.SubElement(ide, 'tpTransp').text = str(manifesto.tipo_transportador) + + etree.SubElement(ide, 'mod').text = str(manifesto.modelo) + etree.SubElement(ide, 'serie').text = manifesto.serie + etree.SubElement(ide, 'nMDF').text = str(manifesto.numero_mdfe) + etree.SubElement(ide, 'cMDF').text = manifesto.codigo_numerico_aleatorio + etree.SubElement(ide, 'cDV').text = manifesto.dv_codigo_numerico_aleatorio + etree.SubElement(ide, 'modal').text = str(manifesto.modal) + etree.SubElement(ide, 'dhEmi').text = manifesto.data_emissao.strftime('%Y-%m-%dT%H:%M:%S') + tz + etree.SubElement(ide, 'tpEmis').text = str(manifesto.forma_emissao) + etree.SubElement(ide, 'procEmi').text = str(manifesto.processo_emissao) + etree.SubElement(ide, 'verProc').text = f'{self._nome_aplicacao} {manifesto.versao_processo_emissao}' + etree.SubElement(ide, 'UFIni').text = manifesto.UFIni + etree.SubElement(ide, 'UFFim').text = manifesto.UFFim + + # Municipios de Carregamento + for num, item in enumerate(manifesto.infMunCarrega): + ide.append(self._serializar_municipio_carrega(item, retorna_string=False)) + + # UFs Percurso + for num, item in enumerate(manifesto.infPercurso): + ide.append(self._serializar_percurso(item, retorna_string=False)) + + if manifesto.dhIniViagem != None: + etree.SubElement(ide, 'dhIniViagem').text = manifesto.dhIniViagem.strftime('%Y-%m-%dT%H:%M:%S') + tz + # - fim ide + + # Emitente + raiz.append(self._serializar_emitente(manifesto.emitente, retorna_string=False)) + + # infModal rodo + raiz.append(self._serializar_modal_rodoviario(manifesto.modal_rodoviario, retorna_string=False)) + + # infDoc infCTe ou infNFe + raiz.append(self._serializar_documentos(manifesto.documentos, retorna_string=False)) + + # seg + if len(manifesto.seguradora) > 0: + for num, item in enumerate(manifesto.seguradora): + raiz.append(self._serializar_seguradora(item, retorna_string=False)) + + # prodPred + if len(manifesto.produto) > 0: + raiz.append(self._serializar_produto(manifesto.produto[0], retorna_string=False)) + + # totais + raiz.append(self._serializar_totais(manifesto.totais, retorna_string=False)) + + # lacres + if len(manifesto.lacres) > 0: + for num, item in enumerate(manifesto.lacres): + raiz.append(self._serializar_lacres(item, retorna_string=False)) + + # Informações adicionais + if manifesto.informacoes_adicionais_interesse_fisco or manifesto.informacoes_complementares_interesse_contribuinte: + info_ad = etree.SubElement(raiz, 'infAdic') + if manifesto.informacoes_adicionais_interesse_fisco: + etree.SubElement(info_ad, 'infAdFisco').text = manifesto.informacoes_adicionais_interesse_fisco + if manifesto.informacoes_complementares_interesse_contribuinte: + etree.SubElement(info_ad, 'infCpl').text = manifesto.informacoes_complementares_interesse_contribuinte + + # Responsavel Tecnico NT2018/003 + if manifesto.responsavel_tecnico: + raiz.append(self._serializar_responsavel_tecnico( + manifesto.responsavel_tecnico[0], retorna_string=False)) + + if retorna_string: + return etree.tostring(raiz, encoding="unicode", pretty_print=True) + else: + return raiz From e473ccd3a15a3490f4858b5d4baa7e4f4000204a Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Wed, 8 Jul 2020 16:18:09 -0400 Subject: [PATCH 07/29] =?UTF-8?q?corre=C3=A7=C3=A3o=20da=20serializa=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20cancelamento=20da=20mdfe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index d4f89b4..575b5fe 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -743,6 +743,11 @@ class SerializacaoXML(Serializacao): etree.SubElement(e, 'tpEvento').text = evento.tp_evento etree.SubElement(e, 'nSeqEvento').text = str(evento.n_seq_evento) det = etree.SubElement(e, 'detEvento', versaoEvento=VERSAO_MDFE) + if evento.descricao == 'Cancelamento': + cancelamento = etree.SubElement(det, 'evCancMDFe') + etree.SubElement(cancelamento, 'descEvento').text = evento.descricao + etree.SubElement(cancelamento, 'nProt').text = evento.protocolo + etree.SubElement(cancelamento, 'xJust').text = evento.justificativa if evento.descricao == 'Encerramento': encerramento = etree.SubElement(det, 'evEncMDFe') etree.SubElement(encerramento, 'descEvento').text = evento.descricao From 4b302ac0d71135dc877fd14a93d49f437ae0f7ca Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Wed, 8 Jul 2020 16:18:47 -0400 Subject: [PATCH 08/29] =?UTF-8?q?corre=C3=A7=C3=A3o=20da=20consulta=20do?= =?UTF-8?q?=20status=20da=20nfe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/nfe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pynfe/processamento/nfe.py b/pynfe/processamento/nfe.py index b15da6c..bbf31df 100644 --- a/pynfe/processamento/nfe.py +++ b/pynfe/processamento/nfe.py @@ -239,13 +239,13 @@ class ComunicacaoNFe(Comunicacao): xml = self._construir_xml_soap('NFeRecepcaoEvento4', raiz) return self._post(url, xml) - def status_servico(self): + def status_servico(self, modelo): """ Verifica status do servidor da receita. :param modelo: modelo é a string com tipo de serviço que deseja consultar, Ex: nfe ou nfce :return: """ - url = self._get_url('mdfe', 'STATUS') + url = self._get_url(modelo, 'STATUS') # Monta XML do corpo da requisição raiz = etree.Element('consStatServ', versao=VERSAO_PADRAO, xmlns=NAMESPACE_NFE) etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) From 8bfe84c8d8722c8097fccea5419038c4d976c832 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Thu, 9 Jul 2020 08:12:02 -0400 Subject: [PATCH 09/29] =?UTF-8?q?corre=C3=A7=C3=A3o=20no=20envio=20do=20ev?= =?UTF-8?q?ento=20"CONSULTA=20N=C3=83O=20ENCERRADOS"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/mdfe.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pynfe/processamento/mdfe.py b/pynfe/processamento/mdfe.py index b2d7256..7e2a87c 100644 --- a/pynfe/processamento/mdfe.py +++ b/pynfe/processamento/mdfe.py @@ -2,7 +2,6 @@ import time import re import requests -import collections from io import StringIO import base64 @@ -152,11 +151,7 @@ class ComunicacaoMDFe(Comunicacao): def consulta_nao_encerrados(self, cpfcnpj): url = self._get_url('NAO_ENCERRADOS') # Monta XML do corpo da requisição - # raiz = etree.Element('consMDFeNaoEnc', xmlns=NAMESPACE_MDFE, versao=self._versao) - attr = collections.OrderedDict() - attr['xmlns'] = NAMESPACE_MDFE - attr['versao'] = self._versao - raiz = etree.Element('consMDFeNaoEnc', attr) + raiz = etree.Element('consMDFeNaoEnc', xmlns=NAMESPACE_MDFE, versao=self._versao) etree.SubElement(raiz, 'tpAmb').text = str(self._ambiente) etree.SubElement(raiz, 'xServ').text = 'CONSULTAR NÃO ENCERRADOS' if len(cpfcnpj) == 11: @@ -197,13 +192,13 @@ class ComunicacaoMDFe(Comunicacao): def _construir_xml_soap(self, metodo, dados): """Mota o XML para o envio via SOAP""" - ns = collections.OrderedDict() - ns['xsi'] = self._namespace_xsi - ns['xsd'] = self._namespace_xsd - ns[self._soap_version] = self._namespace_soap raiz = etree.Element( - '{%s}Envelope' % self._namespace_soap, - nsmap=ns + '{%s}Envelope' % NAMESPACE_SOAP, + nsmap={ + 'xsi': NAMESPACE_XSI, + 'xsd': NAMESPACE_XSD, + 'soap': NAMESPACE_SOAP + } ) if self._header: @@ -256,7 +251,7 @@ class ComunicacaoMDFe(Comunicacao): etree.tostring(xml, encoding='unicode').replace('\n', '') ) xml = xml_declaration + xml - + xml = xml.encode('utf8') # necessário para o evento "CONSULTAR NÃO ENCERRADOS" # print(xml) # print('-' * 20) From f8e84e51dedc6ef1cff4afe80f4e0e82ebb0d89f Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Thu, 9 Jul 2020 16:27:47 -0400 Subject: [PATCH 10/29] =?UTF-8?q?corre=C3=A7=C3=A3o=20do=20envio=20ass?= =?UTF-8?q?=C3=ADncrono?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/mdfe.py | 25 +++++++++---------------- pynfe/processamento/serializacao.py | 15 +++++++-------- pynfe/utils/webservices.py | 4 ++-- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/pynfe/processamento/mdfe.py b/pynfe/processamento/mdfe.py index 7e2a87c..b5449d0 100644 --- a/pynfe/processamento/mdfe.py +++ b/pynfe/processamento/mdfe.py @@ -69,7 +69,6 @@ class ComunicacaoMDFe(Comunicacao): # Monta XML do corpo da requisição raiz = etree.Element('enviMDFe', xmlns=NAMESPACE_MDFE, versao=VERSAO_MDFE) etree.SubElement(raiz, 'idLote').text = str(id_lote) # numero autoincremental gerado pelo sistema - etree.SubElement(raiz, 'indSinc').text = str(ind_sinc) # 0 para assincrono, 1 para sincrono raiz.append(manifesto) # Monta XML para envio da requisição @@ -96,13 +95,9 @@ class ComunicacaoMDFe(Comunicacao): if ind_sinc == 1: try: # Protocolo com envio OK - try: - inf_prot = prot[0][0] # root protMDFe - except IndexError: - # Estados como GO vem com a tag header - inf_prot = prot[1][0] - + inf_prot = prot[1][0] lote_status = inf_prot.xpath("ns:retEnviMDFe/ns:cStat", namespaces=ns)[0].text + # Lote processado if lote_status == self._edoc_situacao_lote_processado: prot_mdfe = inf_prot.xpath("ns:retEnviMDFe/ns:protMDFe", namespaces=ns)[0] @@ -120,7 +115,7 @@ class ComunicacaoMDFe(Comunicacao): return 1, retorno, manifesto else: # Retorna id do protocolo para posterior consulta em caso de sucesso. - rec = prot[0][0] + rec = prot[1][0] status = rec.xpath("ns:retEnviMDFe/ns:cStat", namespaces=ns)[0].text # Lote Recebido com Sucesso! if status == self._edoc_situacao_arquivo_recebido_com_sucesso: @@ -195,9 +190,9 @@ class ComunicacaoMDFe(Comunicacao): raiz = etree.Element( '{%s}Envelope' % NAMESPACE_SOAP, nsmap={ - 'xsi': NAMESPACE_XSI, - 'xsd': NAMESPACE_XSD, - 'soap': NAMESPACE_SOAP + 'xsi': self._namespace_xsi, + 'xsd': self._namespace_xsd, + self._soap_version: self._namespace_soap } ) @@ -244,16 +239,14 @@ class ComunicacaoMDFe(Comunicacao): try: xml_declaration = '' - # limpa xml com caracteres bugados para infNFeSupl em NFC-e + # limpa xml com caracteres bugados para infMDFeSupl em NFC-e xml = re.sub( - '(.*?)', - lambda x: x.group(0).replace('<', '<').replace('>', '>').replace('&', ''), + '(.*?)', + lambda x: x.group(0).replace('<', '<').replace('>', '>').replace('amp;', ''), etree.tostring(xml, encoding='unicode').replace('\n', '') ) xml = xml_declaration + xml xml = xml.encode('utf8') # necessário para o evento "CONSULTAR NÃO ENCERRADOS" - # print(xml) - # print('-' * 20) # Faz o request com o servidor result = requests.post( diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 575b5fe..b7ddc98 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -938,13 +938,12 @@ class SerializacaoQrcodeMDFe(object): # adiciona tag infMDFeSupl com qrcode infMDFeSupl = etree.Element('infMDFeSupl') etree.SubElement(infMDFeSupl, 'qrCodMDFe').text = f'' - mdfe.insert(1, infMDFeSupl) - # correção da tag qrCodMDFe + # correção da tag qrCode, retira caracteres pois e CDATA tmdfe = etree.tostring(mdfe, encoding='unicode') etree.tostring(mdfe.find('.//qrCodMDFe'), encoding='unicode') \ - .replace('\n','').replace('<','<').replace('>','>').replace('amp;','') + .replace('\n', '').replace('<', '<').replace('>', '>').replace('amp;', '') mdfe = etree.fromstring(tmdfe) if return_qr: @@ -1126,7 +1125,7 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(infANTT, 'RNTRC').text = modal_rodoviario.rntrc # CIOT - if len(modal_rodoviario.ciot) > 0: + if modal_rodoviario.ciot != None: for num, item in enumerate(modal_rodoviario.ciot): infCIOT = etree.SubElement(infANTT, 'infCIOT') etree.SubElement(infCIOT, 'CIOT').text = item.numero_ciot @@ -1136,7 +1135,7 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(infCIOT, 'CNPJ').text = item.cpfcnpj # Vale Pedágio - if len(modal_rodoviario.pedagio) > 0: + if modal_rodoviario.pedagio != None: valePed = etree.SubElement(infANTT, 'valePed') for num, item in enumerate(modal_rodoviario.pedagio): disp = etree.SubElement(valePed, 'disp') @@ -1149,7 +1148,7 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(disp, 'vValePed').text = '{:.2f}'.format(item.valor_pedagio or 0) # Valor do ICMS # Contratantes - if len(modal_rodoviario.contratante) > 0: + if modal_rodoviario.contratante != None: for num, item in enumerate(modal_rodoviario.contratante): infContratante = etree.SubElement(infANTT, 'infContratante') etree.SubElement(infContratante, 'xNome').text = item.nome @@ -1185,7 +1184,7 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(prop, 'tpProp').text = modal_rodoviario.veiculo_tracao.proprietario.tipo # condutor 1-n - if len(modal_rodoviario.veiculo_tracao.condutor) > 0: + if modal_rodoviario.veiculo_tracao.condutor != None: for num, item_condutor in enumerate(modal_rodoviario.veiculo_tracao.condutor): condutor = etree.SubElement(veicTracao, 'condutor') etree.SubElement(condutor, 'xNome').text = item_condutor.nome_motorista @@ -1198,7 +1197,7 @@ class SerializacaoMDFe(Serializacao): # fim-veicTracao # Veículos reboque 1-n - if len(modal_rodoviario.veiculo_reboque) > 0: + if modal_rodoviario.veiculo_reboque != None: for num, item_reboque in enumerate(modal_rodoviario.veiculo_reboque): veicReboque = etree.SubElement(rodo, 'veicReboque') etree.SubElement(veicReboque, 'cInt').text = item_reboque.cInt diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index 31f87ae..9e45e80 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -503,8 +503,8 @@ NFSE = { MDFE = { # unico autorizador de MDF-e 'SVRS': { - 'RECEPCAO': 'mdferecepcao/MDFeRecepcao.asmx', - 'RECEPCAO_SINC': 'mdferecepcao/MDFeRecepcaoSinc.asmx', + 'RECEPCAO': 'MDFeRecepcao/MDFeRecepcao.asmx', + 'RECEPCAO_SINC': 'MDFeRecepcaoSinc/MDFeRecepcaoSinc.asmx', 'RET_RECEPCAO': 'mdferetrecepcao/MDFeRetRecepcao.asmx', 'EVENTOS': 'mdferecepcaoevento/MDFeRecepcaoEvento.asmx', 'CONSULTA': 'mdfeconsulta/MDFeConsulta.asmx', From 85a6e6ad243df79f8d0d2b210239669dd5f94511 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Sat, 18 Jul 2020 12:10:03 -0400 Subject: [PATCH 11/29] =?UTF-8?q?MDFe:=20corre=C3=A7=C3=A3o=20na=20seriali?= =?UTF-8?q?za=C3=A7=C3=A3o=20do=20ve=C3=ADculo=20tra=C3=A7=C3=A3o=20e=20re?= =?UTF-8?q?boque?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 88 +++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index b7ddc98..1732cfe 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1122,7 +1122,7 @@ class SerializacaoMDFe(Serializacao): rodo = etree.SubElement(raiz, 'rodo') infANTT = etree.SubElement(rodo, 'infANTT') - etree.SubElement(infANTT, 'RNTRC').text = modal_rodoviario.rntrc + etree.SubElement(infANTT, 'RNTRC').text = modal_rodoviario.rntrc.zfill(8) # CIOT if modal_rodoviario.ciot != None: @@ -1158,43 +1158,47 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(infContratante, 'CNPJ').text = item.cpfcnpj # Veículo Tração - veicTracao = etree.SubElement(rodo, 'veicTracao') - etree.SubElement(veicTracao, 'cInt').text = modal_rodoviario.veiculo_tracao.cInt - etree.SubElement(veicTracao, 'placa').text = modal_rodoviario.veiculo_tracao.placa - etree.SubElement(veicTracao, 'RENAVAM').text = modal_rodoviario.veiculo_tracao.RENAVAM - etree.SubElement(veicTracao, 'tara').text = modal_rodoviario.veiculo_tracao.tara - etree.SubElement(veicTracao, 'capKG').text = modal_rodoviario.veiculo_tracao.capKG - etree.SubElement(veicTracao, 'capM3').text = modal_rodoviario.veiculo_tracao.capM3 - - # Propritario do veículo Tração - if modal_rodoviario.veiculo_tracao.proprietario: - prop = etree.SubElement(veicTracao, 'prop') - - if len(modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj) == 11: - etree.SubElement(prop, 'CPF').text = modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj - elif len(modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj) == 14: - etree.SubElement(prop, 'CNPJ').text = modal_rodoviario.veiculo_tracao.proprietario.cpfcnpj - - etree.SubElement(prop, 'RNTRC').text = modal_rodoviario.veiculo_tracao.proprietario.rntrc - etree.SubElement(prop, 'xNome').text = modal_rodoviario.veiculo_tracao.proprietario.nome - if modal_rodoviario.veiculo_tracao.proprietario.inscricao_estudual != None: - etree.SubElement(prop, 'IE').text = modal_rodoviario.veiculo_tracao.proprietario.inscricao_estudual - etree.SubElement(prop, 'UF').text = modal_rodoviario.veiculo_tracao.proprietario.uf - # tpProp: 0=TACAgregado; 1=TACIndependente; 2=Outros - etree.SubElement(prop, 'tpProp').text = modal_rodoviario.veiculo_tracao.proprietario.tipo - - # condutor 1-n - if modal_rodoviario.veiculo_tracao.condutor != None: - for num, item_condutor in enumerate(modal_rodoviario.veiculo_tracao.condutor): - condutor = etree.SubElement(veicTracao, 'condutor') - etree.SubElement(condutor, 'xNome').text = item_condutor.nome_motorista - etree.SubElement(condutor, 'CPF').text = item_condutor.cpf_motorista - # fim-condutor - - etree.SubElement(veicTracao, 'tpRod').text = modal_rodoviario.veiculo_tracao.tpRod - etree.SubElement(veicTracao, 'tpCar').text = modal_rodoviario.veiculo_tracao.tpCar - etree.SubElement(veicTracao, 'UF').text = modal_rodoviario.veiculo_tracao.UF - # fim-veicTracao + if (len(modal_rodoviario.veiculo_tracao) != 1): + raise f'Permitido somente um único veículo Tração' + + for num, item in enumerate(modal_rodoviario.veiculo_tracao): + veicTracao = etree.SubElement(rodo, 'veicTracao') + etree.SubElement(veicTracao, 'cInt').text = item.cInt + etree.SubElement(veicTracao, 'placa').text = item.placa + etree.SubElement(veicTracao, 'RENAVAM').text = item.RENAVAM + etree.SubElement(veicTracao, 'tara').text = '{:.2f}'.format(item.tara or 0) + etree.SubElement(veicTracao, 'capKG').text = '{:.2f}'.format(item.capKG or 0) + etree.SubElement(veicTracao, 'capM3').text = '{:.2f}'.format(item.capM3 or 0) + + # Propritario do veículo Tração + if item.proprietario: + prop = etree.SubElement(veicTracao, 'prop') + + if len(item.proprietario.cpfcnpj) == 11: + etree.SubElement(prop, 'CPF').text = item.proprietario.cpfcnpj + elif len(item.proprietario.cpfcnpj) == 14: + etree.SubElement(prop, 'CNPJ').text = item.proprietario.cpfcnpj + + etree.SubElement(prop, 'RNTRC').text = item.proprietario.rntrc.zfill(8) + etree.SubElement(prop, 'xNome').text = item.proprietario.nome + if item.proprietario.inscricao_estudual != None: + etree.SubElement(prop, 'IE').text = item.proprietario.inscricao_estudual + etree.SubElement(prop, 'UF').text = item.proprietario.uf + # tpProp: 0=TACAgregado; 1=TACIndependente; 2=Outros + etree.SubElement(prop, 'tpProp').text = item.proprietario.tipo + + # condutor 1-n + if item.condutor != None: + for num, item_condutor in enumerate(item.condutor): + condutor = etree.SubElement(veicTracao, 'condutor') + etree.SubElement(condutor, 'xNome').text = item_condutor.nome_motorista + etree.SubElement(condutor, 'CPF').text = item_condutor.cpf_motorista + # fim-condutor + + etree.SubElement(veicTracao, 'tpRod').text = item.tpRod + etree.SubElement(veicTracao, 'tpCar').text = item.tpCar + etree.SubElement(veicTracao, 'UF').text = item.UF + # fim-veicTracao # Veículos reboque 1-n if modal_rodoviario.veiculo_reboque != None: @@ -1203,9 +1207,9 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(veicReboque, 'cInt').text = item_reboque.cInt etree.SubElement(veicReboque, 'placa').text = item_reboque.placa etree.SubElement(veicReboque, 'RENAVAM').text = item_reboque.RENAVAM - etree.SubElement(veicReboque, 'tara').text = item_reboque.tara - etree.SubElement(veicReboque, 'capKG').text = item_reboque.capKG - etree.SubElement(veicReboque, 'capM3').text = item_reboque.capM3 + etree.SubElement(veicReboque, 'tara').text = '{:.2f}'.format(item_reboque.tara or 0) + etree.SubElement(veicReboque, 'capKG').text = '{:.2f}'.format(item_reboque.capKG or 0) + etree.SubElement(veicReboque, 'capM3').text = '{:.2f}'.format(item_reboque.capM3 or 0) # Propritario do veículo Reboque if item_reboque.proprietario: @@ -1216,7 +1220,7 @@ class SerializacaoMDFe(Serializacao): elif len(item_reboque.proprietario.cpfcnpj) == 14: etree.SubElement(prop, 'CNPJ').text = item_reboque.proprietario.cpfcnpj - etree.SubElement(prop, 'RNTRC').text = item_reboque.proprietario.rntrc + etree.SubElement(prop, 'RNTRC').text = item_reboque.proprietario.rntrc.zfill(8) etree.SubElement(prop, 'xNome').text = item_reboque.proprietario.nome if item_reboque.proprietario.inscricao_estudual != None: etree.SubElement(prop, 'IE').text = item_reboque.proprietario.inscricao_estudual From 9cbdc2bba0766ebdd2ddc34e53a872dc1ef198d6 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Tue, 4 Aug 2020 08:44:33 -0400 Subject: [PATCH 12/29] =?UTF-8?q?fix:=20Corre=C3=A7=C3=A3o=20na=20serializ?= =?UTF-8?q?a=C3=A7=C3=A3o=20de=20valores=20do=20MDFe=20para=20os=20ve?= =?UTF-8?q?=C3=ADculos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 1732cfe..06c7336 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -1166,9 +1166,9 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(veicTracao, 'cInt').text = item.cInt etree.SubElement(veicTracao, 'placa').text = item.placa etree.SubElement(veicTracao, 'RENAVAM').text = item.RENAVAM - etree.SubElement(veicTracao, 'tara').text = '{:.2f}'.format(item.tara or 0) - etree.SubElement(veicTracao, 'capKG').text = '{:.2f}'.format(item.capKG or 0) - etree.SubElement(veicTracao, 'capM3').text = '{:.2f}'.format(item.capM3 or 0) + etree.SubElement(veicTracao, 'tara').text = '{:.0f}'.format(item.tara or 0) + etree.SubElement(veicTracao, 'capKG').text = '{:.0f}'.format(item.capKG or 0) + etree.SubElement(veicTracao, 'capM3').text = '{:.0f}'.format(item.capM3 or 0) # Propritario do veículo Tração if item.proprietario: @@ -1207,7 +1207,7 @@ class SerializacaoMDFe(Serializacao): etree.SubElement(veicReboque, 'cInt').text = item_reboque.cInt etree.SubElement(veicReboque, 'placa').text = item_reboque.placa etree.SubElement(veicReboque, 'RENAVAM').text = item_reboque.RENAVAM - etree.SubElement(veicReboque, 'tara').text = '{:.2f}'.format(item_reboque.tara or 0) + etree.SubElement(veicReboque, 'tara').text = '{:.2f}'.format(item_reboque.tara or 0) etree.SubElement(veicReboque, 'capKG').text = '{:.2f}'.format(item_reboque.capKG or 0) etree.SubElement(veicReboque, 'capM3').text = '{:.2f}'.format(item_reboque.capM3 or 0) @@ -1311,8 +1311,8 @@ class SerializacaoMDFe(Serializacao): elif totais.cUnid == 'TON': etree.SubElement(raiz, 'cUnid').text = '02' else: - raise f'cUnid deve ser KG ou TON' - etree.SubElement(raiz, 'qCarga').text = str(totais.qCarga) + raise 'cUnid deve ser KG ou TON' + etree.SubElement(raiz, 'qCarga').text = str('{:.4f}').format(totais.qCarga or 0) if retorna_string: return etree.tostring(raiz, encoding="unicode", pretty_print=True) From 7cde3cf756405825b93aaba93d28f240ccd11f50 Mon Sep 17 00:00:00 2001 From: Leonardo Gregianin Date: Wed, 5 Aug 2020 23:14:40 -0400 Subject: [PATCH 13/29] =?UTF-8?q?Corre=C3=A7=C3=A3o=20do=20envio=20de=20en?= =?UTF-8?q?cerramento=20na=20convers=C3=A3o=20de=20int=20para=20str?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 06c7336..7433e28 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -753,8 +753,8 @@ class SerializacaoXML(Serializacao): etree.SubElement(encerramento, 'descEvento').text = evento.descricao etree.SubElement(encerramento, 'nProt').text = evento.protocolo etree.SubElement(encerramento, 'dtEnc').text = evento.dtenc.strftime('%Y-%m-%d') - etree.SubElement(encerramento, 'cUF').text = evento.cuf - etree.SubElement(encerramento, 'cMun').text = evento.cmun + etree.SubElement(encerramento, 'cUF').text = str(evento.cuf) + etree.SubElement(encerramento, 'cMun').text = str(evento.cmun) elif evento.descricao == 'Inclusão Condutor': inclusao = etree.SubElement(det, 'evIncCondutorMDFe') etree.SubElement(inclusao, 'descEvento').text = evento.descricao @@ -765,10 +765,10 @@ class SerializacaoXML(Serializacao): inclusao = etree.SubElement(det, 'evIncDFeMDFe') etree.SubElement(inclusao, 'descEvento').text = evento.descricao etree.SubElement(inclusao, 'nProt').text = evento.protocolo - etree.SubElement(inclusao, 'cMunCarrega').text = evento.cmun_carrega + etree.SubElement(inclusao, 'cMunCarrega').text = str(evento.cmun_carrega) etree.SubElement(inclusao, 'xMunCarrega').text = evento.xmun_carrega infDoc = etree.SubElement(inclusao, 'infDoc') - etree.SubElement(infDoc, 'cMunDescarga').text = evento.cmun_descarga + etree.SubElement(infDoc, 'cMunDescarga').text = str(evento.cmun_descarga) etree.SubElement(infDoc, 'xMunDescarga').text = evento.xmun_descarga etree.SubElement(infDoc, 'chNFe').text = evento.chave_nfe elif evento.descricao == 'Pagamento Operacao MDF-e': From 170ca7fd23e7803795c2a61a7343493f892c12ef Mon Sep 17 00:00:00 2001 From: leotada Date: Mon, 24 Aug 2020 22:19:02 -0300 Subject: [PATCH 14/29] [Fix] Corrige URL de consulta de nota em MG --- pynfe/utils/webservices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index 49c6029..783b940 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -352,7 +352,7 @@ 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', From 101ded06f829911369e32813cb13f1ef1a7d3031 Mon Sep 17 00:00:00 2001 From: erikseyti Date: Thu, 24 Sep 2020 11:06:46 -0300 Subject: [PATCH 15/29] =?UTF-8?q?[new]=20add=20frete,=20seguro=20e=20outra?= =?UTF-8?q?s=20despesas=20acess=C3=B3rias=20em=20itens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index c571654..3c3fd73 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -243,8 +243,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 From 3a72289077ba72d27c3763c559bcdfa30e5eb13a Mon Sep 17 00:00:00 2001 From: juniortada Date: Tue, 6 Oct 2020 17:28:52 -0300 Subject: [PATCH 16/29] [fix] corrigido url consulta cadastro RS --- pynfe/processamento/comunicacao.py | 6 ++++-- pynfe/utils/webservices.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pynfe/processamento/comunicacao.py b/pynfe/processamento/comunicacao.py index 2a9bd41..37ba868 100644 --- a/pynfe/processamento/comunicacao.py +++ b/pynfe/processamento/comunicacao.py @@ -200,9 +200,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', 'RN', 'PB', 'SC'] + 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: @@ -358,7 +360,7 @@ class ComunicacaoSefaz(Comunicacao): raise Exception('Modelo não encontrado! Defina modelo="nfe" ou "nfce"') # Estados que utilizam outros ambientes else: - lista_svrs = ['AC', 'AL', 'AP', 'DF', 'ES', 'PB', 'PI', 'RJ', 'RN', 'RO', 'RR', 'SC', 'SE', 'TO'] + 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' diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index 783b940..37d350c 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -389,7 +389,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.', From ed911e033d0ea85844d0a3517b1943b4cf0384ef Mon Sep 17 00:00:00 2001 From: leogregianin Date: Wed, 7 Oct 2020 12:52:50 -0400 Subject: [PATCH 17/29] =?UTF-8?q?Corre=C3=A7=C3=A3o=20na=20serializa=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20vAliqProd=20no=20Pis=20e=20Cofins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 3c3fd73..affa19c 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -381,7 +381,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') @@ -390,7 +390,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 @@ -425,7 +425,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 From 51ec40f92e4106ad6c0d39a7d71a11226a89a71b Mon Sep 17 00:00:00 2001 From: leogregianin Date: Wed, 7 Oct 2020 12:57:20 -0400 Subject: [PATCH 18/29] Adiciona .vscode/ no .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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 From 8ce55069bcfbb36fe5e666eb8712e547d1d0bcaf Mon Sep 17 00:00:00 2001 From: leogregianin Date: Wed, 7 Oct 2020 13:16:43 -0400 Subject: [PATCH 19/29] Adiciona grupo do ICMS com CST 40 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grupo Tributação do ICMS 40, 41. 50. Página 196 do Manual de Orientação do Contribuinte --- pynfe/processamento/serializacao.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 3c3fd73..ec0bddf 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -303,6 +303,10 @@ 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 in ['40', '41', '50']: + icms_item = etree.SubElement(icms, 'ICMS40') + etree.SubElement(icms_item, 'orig').text = str(produto_servico.icms_origem) + etree.SubElement(icms_item, 'CST').text = str(produto_servico.icms_modalidade) 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) From afc36ce601da0b2e41e4103ae7124708dccd9251 Mon Sep 17 00:00:00 2001 From: Erogue Lord Date: Sat, 17 Oct 2020 10:20:02 -0300 Subject: [PATCH 20/29] =?UTF-8?q?excluir=20pasta=20de=20testes=20da=20inst?= =?UTF-8?q?ala=C3=A7=C3=A3o=20cria=C3=A7=C3=A3o=20do=20arquivo=20MANIFEST.?= =?UTF-8?q?in=20inclus=C3=A3o=20de=20um=20extras=5Frequire=20no=20setup.py?= =?UTF-8?q?=20para=20instala=C3=A7=C3=A3o=20de=20dependencias=20do=20nfse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MANIFEST.in | 2 ++ requirements-nfse.txt | 2 +- setup.py | 8 +++++++- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 MANIFEST.in 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/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 4a8f870..f8a3b9b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setuptools.setup( author='TadaSoftware', author_email='tadasoftware@gmail.com', url='https://github.com/TadaSoftware', - packages=setuptools.find_packages(), + packages=setuptools.find_packages(exclude=['tests', 'tests.*']), package_data={ 'pynfe': ['data/**/*.txt'], }, @@ -17,6 +17,12 @@ setuptools.setup( 'lxml', 'signxml', ], + extras_require={ + 'nfse': [ + 'suds-jurko', + 'pyxb==1.2.4', + ], + }, zip_safe=False, python_requires='>=3.6', ) From f0ca867a6c13f09cf3ad64252cff0f7ad73464bc Mon Sep 17 00:00:00 2001 From: leogregianin Date: Mon, 26 Oct 2020 10:49:43 -0400 Subject: [PATCH 21/29] [feat] Grupo de pessoas autorizadas a baixar XML --- pynfe/entidades/notafiscal.py | 12 ++++++++++++ pynfe/processamento/serializacao.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index 0036160..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) @@ -1026,3 +1035,6 @@ class NotaFiscalResponsavelTecnico(Entidade): email = str() fone = str() csrt = str() + +class AutorizadosBaixarXML(Entidade): + CPFCNPJ = str() diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 3c3fd73..19b0bce 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -214,6 +214,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) @@ -555,6 +568,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) From d9bac91547668a50392d161305f6fd02bf66090e Mon Sep 17 00:00:00 2001 From: juniortada Date: Thu, 5 Nov 2020 20:02:33 -0300 Subject: [PATCH 22/29] [fix] update url consulta cadastro mg --- pynfe/utils/webservices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index 37d350c..aa2ce29 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -355,7 +355,7 @@ NFE = { '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' }, From 02ca977a1dcad087f5d408b916bdd9ab3ae2efbe Mon Sep 17 00:00:00 2001 From: Sergio Oliveira Date: Sun, 6 Dec 2020 08:33:31 -0300 Subject: [PATCH 23/29] =?UTF-8?q?Melhoria=20no=20tratamento=20de=20excess?= =?UTF-8?q?=C3=B5es=20na=20abertura=20do=20arquivo=20do=20certificado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Essa mudança também evita que o arquivo permaneça aberto após a leitura de seu conteúdo. --- pynfe/entidades/certificado.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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()) From c036af426b5a102876035ba59f575db752aab74c Mon Sep 17 00:00:00 2001 From: leogregianin Date: Wed, 24 Feb 2021 21:24:01 -0400 Subject: [PATCH 24/29] =?UTF-8?q?Altera=C3=A7=C3=A3o=20para=2010=20casas?= =?UTF-8?q?=20decimais=20vUnCom=20e=20vUnTrib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 2afb8d9..4a4a280 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -245,7 +245,7 @@ class SerializacaoXML(Serializacao): 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) - etree.SubElement(prod, 'vUnCom').text = str('{:.4f}').format(produto_servico.valor_unitario_comercial or 0) + etree.SubElement(prod, 'vUnCom').text = str('{:.10f}').format(produto_servico.valor_unitario_comercial or 0) """ Código Especificador da Substituição Tributária – CEST, que estabelece a sistemática de uniformização e identificação das mercadorias e bens passíveis de sujeição aos regimes de substituição tributária e de antecipação de recolhimento do ICMS. """ #if produto_servico.cest: @@ -254,7 +254,7 @@ class SerializacaoXML(Serializacao): etree.SubElement(prod, 'cEANTrib').text = produto_servico.ean_tributavel etree.SubElement(prod, 'uTrib').text = produto_servico.unidade_tributavel etree.SubElement(prod, 'qTrib').text = str(produto_servico.quantidade_tributavel) - etree.SubElement(prod, 'vUnTrib').text = '{:.4f}'.format(produto_servico.valor_unitario_tributavel or 0) + etree.SubElement(prod, 'vUnTrib').text = '{:.10f}'.format(produto_servico.valor_unitario_tributavel or 0) # frete if produto_servico.total_frete: From 016a8dfbf23d60020a99dcbf5bba3a234818c06e Mon Sep 17 00:00:00 2001 From: leogregianin Date: Wed, 24 Feb 2021 21:49:37 -0400 Subject: [PATCH 25/29] =?UTF-8?q?Cria=C3=A7=C3=A3o=20do=20grupo=20de=20com?= =?UTF-8?q?bust=C3=ADvel=20dentro=20do=20produto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/entidades/notafiscal.py | 22 ++++++++++++++++++++++ pynfe/processamento/serializacao.py | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/pynfe/entidades/notafiscal.py b/pynfe/entidades/notafiscal.py index 4c204ed..4edd2e2 100644 --- a/pynfe/entidades/notafiscal.py +++ b/pynfe/entidades/notafiscal.py @@ -598,6 +598,28 @@ class NotaFiscalProduto(Entidade): # - Produto especifico (seleciona de lista) - NF_PRODUTOS_ESPECIFICOS produto_especifico = str() + # Grupo de informações de Combustível + # Código de produto da ANP + cProdANP = str() + + # Descrição do produto conforme ANP + descANP = str() + + # Percentual de Gás derivado do Petróleo + pGLP = Decimal() + + # Percentual de gás natural nacional + pGNn = Decimal() + + # Percentual do gás natural importado + pGNi = Decimal() + + # Valor de Partida (apenas para GLP) + vPart = Decimal() + + # Sigla da UF de consumo – (OBS: Deve ser a Sigla e não o Código da UF) + UFCons = str() + # - Tributos # - ICMS # - Situacao tributaria (obrigatorio - seleciona de lista) - ICMS_TIPOS_TRIBUTACAO diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 2afb8d9..3a16c7b 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -283,6 +283,17 @@ class SerializacaoXML(Serializacao): if produto_servico.numero_item: etree.SubElement(prod, 'nItemPed').text = str(produto_servico.numero_item) + # Combustível + if produto_servico.cProdANP: + combustivel = etree.SubElement(prod, 'comb') + etree.SubElement(combustivel, 'cProdANP').text = str(produto_servico.cProdANP) + etree.SubElement(combustivel, 'descANP').text = str(produto_servico.descANP) + etree.SubElement(combustivel, 'pGLP').text = '{:.4f}'.format(produto_servico.pGLP or 0) + etree.SubElement(combustivel, 'pGNn').text = '{:.4f}'.format(produto_servico.pGNn or 0) + etree.SubElement(combustivel, 'pGNi').text = '{:.4f}'.format(produto_servico.pGNi or 0) + etree.SubElement(combustivel, 'vPart').text = '{:.2f}'.format(produto_servico.vPart or 0) + etree.SubElement(combustivel, 'UFCons').text = str(produto_servico.UFCons) + # Imposto imposto = etree.SubElement(raiz, 'imposto') From a66c1a0d55c12f9edc54a841252690d20d18fbd8 Mon Sep 17 00:00:00 2001 From: leogregianin Date: Wed, 24 Feb 2021 22:01:23 -0400 Subject: [PATCH 26/29] Adiciona grupo do ICMS com CST 30, 70 e 90 --- pynfe/processamento/serializacao.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 2afb8d9..3bcd4a4 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -359,6 +359,50 @@ class SerializacaoXML(Serializacao): etree.SubElement(icms_item, 'vBCFCP').text = '{:.2f}'.format(produto_servico.fcp_base_calculo or 0) # Base de calculo FCP etree.SubElement(icms_item, 'pFCP').text = '{:.2f}'.format(produto_servico.fcp_percentual or 0) # Percentual FCP etree.SubElement(icms_item, 'vFCP').text = '{:.2f}'.format(produto_servico.fcp_valor or 0) # Valor Fundo Combate a Pobreza + # 30=Isenta / não tributada e com cobrança do ICMS por substituição tributária + elif produto_servico.icms_modalidade == '30': + etree.SubElement(icms_item, 'modBCST').text = str(produto_servico.icms_st_modalidade_determinacao_bc) + etree.SubElement(icms_item, 'pMVAST').text = '{:.2f}'.format(produto_servico.icms_st_percentual_adicional or 0) # Percentual da margem de valor Adicionado do ICMS ST + etree.SubElement(icms_item, 'pRedBCST').text = '{:.2f}'.format(produto_servico.icms_st_percentual_reducao_bc or 0) # APercentual da Redução de BC do ICMS ST + etree.SubElement(icms_item, 'vBCST').text = '{:.2f}'.format(produto_servico.icms_st_valor_base_calculo or 0) + etree.SubElement(icms_item, 'pICMSST').text = '{:.2f}'.format(produto_servico.icms_st_aliquota or 0) + etree.SubElement(icms_item, 'vICMSST').text = '{:.2f}'.format(produto_servico.icms_st_valor or 0) + if produto_servico.icms_desonerado > 0: + etree.SubElement(icms_item, 'vICMSDeson').text = '{:.2f}'.format(produto_servico.icms_desonerado or 0) # Valor do ICMS Desonerado + etree.SubElement(icms_item, 'motDesICMS').text = str(produto_servico.icms_motivo_desoneracao) + # 70=Com redução da BC e cobrança do ICMS por substituição tributária + elif produto_servico.icms_modalidade == '70': + etree.SubElement(icms_item, 'modBC').text = str(produto_servico.icms_modalidade_determinacao_bc) + etree.SubElement(icms_item, 'pRedBC').text = '{:.2f}'.format(produto_servico.icms_percentual_reducao_bc or 0) # Percentual da Redução de BC + etree.SubElement(icms_item, 'vBC').text = '{:.2f}'.format(produto_servico.icms_valor_base_calculo or 0) # Valor da BC do ICMS + etree.SubElement(icms_item, 'pICMS').text = '{:.2f}'.format(produto_servico.icms_aliquota or 0) # Alíquota do imposto + etree.SubElement(icms_item, 'vICMS').text = '{:.2f}'.format(produto_servico.icms_valor or 0) # Valor do ICMS + + etree.SubElement(icms_item, 'modBCST').text = str(produto_servico.icms_st_modalidade_determinacao_bc) + etree.SubElement(icms_item, 'pMVAST').text = '{:.2f}'.format(produto_servico.icms_st_percentual_adicional or 0) # Percentual da margem de valor Adicionado do ICMS ST + etree.SubElement(icms_item, 'pRedBCST').text = '{:.2f}'.format(produto_servico.icms_st_percentual_reducao_bc or 0) # APercentual da Redução de BC do ICMS ST + etree.SubElement(icms_item, 'vBCST').text = '{:.2f}'.format(produto_servico.icms_st_valor_base_calculo or 0) + etree.SubElement(icms_item, 'pICMSST').text = '{:.2f}'.format(produto_servico.icms_st_aliquota or 0) + etree.SubElement(icms_item, 'vICMSST').text = '{:.2f}'.format(produto_servico.icms_st_valor or 0) + + if produto_servico.icms_desonerado > 0: + etree.SubElement(icms_item, 'vICMSDeson').text = '{:.2f}'.format(produto_servico.icms_desonerado or 0) # Valor do ICMS Desonerado + etree.SubElement(icms_item, 'motDesICMS').text = str(produto_servico.icms_motivo_desoneracao) + # 90=Outras + elif produto_servico.icms_modalidade == '90': + etree.SubElement(icms_item, 'vBC').text = '{:.2f}'.format(produto_servico.icms_valor_base_calculo or 0) # Valor da BC do ICMS + etree.SubElement(icms_item, 'pRedBC').text = '{:.2f}'.format(produto_servico.icms_percentual_reducao_bc or 0) # Percentual da Redução de BC + etree.SubElement(icms_item, 'pICMS').text = '{:.2f}'.format(produto_servico.icms_aliquota or 0) # Alíquota do imposto + etree.SubElement(icms_item, 'vICMS').text = '{:.2f}'.format(produto_servico.icms_valor or 0) # Valor do ICMS + + if (produto_servico.icms_st_valor_base_calculo > 0) and (produto_servico.icms_st_valor > 0): + etree.SubElement(icms_item, 'modBCST').text = str(produto_servico.icms_st_modalidade_determinacao_bc) + etree.SubElement(icms_item, 'pMVAST').text = '{:.2f}'.format(produto_servico.icms_st_percentual_adicional or 0) # Percentual da margem de valor Adicionado do ICMS ST + etree.SubElement(icms_item, 'pRedBCST').text = '{:.2f}'.format(produto_servico.icms_st_percentual_reducao_bc or 0) # APercentual da Redução de BC do ICMS ST + etree.SubElement(icms_item, 'vBCST').text = '{:.2f}'.format(produto_servico.icms_st_valor_base_calculo or 0) + etree.SubElement(icms_item, 'pICMSST').text = '{:.2f}'.format(produto_servico.icms_st_aliquota or 0) + etree.SubElement(icms_item, 'vICMSST').text = '{:.2f}'.format(produto_servico.icms_st_valor or 0) + # Impostos não implementados else: raise NotImplementedError From 47b773a511864cd6d0eed1a649195d3defb2f0f2 Mon Sep 17 00:00:00 2001 From: leogregianin Date: Mon, 29 Mar 2021 09:47:27 -0400 Subject: [PATCH 27/29] =?UTF-8?q?Atualiza=C3=A7=C3=A3o=20das=20URLS=20do?= =?UTF-8?q?=20Mato=20Grosso=20do=20Sul?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/utils/webservices.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index aa2ce29..538534f 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -245,13 +245,16 @@ NFCE = { 'HOMOLOGACAO': 'https://nfce-homologacao.' }, 'MS': { - 'STATUS': '', - 'AUTORIZACAO': '', - 'RECIBO': '', - 'CHAVE': '', - 'INUTILIZACAO': '', - 'EVENTOS': '', - 'QR': '' + 'STATUS': 'sefaz.ms.gov.br/ws/NFeStatusServico4?wsdl', + 'AUTORIZACAO': 'sefaz.ms.gov.br/ws/NFeAutorizacao4?wsdl', + 'RECIBO': 'sefaz.ms.gov.br/ws/NFeRetAutorizacao4?wsdl', + 'CHAVE': 'sefaz.ms.gov.br/ws/NFeConsultaProtocolo4?wsdl', + 'INUTILIZACAO': 'sefaz.ms.gov.br/ws/NFeInutilizacao4?wsdl', + 'EVENTOS': 'sefaz.ms.gov.br/ws/NFeRetAutorizacao4', + 'QR': 'http://www.dfe.ms.gov.br/nfce/qrcode', + 'URL': 'http://www.dfe.ms.gov.br/nfce/consulta', + 'HTTPS': 'https://nfce.', + 'HOMOLOGACAO': 'https://hom.nfce.' }, 'MT': { 'QR': 'sefaz.mt.gov.br/nfce/consultanfce?', From 4d58607115ccfac263f25523c257062b27886b1e Mon Sep 17 00:00:00 2001 From: leogregianin Date: Mon, 29 Mar 2021 09:57:25 -0400 Subject: [PATCH 28/29] =?UTF-8?q?Atualiza=C3=A7=C3=A3o=20das=20URLS=20da?= =?UTF-8?q?=20NFCe=20de=20Santa=20Catarina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/utils/webservices.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index aa2ce29..b07642d 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -230,7 +230,10 @@ NFCE = { 'CHAVE': '', 'INUTILIZACAO': '', 'EVENTOS': '', - 'QR': '' + 'QR': 'sat.sef.sc.gov.br/nfce/consulta?p=', + 'HTTPS': 'https://', + 'HOMOLOGACAO': 'https://hom.', + 'URL': 'sat.sef.sc.gov.br/nfce/consulta' }, 'RS': { 'STATUS': 'sefazrs.rs.gov.br/ws/NfeStatusServico/NfeStatusServico4.asmx', From 8e395fab91d83003f3e29d8f928e71a4d6969600 Mon Sep 17 00:00:00 2001 From: Miguel Vellasco Date: Mon, 29 Mar 2021 11:59:01 -0300 Subject: [PATCH 29/29] Atualiza URL de NFCe do Rio de Janeiro --- pynfe/utils/webservices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pynfe/utils/webservices.py b/pynfe/utils/webservices.py index 2aaeefa..f1d85fb 100644 --- a/pynfe/utils/webservices.py +++ b/pynfe/utils/webservices.py @@ -189,7 +189,7 @@ NFCE = { 'INUTILIZACAO': '', 'EVENTOS': '', 'QR': 'http://www4.fazenda.rj.gov.br/consultaNFCe/QRCode?', - 'URL': 'www.nfce.fazenda.rj.gov.br/consulta' + 'URL': 'www.fazenda.rj.gov.br/nfce/consulta' }, # Os Web Services de homologação da NFC-e 4.00 são: # https://homologacao.nfce.fazenda.sp.gov.br/ws/NFeAutorizacao4.asmx