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:
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.