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.
