Baixando XML via API do Espião NFE

No artigo de hoje, vamos demonstrar em como acessar a API do Espião NFE e baixar os XMLs das Notas usando AdvPL / TLPP.

 

Para quem já precisou baixar informações de notas fiscais, e utiliza o serviço do Espião NFE, saiba que eles tem uma API, onde é possível fazermos o consumo direto via código fonte usando recursos das linguagens AdvPL ou TLPP.

 

Basicamente a lógica é a seguinte:

  1. Contratar um plano de serviços do Espião NFE utilizando um CNPJ (na customização, vamos usar o parâmetro MV_X_BXCNP)
  2. Pegar as informações com eles de Cloud Token e de User Token (na customização, vamos acionar através dos parâmetros MV_X_BXCLO e MV_X_BXUSR respectivamente)
  3. É passado as informações dos Tokens no Header da Requisição
  4. Na URL da requisição é passado o CNPJ e a data em questão, algo similar a https://api.espiaonfe.com.br/v1-cloud/consulta/periodo/xmls?dataInicial=2024-11-13&dataFinal=2024-11-13&modelo=55&cnpjCpf=[XXXXXXXXXXXXXX]&tipoPeriodo=E
  5. É efetuado o consumo da API, e se houver dados, faz um For percorrendo os XMLs
  6. Captura a String, usa a Decode64() para decodificar, em seguida usa a GzStrDecomp para descompatar do GZIP
  7. Baixa o XML em questão dentro de uma pasta da Protheus Data
  8. Se teve paginação, repete os passos 5 e 6
  9. Ai depois, basta usarmos os XMLs que foram baixados

 

Um ponto de atenção que percebemos, não sei dizer se foi algo atípico do cliente em que atendemos, mas a cada caractere do XML, vinha um caractere inválido (char 0 da tabela ASCII), e mesmo tentando tratar com StrTran, FWNoAccent, RemCharEsp, OemToAnsi / AnsiToOem, entre outras… O resultado permanecia o mesmo (com caracteres invisíveis entre os outros). Então foi feito um For … Next, e a cada posição par, irá desconsiderar o caractere. Abaixo um print do resultado do XML antes de tratar (através do codebeautify.org):

Exemplo de um Resultado do XML com caracteres a mais

Exemplo de um Resultado do XML com caracteres a mais

 

E abaixo o código fonte desenvolvido conforme a lógica descrita acima:

//Bibliotecas
#Include "Totvs.ch"

/*/{Protheus.doc} User Function zBxEspiao
Integração com o XML Espião
@author Atilio
@since 14/11/2024
@version 1.0
@type function
@see https://api.espiaonfe.com.br/swagger/index.html
/*/

User Function zBxEspiao()
	Local aArea         := FWGetArea()
	Local cResultado    := ''
	Local cErro         := ''
    Local jListaDados   := Nil
    Local nRegAtu       := 0
    Local jXMLAtual     := Nil
    //Arquivo XML
    Local cPastaTemp    := "\x_xmls\"
    Local cArquiXML     := ""
    Local cXMLDecomp    := ""
    Local cXMLConteu    := ""
    //Data do Filtro
    Local dData         := Date()
    Local cData         := dToS(dData)
    Local cDataFormat   := Left(cData, 4) + "-" + StrZero(Month(dData), 2) + "-" + StrZero(Day(dData), 2)
    //Informações usadas na integração
    Local cProxPagina   := ""
    Local cPathRequi    := ""
    Local aHeader       := {}
    Local cEspCloud     := Alltrim(SuperGetMV("MV_X_BXCLO", .F., ""))
    Local cUsrToken     := Alltrim(SuperGetMV("MV_X_BXUSR", .F., ""))
    Local cCnpjCpf      := Alltrim(SuperGetMV("MV_X_BXCNP", .F., ""))
	Local oRestClient   := FWRest():New('https://api.espiaonfe.com.br/v1-cloud/')
    Local cInitDate     := cDataFormat       //"2024-11-13"
    Local cFinalDate    := cDataFormat       //"2024-11-13"
    Local cModelo       := "55"              // 55 (NF-e), 65 (NFC-e), 57 (CT-e), 67 (CT-e OS), 59 (Sat), 41 (NFS-e Nacional)
    Local cTpPer        := "E"               // E (período de emissão do XML) ou I (período de inclusão no Espião Cloud)

    //Se a pasta não existir, cria ela
    If ! ExistDir(cPastaTemp)
        MakeDir(cPastaTemp)
    EndIf
	
	//Monta o cabeçalho da Requisição
	aAdd(aHeader, 'User-Agent: Mozilla/4.0 (compatible; Protheus 12.1.x)')
	aAdd(aHeader, 'Content-Type: application/json; charset=utf-8')
    aAdd(aHeader, 'esp-cloud-token: ' + cEspCloud)
    aAdd(aHeader, 'user-token: ' + cUsrToken)
	
    //Faz um laço percorrendo todas as NFs do dia
    While .T.
        //Monta a url da path
        cPathRequi := 'consulta/periodo/xmls?dataInicial=' + cInitDate + '&dataFinal=' + cFinalDate + '&modelo=' + cModelo + '&cnpjCpf=' + cCnpjCpf + '&tipoPeriodo=' + cTpPer
        If ! Empty(cProxPagina) .And. cProxPagina != "-1"
            cPathRequi += '&codigoProximaPagina=' + cProxPagina
        EndIf
        cProxPagina := ""

        //Define a path e Executa a Requisição
        oRestClient:setPath(cPathRequi)
        If oRestClient:Get(aHeader)
            //Captura o resultado da requisição e converte para Json
            cResultado := oRestClient:GetResult()
            jListaDados := JsonObject():New()
            jListaDados:FromJson(cResultado)

            //Busca a próxima página
            cProxPagina := jListaDados:GetJsonObject("codigoProximaPagina")

            //Busca os XMLs
            aXMLs := jListaDados:GetJsonObject("dados")
            For nRegAtu := 1 To Len(aXMLs)
                jXMLAtual := aXMLs[nRegAtu]

                //Se tiver autorizada
                If Upper(jXMLAtual:GetJsonObject("situacao")) == "AUTORIZADA"
                    //Define o nome do arquivo
                    cArquiXML  := cPastaTemp + "arquivo_" + cData + "_seq_" + StrZero(nRegAtu, 5) + ".xml"

                    //Se o arquivo já existir, apaga
                    If File(cArquiXML)
                        FErase(cArquiXML)
                    EndIf

                    //Pega o conteúdo vindo do WS e aplica a Base64
                    cXMLDecomp := ""
                    cXMLConteu := jXMLAtual:GetJsonObject("xml")
                    cXMLConteu := Decode64(cXMLConteu)

                    //Aplica para descomprimir usando GZip
                    GzStrDecomp(cXMLConteu, Len(cXMLConteu), @cXMLDecomp)
					
					//Como vem com caracteres 'irreconhecíveis', vamos tratar esse cenário, exemplo: 
					//      '< n f e P r o c   v e r s a o = " 4 . 0 0 "   x m l n s = '           ===>          '<nfeProc versao="4.00" xmlns='
                    //Teve que ser feito com SubStr, pois as posições vazias estavam vindo com caractere irreconhecível (Chr(0))
                    /*
                    cXMLDecomp := StrTran(cXMLDecomp, "  ", "|||-|||")
                    cXMLDecomp := StrTran(cXMLDecomp, " ", "")
                    cXMLDecomp := StrTran(cXMLDecomp, "|||-|||", " ")
                    */
					cXMLDecomp := fFormataStr(cXMLDecomp)
					
					//Grava dentro da Protheus Data
                    MemoWrite(cArquiXML, cXMLDecomp)
                EndIf
            Next

            QOut('Sucesso na integração, resultado é: ' + CRLF + cResultado)
        Else
            cErro := oRestClient:GetLastError()
            QOut('Houve um erro na integração: ' + CRLF + cErro)
        EndIf

        //Se não tiver a próxima página
        If Empty(cProxPagina) .Or. cProxPagina == "-1"
            Exit
        EndIf
    EndDo
	
	FWRestArea(aArea)
Return Nil

Static Function fFormataStr(cString)
    Local nAtual   := 0
    Local lPar     := .F.
    Local cNovaStr := ""

    //Percorre a string
    For nAtual := 1 To Len(cString)
        //Encontra se é par
        lPar := nAtual % 2 == 0

        //Se não for par, incrementa a string
        If ! lPar
            cNovaStr += SubStr(cString, nAtual, 1)
        EndIf
    Next
Return cNovaStr

Referências:

 

 

Bom pessoal, por hoje é só.

Abraços e até a próxima.

Dan (Daniel Atilio)
Cristão de ramificação protestante. Especialista em Engenharia de Software pela FIB, graduado em Banco de Dados pela FATEC Bauru e técnico em informática pelo CTI da Unesp. Entusiasta de soluções Open Source e blogueiro nas horas vagas. Autor e mantenedor do portal Terminal de Informação.

Deixe uma resposta

Terminal de Informação