Enviando arquivos pelo WhatsApp usando AdvPL/TL++

Hoje iremos continuar nossa série de mensagens para o WhatsApp, mostrando como enviar arquivos, no nosso exemplo iremos criar um job que dispara automaticamente as DANFEs para os clientes.

Primeiramente pessoal, tenha os fontes NETiZAP.prw e zZapSend.prw disponibilizados na primeira semana da série – clique aqui para saber mais.

Agora, iremos adaptar no zZapSend.prw para que ele possa enviar arquivos também. Para isso, iremos criar mais um parâmetro na rotina o cAnexo. E nesse arquivo iremos fazer as tratativas, como pegar a extensão e usar o Encode64 para codificar o conteúdo dele. Um detalhe muito importante, é que esse arquivo deve estar dentro da Protheus Data. Abaixo o código completo:

//Bibliotecas
#Include "TOTVS.ch"

/*/{Protheus.doc} User Function zZapSend
Função que dispara uma mensagem para um smartphone com o aplicativo do WhatsApp
@type  Function
@author Atilio
@since 05/08/2021
@version version
@param cTelDestin, Caractere, Telefone de destino com o país 55 e o ddd - por exemplo 5514999998888
@param cMensagem,  Caractere, Mensagem que será enviada para esse WhatsApp
@param cAnexo,     Caractere, Caminho do arquivo que tem de estar dentro da Protheus Data
@return aRet, Array, Posição 1 define se deu certo o envio com .T. ou .F. e a Posição 2 é o JSON obtido como resposta do envio com o protocolo
@obs É necessário baixar a classe NETiZAP desenvolvida pelo grande Júlio Wittwer
    disponível em https://github.com/siga0984/NETiZAP/blob/master/NETiZAP.prw
/*/

User Function zZapSend(cTelDestin, cMensagem, cAnexo)
    Local aArea  := GetArea()
    Local cLinha := SuperGetMV("MV_X_ZAPLI", .F., "5521986855299")
    Local nPorta := SuperGetMV("MV_X_ZAPPO", .F., 13155)
    Local cChave := SuperGetMV("MV_X_ZAPCH", .F., "fUzanE5ncxlTAWBjMO30")
    Local aRet   := {.F., ""}
    Local oZap
    Local oFile
    Local cArqConteu
    Local cArqEncode
    Local cExtensao
    Local cArqNome
    Default cTelDestin := ""
    Default cMensagem  := ""
    Default cAnexo     := ""

    //Retira caracteres em branco dos lados
    cTelDestin := Alltrim(cTelDestin)
    cMensagem  := Alltrim(cMensagem)

    //Transforma o texto em UTF-8
    cMensagem := EncodeUTF8(cMensagem)

    //Retira caracteres especiais do telefone por exemplo +55 (14) 9 9999-8888
    cTelDestin := StrTran(cTelDestin, " ", "")
    cTelDestin := StrTran(cTelDestin, "+", "")
    cTelDestin := StrTran(cTelDestin, "(", "")
    cTelDestin := StrTran(cTelDestin, ")", "")
    cTelDestin := StrTran(cTelDestin, "-", "")

    //Se houver telefone, mensagem, o número for menor que 12 caracteres (551400000000) e não iniciar com 55 não irá enviar a mensagem
    If Empty(cTelDestin) .And. Empty(cMensagem) .And. Len(cTelDestin) < 12 .And. SubStr(cTelDestin, 1, 2) != "55"
        aRet[1] := .F.
        aRet[2] := '[{"error":"Parametro(s) enviado(s) para zZapSend, invalido(s)"}]'
    Else
        //Se na mesma mensagem, tiver o -Enter- normal e tags br, retira os -Enter-
        If CRLF $ cMensagem .And. "<br>" $ cMensagem
            cMensagem := StrTran(cMensagem, CRLF, '')
        EndIf

        //Agora, irá converter o restante para o formato que o WhatsApp entenda
        cMensagem := StrTran(cMensagem, CRLF   , '\n')
        cMensagem := StrTran(cMensagem, '<br>' , '\n')
        cMensagem := StrTran(cMensagem, '<b>'  , '*')
        cMensagem := StrTran(cMensagem, '</b>' , '*')
        cMensagem := StrTran(cMensagem, '<i>'  , '_')
        cMensagem := StrTran(cMensagem, '</i>' , '_')
        
        //Instancia a classe, e passa os parametros da NETiZAP
        oZap := NETiZAP():New(cLinha, cChave, nPorta)

        //Define o destino e a mensagem de envio
        oZap:SetDestiny(cTelDestin)
        oZap:SetText(cMensagem)

        //Se o parâmetro do arquivo estiver preenchido, e ele existir
        If ! Empty(cAnexo) .And. File(cAnexo)
            //Tenta abrir o arquivo
            oFile   := FwFileReader():New(cAnexo)
            If oFile:Open()
                //Busca o conteúdo do arquivo (foi usado FWFileReader ao invés de MemoRead, por causa de limitação de bytes na leitura)
                cArqConteu  := oFile:FullRead()
                cArqEncode  := Encode64(cArqConteu)

                //Busca a extensão do arquivo
                cExtensao := Upper(SubStr(cAnexo, RAt(".", cAnexo) + 1))

                //Busca o nome do arquivo sem extensão
                cArqNome := SubStr(cAnexo, RAt("\", cAnexo) + 1)

                //Só irá prosseguir, se for um pdf ou uma imagem
                If cExtensao $ "PDF,PNG,JPG,BMP,GIF"
                    //Se a extensão for PDF, tira o pdf do nome, para não ficar por exemplo, arquivo.pdf.pdf
                    If cExtensao == "PDF"
                        cArqNome := SubStr(cArqNome, 1, RAt(".", cArqNome) - 1)
                    EndIf
                    oZap:SetFile(cArqNome, cExtensao, cArqEncode)

                    //Atualiza o retorno conforme se a mensagem foi enviada ou houve falha
                    If oZap:FileSend()
                        aRet[1] := .T.
                        aRet[2] := oZap:GetResponse()
                    Else
                        aRet[1] := .F.
                        aRet[2] := oZap:GetLastError()
                    EndIf

                Else
                    aRet[1] := .F.
                    aRet[2] := '[{"error":"Extensao invalida, aguardando pdf ou imagens png, jpg, bmp e gif"}]'
                EndIf

                oFile:Close()
            EndIf

        //Senão existir o arquivo, irá ser enviado uma mensagem simples
        Else
            //Atualiza o retorno conforme se a mensagem foi enviada ou houve falha
            If oZap:MessageSend()
                aRet[1] := .T.
                aRet[2] := oZap:GetResponse()
            Else
                aRet[1] := .F.
                aRet[2] := oZap:GetLastError()
            EndIf
        EndIf
    EndIf

    RestArea(aArea)
Return aRet

Caso queira testar, coloque uns arquivos na Protheus Data, e acione na chamada, algo +- nesse sentido:

u_zZapSend("5514999998888", "Mensagem de teste com png anexado", "\x_anexos\teste.png")
u_zZapSend("5514999998888", "Mensagem de teste com pdf anexado", "\x_anexos\teste.pdf")

Após fazer essa alteração do arquivo, iremos mexer no processo de vendas, para isso iremos fazer 3 funções:

  • MT410INC: Ponto de entrada na inclusão do pedido de venda, para avisar o cliente que já estamos preparando o pedido
  • M460FIM: Ponto de entrada após gravar a nota fiscal de saída, para avisar o cliente que em breve iremos enviar a DANFE
  • zZapDanfe: Job que irá ser executado automaticamente, varrendo a SF2, verificando quais danfes não foram enviadas ainda, e fazer o processamento e envio delas (essa rotina pode ser agendada no Scheduler do Protheus a cada 10 minutos por exemplo)

Antes de começarmos o desenvolvimento, precisamos criar alguns campos customizados, principalmente para o processo que será executado via Job. Então iremos criar 3 campos na SF2:

  • F2_X_ZAPDA: Tipo Data, irá conter a Data de envio da DANFE pelo WhatsApp
  • F2_X_ZAPHO: Tipo Caractere, tamanho 8, irá conter a Hora de envio da DANFE pelo WhatsApp
  • F2_X_ZAPOB: Tipo Memo, irá conter a observação do envio em caso de falha ou de êxito

Abaixo o código das funções desenvolvidas:

//Bibliotecas
#Include "TOTVS.ch"
#Include "TopConn.ch"

/*/{Protheus.doc} User Function MT410INC
Ponto de Entrada na inclusão do pedido de venda
@type  Function
@author Atilio
@since 12/08/2021
@see https://tdn.totvs.com/display/public/PROT/MT410INC
/*/

User Function MT410INC()
    Local aArea    := GetArea()
    Local aAreaSA1 := SA1->(GetArea())
    Local cNome
    Local cDDD
    Local cTelefone
    Local cMensagem
    Local aZap
    Local cSacola  := "\uD83D\uDECD"
    Local cSorriso := "\uE056"
    Local cOculos  := "\uD83D\uDE0E"

    //Posiciona no cliente
    DbSelectArea('SA1')
    SA1->(DbSetOrder(1)) // Filial + Código + Loja
    If SA1->(DbSeek(FWxFilial('SA1') + SC5->C5_CLIENTE + SC5->C5_LOJACLI))
        //Se tiver o contato, usa ele, do contrário, usa o nome reduzido
        If ! Empty(SA1->A1_CONTATO)
            cNome := Alltrim(SA1->A1_CONTATO)
        Else
            cNome := Alltrim(SA1->A1_NREDUZ)
        EndIf
        cNome := Capital(cNome)

        //Pega o DDD, e se o usuário ter digitado 3 caracteres, retira o primeiro, por exemplo, 014 -> 14
        cDDD := Alltrim(SA1->A1_DDD)
        If Len(cDDD) == 3
            cDDD := SubStr(cDDD, 2)
        EndIf

        //Pega o Telefone e retira os espaços
        cTelefone := Alltrim(SA1->A1_TEL)

        //Se tiver DDD e Telefone
        If ! Empty(cDDD) .And. ! Empty(cTelefone)

            //Monta a mensagem que será enviada ao cliente
            cMensagem := 'Olá <b>' + cNome + '</b> ' + cOculos + '<br>' + CRLF
            cMensagem += '<br>' + CRLF
            cMensagem += 'Recebemos o seu pedido, e iremos preparar o mais rápido possível a separação e expedição dele ' + cSacola + '<br>' + CRLF
            cMensagem += '<br>' + CRLF
            cMensagem += 'Em breve, iremos lhe enviar mais informações ' + cSorriso

            //Faz o envio da mensagem
            aZap := u_zZapSend("55" + cDDD + cTelefone, cMensagem)

            //Se houve falha, mostra a mensagem de erro
            If ! aZap[1]
                MsgStop(aZap[2], "Falha no envio")
            EndIf
        EndIf
    EndIf

    RestArea(aAreaSA1)
    RestArea(aArea)
Return

/*/{Protheus.doc} User Function M460FIM
Ponto de Entrada na geração da nota fiscal de saída
@type  Function
@author Atilio
@since 12/08/2021
@see https://tdn.totvs.com/pages/releaseview.action?pageId=6784180
/*/

User Function M460FIM()
    Local aArea      := GetArea()
    Local aAreaSA1   := SA1->(GetArea())
    Local cAceno     := "\uE41E"
    Local cFolha     := "\uD83D\uDCC4"

    //Posiciona no cliente
    DbSelectArea('SA1')
    SA1->(DbSetOrder(1)) // Filial + Código + Loja
    If SA1->(DbSeek(FWxFilial('SA1') + SF2->F2_CLIENTE + SF2->F2_LOJA))
        //Se tiver o contato, usa ele, do contrário, usa o nome reduzido
        If ! Empty(SA1->A1_CONTATO)
            cNome := Alltrim(SA1->A1_CONTATO)
        Else
            cNome := Alltrim(SA1->A1_NREDUZ)
        EndIf
        cNome := Capital(cNome)

        //Pega o DDD, e se o usuário ter digitado 3 caracteres, retira o primeiro, por exemplo, 014 -> 14
        cDDD := Alltrim(SA1->A1_DDD)
        If Len(cDDD) == 3
            cDDD := SubStr(cDDD, 2)
        EndIf

        //Pega o Telefone e retira os espaços
        cTelefone := Alltrim(SA1->A1_TEL)

        //Se tiver DDD e Telefone
        If ! Empty(cDDD) .And. ! Empty(cTelefone)

            //Monta a mensagem que será enviada ao cliente
            cMensagem := '<b>' + cNome + '</b>, ' + cAceno + '<br>' + CRLF
            cMensagem += '<br>' + CRLF
            cMensagem += 'Seu pedido já está quase concluido, se precisar tirar alguma dúvida conosco, o código de referência é <b>' + Alltrim(SF2->F2_DOC) + '-' + Alltrim(SF2->F2_SERIE) + '</b><br>' + CRLF
            cMensagem += '<br>' + CRLF
            cMensagem += 'Em breve, iremos lhe enviar a DANFE ' + cFolha

            //Faz o envio da mensagem
            aZap := u_zZapSend("55" + cDDD + cTelefone, cMensagem)

            //Se houve falha, mostra a mensagem de erro
            If ! aZap[1]
                MsgStop(aZap[2], "Falha no envio")
            EndIf
        EndIf
    EndIf

    RestArea(aAreaSA1)
    RestArea(aArea)
Return

/*/{Protheus.doc} User Function zZapDanfe
Função que processa as danfes que precisam ser enviadas aos clientes
@type  Function
@author Atilio
@since 12/08/2021
@version version
@obs O ideal é agendar a rotina via Scheduler do Protheus
    É necessário baixar o fonte zGerDanfe, disponível em https://terminaldeinformacao.com/2019/03/02/funcao-para-gerar-danfe-e-xml-de-uma-nota-em-uma-pasta-via-advpl/
/*/

User Function zZapDanfe()
    Local aArea
    Local cUser     := "Administrador"
    Local cPass     := MemoRead("\x_temp\teste.txt") //Aqui você pode adaptar para a sua lógica, mas evite deixar senhas chumbadas no código fonte
    Local cEmpAux   := "99"
    Local cFilAux   := "01"
    Local lContinua := .F.
    Private lJobPvt := .F.

    //Se não tiver ambiente aberto, é job
    If Select("SX2") == 0
        //Reseta o ambiente e abre ele novamente
        RpcClearEnv()
        RpcSetEnv(cEmpAux, cFilAux, cUser, cPass, "FAT")
        lJobPvt := .T.
        lContinua := .T.
    Else
        lContinua := MsgYesNo("Deseja executar o processamento das DANFEs para WhatsApp?", "Atenção")
    EndIf
    aArea := GetArea()

    //Se for continuar, irá chamar a rotina de processamento
    If lContinua
        Processa({|| fGerar() }, "Processando...")
    EndIf

    RestArea(aArea)
Return

Static Function fGerar()
    Local aArea     := GetArea()
    Local cPasta    := "\x_danfe\"
    Local cArqDanfe := ""
    Local cFilBkp   := cFilAnt
    Local cQryDoc   := ""
    Local nAtual    := 0
    Local nTotal    := 0
    Local cBraco    := "\uE14C"
    Local cFolha    := "\uD83D\uDCC4"
    Local cPiscada  := "\uE405"

    //Se a pasta não existir, cria
    If ! ExistDir(cPasta)
        MakeDir(cPasta)
    EndIf

    //Monta a consulta das NFs, que tenham chave de acesso, que não foram enviadas, e
    //   foi colocado uma data de corte para não processar NFs antigas
    cQryDoc := " SELECT " + CRLF
    cQryDoc += " 	F2_FILIAL, " + CRLF
    cQryDoc += " 	F2_DOC, " + CRLF
    cQryDoc += " 	F2_SERIE, " + CRLF
    cQryDoc += " 	F2_CLIENTE, " + CRLF
    cQryDoc += " 	F2_LOJA, " + CRLF
    cQryDoc += " 	SF2.R_E_C_N_O_ AS SF2REC " + CRLF
    cQryDoc += " FROM " + CRLF
    cQryDoc += " 	" + RetSQLName("SF2") + " SF2 " + CRLF
    cQryDoc += " WHERE " + CRLF
    cQryDoc += " 	F2_CHVNFE != '' " + CRLF
    cQryDoc += " 	AND F2_X_ZAPDA = '' " + CRLF
    cQryDoc += " 	AND F2_EMISSAO >= '20210801' " + CRLF
    cQryDoc += " 	AND SF2.D_E_L_E_T_ = '' " + CRLF
    TCQuery cQryDoc New Alias "QRY_DOC"

    //Define o tamanho da régua
    Count To nTotal
    ProcRegua(nTotal)
    QRY_DOC->(DbGoTop())

    //Enquanto houver notas
    While ! QRY_DOC->(EoF())
        //Incrementa a régua
        nAtual++
        IncProc("Analisando NF " + cValToChar(nAtual) + " de " + cValToChar(nTotal) + "...")

        //Se a filial for diferente, troca a empresa na memória
        If cFilAnt != QRY_DOC->F2_FILIAL
            cFilAnt := QRY_DOC->F2_FILIAL
            cNumEmp := Alltrim(cEmpAnt) + AllTrim(cFilAnt)
            OpenFile(cNumEmp)
        EndIf

        //Define o nome do arquivo da danfe
        cArqDanfe := "danfe_" + Alltrim(QRY_DOC->F2_FILIAL) + Alltrim(QRY_DOC->F2_DOC) + Alltrim(QRY_DOC->F2_SERIE)

        //Se o arquivo existir, faz a exclusão dele
        If File(cPasta + cArqDanfe + ".pdf")
            FErase(cPasta + cArqDanfe + ".pdf")
        EndIf

        //Chama a geração da danfe
        u_zGerDanfe(cParNotFis, cParSerie, cPasta, cArqDanfe)
        
        //Se o arquivo existir, irá fazer o disparo da mensagem
        If File(cPasta + cArqDanfe + ".pdf")
            //Posiciona na NF
            DbSelectArea('SF2')
            SF2->(DbGoTo(QRY_DOC->SF2REC))

            //Posiciona no cliente
            DbSelectArea('SA1')
            SA1->(DbSetOrder(1)) // Filial + Código + Loja
            If SA1->(DbSeek(FWxFilial('SA1') + SF2->F2_CLIENTE + SF2->F2_LOJA))
                //Se tiver o contato, usa ele, do contrário, usa o nome reduzido
                If ! Empty(SA1->A1_CONTATO)
                    cNome := Alltrim(SA1->A1_CONTATO)
                Else
                    cNome := Alltrim(SA1->A1_NREDUZ)
                EndIf
                cNome := Capital(cNome)

                //Pega o DDD, e se o usuário ter digitado 3 caracteres, retira o primeiro, por exemplo, 014 -> 14
                cDDD := Alltrim(SA1->A1_DDD)
                If Len(cDDD) == 3
                    cDDD := SubStr(cDDD, 2)
                EndIf

                //Pega o Telefone e retira os espaços
                cTelefone := Alltrim(SA1->A1_TEL)

                //Se tiver DDD e Telefone
                If ! Empty(cDDD) .And. ! Empty(cTelefone)

                    //Monta a mensagem que será enviada ao cliente
                    cMensagem := '<b>' + cNome + '</b>, ' + cBraco + '<br>' + CRLF
                    cMensagem += '<br>' + CRLF
                    cMensagem += 'A Nota Fiscal já foi emitida, segue o PDF ' + cFolha + '<br>' + CRLF
                    cMensagem += '<br>' + CRLF
                    cMensagem += 'Obrigado por comprar conosco, do que precisar conte conosco ' + cPiscada

                    //Faz o envio da mensagem
                    aZap := u_zZapSend("55" + cDDD + cTelefone, cMensagem, cPasta + cArqDanfe + ".pdf")

                    //Se houve falha, grava apenas a mensagem de erro
                    If ! aZap[1]
                        RecLock("SF2", .F.)
                            SF2->F2_X_ZAPOB := aZap[2]
                        SF2->(MsUnlock())

                    //Senão, se foi com sucesso, grava a data e hora também
                    Else
                        RecLock("SF2", .F.)
                            SF2->F2_X_ZAPDA := Date()
                            SF2->F2_X_ZAPHO := Time()
                            SF2->F2_X_ZAPOB := aZap[2]
                        SF2->(MsUnlock())
                    EndIf
                EndIf
            EndIf
        EndIf

        QRY_DOC->(DbSkip())
    EndDo
    QRY_DOC->(DbCloseArea())

    //Volta para a filial que estava
    If cFilAnt != cFilBkp
        cFilAnt := cFilBkp
        cNumEmp := Alltrim(cEmpAnt) + AllTrim(cFilAnt)
        OpenFile(cNumEmp)
    EndIf

    RestArea(aArea)
Return

Aproveitando a lógica da User Function zZapDanfe, você pode acionar a impressão do boleto em pdf na sua empresa, e já anexar também nas mensagens.

Por último, veja o print dos envios das mensagens:

Exemplo das mensagens enviadas

Obs.: Os códigos desenvolvidos nessa série do WhatsApp, estão dentro do nosso GitHub, o link é https://github.com/dan-atilio/AdvPL.

Lembrando também pessoal, se tiverem interesse em adquirir uma licença da API, entrem em contato com o pessoal da NETiZAP clicando aqui, e digam que conhecem o Atilio do Terminal de Informação.

Bom pessoal, por hoje é só.

Abraços e até a próxima.

Dan Atilio (Daniel Atilio)
Especialista em Engenharia de Software pela FIB. Entusiasta de soluções Open Source. E blogueiro nas horas vagas.

Deixe uma resposta