Nesse vídeo será demonstrado em como criar APIs do zero no Protheus usando REST e como debugar elas via VSCode.
Abaixo o vídeo no nosso canal no YouTube:
Abaixo alguns links que foram usados como apoio para montagem do vídeo:
- https://terminaldeinformacao.com/2024/07/29/para-que-serve-o-stateless-na-configuracao-do-rest/
- https://autumncodemaker.com/
- https://tipremium.com/page.php?slug=curso-webservices
- https://www.postman.com/downloads/
Abaixo um exemplo de appserver.ini:
[AMBTST] SourcePath=C:\TOTVS\ERP\Protheus_OP\apo RootPath=C:\TOTVS\ERP\Protheus_Data_OP StartPath=\system\ RPOCustom=C:\TOTVS\ERP\Protheus_OP\apo\mycustom.rpo RpoDb=top RpoLanguage=Portuguese RpoVersion=120 Trace=0 TopMemoMega=1 DBAlias=PROTHEUS_OP DBServer=localhost DBDatabase=MSSQL DBPort=7891 StartSysInDB=1 Theme=Standard [TopConnect] Database=MSSQL Alias=PROTHEUS_OP ProtheusOnly=0 Port=7891 [Drivers] Active=TCP ;MultiProtocolPortSecure=1 ;MultiProtocolPort=1 [TCP] TYPE=TCPIP Port=1268 [General] InstallPath=C:\TOTVS\ERP\Protheus_OP Segmento=YddTQHWW=VZF=yhu Serie===AV app_environment=AMBTST EchoConsoleLogDate=1 AsyncConsoleLog=1 ConsoleLogDate=0 [LICENSECLIENT] server=localhost port=5555 [service] Name=TOTVS_PROTHEUS_OP_REST Displayname=TOTVS | Protheus - Onca Preta - REST [TCPSERVER] Enable=0 [TDS] AllowApplyPatch=* AllowEdit=* AllowMonitor=* EnableDisconnectUser=* EnableSendMessage=* EnableBlockNewConnection=* EnableStopServer=* ;Configurando o serviço que irá rodar no ambiente do REST [HTTPJOB] Main=HTTP_START Environment=AMBTST ;Instrução para quando iniciar o serviço, iniciar o HTTPJOB [ONSTART] Jobs=HTTPJOB RefreshRate=120 ;Habilitar o HTTP para REST [HTTPV11] Enable=1 Sockets=HTTPREST ;Define a porta o HTTP do Rest, e qual/quais serão os URI [HTTPREST] Port=8401 URIs=HTTPURI SECURITY=1 ;Define no URI que irá usar a empresa 99, filial 01 e somente 1 instância [HTTPURI] URL=/rest PrepareIn=99,01 Instances=1,2 AllowOrigin=* CORSEnable=1 Stateless=1 [WEBAPP] Port=8098 ;HideParamsForm=1 ;LastMainProg=U_ZVID0075 ;EnvServer=AMBTST2 [WebApp/webapp] MPP=
Abaixo o código fonte gerado pelo Autumn demonstrado do vídeo:
//Bibliotecas
#Include "Totvs.ch"
#Include "RESTFul.ch"
#Include "TopConn.ch"
/*/{Protheus.doc} WSRESTFUL zWsProdutos
Integração com cadastro de Produtos
@author Atilio
@since 17/05/2025
@version 1.0
@type wsrestful
@obs Codigo gerado automaticamente pelo Autumn Code Maker
@see http://autumncodemaker.com
/*/
WSRESTFUL zWsProdutos DESCRIPTION 'Integração com cadastro de Produtos'
//Atributos
WSDATA id AS STRING
WSDATA updated_at AS STRING
WSDATA limit AS INTEGER
WSDATA page AS INTEGER
//Métodos
WSMETHOD GET ID DESCRIPTION 'Retorna o registro pesquisado' WSSYNTAX '/zWsProdutos/get_id?{id}' PATH 'get_id' PRODUCES APPLICATION_JSON
WSMETHOD GET ALL DESCRIPTION 'Retorna todos os registros' WSSYNTAX '/zWsProdutos/get_all?{updated_at, limit, page}' PATH 'get_all' PRODUCES APPLICATION_JSON
WSMETHOD POST NEW DESCRIPTION 'Inclusão de registro' WSSYNTAX '/zWsProdutos/new' PATH 'new' PRODUCES APPLICATION_JSON
END WSRESTFUL
/*/{Protheus.doc} WSMETHOD GET ID
Busca registro via ID
@author Atilio
@since 17/05/2025
@version 1.0
@type method
@param id, Caractere, String que será pesquisada através do MsSeek
@obs Codigo gerado automaticamente pelo Autumn Code Maker
@see http://autumncodemaker.com
/*/
WSMETHOD GET ID WSRECEIVE id WSSERVICE zWsProdutos
Local lRet := .T.
Local jResponse := JsonObject():New()
Local cAliasWS := 'SB1'
//Se o id estiver vazio
If Empty(::id)
//SetRestFault(500, 'Falha ao consultar o registro') //caso queira usar esse comando, você não poderá usar outros retornos, como os abaixo
Self:setStatus(500)
jResponse['errorId'] := 'ID001'
jResponse['error'] := 'ID vazio'
jResponse['solution'] := 'Informe o ID'
Else
DbSelectArea(cAliasWS)
(cAliasWS)->(DbSetOrder(1))
//Se não encontrar o registro
If ! (cAliasWS)->(MsSeek(FWxFilial(cAliasWS) + ::id))
//SetRestFault(500, 'Falha ao consultar ID') //caso queira usar esse comando, você não poderá usar outros retornos, como os abaixo
Self:setStatus(500)
jResponse['errorId'] := 'ID002'
jResponse['error'] := 'ID não encontrado'
jResponse['solution'] := 'Código ID não encontrado na tabela ' + cAliasWS
Else
//Define o retorno
jResponse['cod'] := (cAliasWS)->B1_COD
jResponse['desc'] := (cAliasWS)->B1_DESC
jResponse['tipo'] := (cAliasWS)->B1_TIPO
jResponse['um'] := (cAliasWS)->B1_UM
jResponse['locpad'] := (cAliasWS)->B1_LOCPAD
EndIf
EndIf
//Define o retorno
Self:SetContentType('application/json')
Self:SetResponse(EncodeUTF8(jResponse:toJSON()))
Return lRet
/*/{Protheus.doc} WSMETHOD GET ALL
Busca todos os registros através de paginação
@author Atilio
@since 17/05/2025
@version 1.0
@type method
@param updated_at, Caractere, Data de alteração no formato string 'YYYY-MM-DD' (somente se tiver o campo USERLGA / USERGA na tabela)
@param limit, Numérico, Limite de registros que irá vir (por exemplo trazer apenas 100 registros)
@param page, Numérico, Número da página que irá buscar (se existir 1000 registros dividido por 100 terá 10 páginas de pesquisa)
@obs Codigo gerado automaticamente pelo Autumn Code Maker
Poderia ser usado o FWAdapterBaseV2(), mas em algumas versões antigas não existe essa funcionalidade
então a paginação foi feita manualmente
@see http://autumncodemaker.com
/*/
WSMETHOD GET ALL WSRECEIVE updated_at, limit, page WSSERVICE zWsProdutos
Local lRet := .T.
Local jResponse := JsonObject():New()
Local cQueryTab := ''
Local nTamanho := 10
Local nTotal := 0
Local nPags := 0
Local nPagina := 0
Local nAtual := 0
Local oRegistro
Local cAliasWS := 'SB1'
//Efetua a busca dos registros
cQueryTab := " SELECT " + CRLF
cQueryTab += " TAB.R_E_C_N_O_ AS TABREC " + CRLF
cQueryTab += " FROM " + CRLF
cQueryTab += " " + RetSQLName(cAliasWS) + " TAB " + CRLF
cQueryTab += " WHERE " + CRLF
cQueryTab += " TAB.D_E_L_E_T_ = '' " + CRLF
//Abaixo esta sendo feito o filtro com o campo de log de alteração (LGA), porém desde Maio de 2023, pode apresentar divergências
// então você pode substituir o campo 'B1_USERLGA' por S_T_A_M_P_, I_N_S_D_T_ ou outro campo de data da tabela
If ! Empty(::updated_at)
cQueryTab += " AND ((CASE WHEN SUBSTRING(B1_USERLGA, 03, 1) != ' ' THEN " + CRLF
cQueryTab += " CONVERT(VARCHAR,DATEADD(DAY,((ASCII(SUBSTRING(B1_USERLGA,12,1)) - 50) * 100 + (ASCII(SUBSTRING(B1_USERLGA,16,1)) - 50)),'19960101'),112) " + CRLF
cQueryTab += " ELSE '' " + CRLF
cQueryTab += " END) >= '" + StrTran(::updated_at, '-', '') + "') " + CRLF
EndIf
cQueryTab += " ORDER BY " + CRLF
cQueryTab += " TABREC " + CRLF
TCQuery cQueryTab New Alias 'QRY_TAB'
//Se não encontrar registros
If QRY_TAB->(EoF())
//SetRestFault(500, 'Falha ao consultar registros') //caso queira usar esse comando, você não poderá usar outros retornos, como os abaixo
Self:setStatus(500)
jResponse['errorId'] := 'ALL003'
jResponse['error'] := 'Registro(s) não encontrado(s)'
jResponse['solution'] := 'A consulta de registros não retornou nenhuma informação'
Else
jResponse['objects'] := {}
//Conta o total de registros
Count To nTotal
QRY_TAB->(DbGoTop())
//O tamanho do retorno, será o limit, se ele estiver definido
If ! Empty(::limit)
nTamanho := ::limit
EndIf
//Pegando total de páginas
nPags := NoRound(nTotal / nTamanho, 0)
nPags += Iif(nTotal % nTamanho != 0, 1, 0)
//Se vier página
If ! Empty(::page)
nPagina := ::page
EndIf
//Se a página vier zerada ou negativa ou for maior que o máximo, será 1
If nPagina <= 0 .Or. nPagina > nPags
nPagina := 1
EndIf
//Se a página for diferente de 1, pula os registros
If nPagina != 1
QRY_TAB->(DbSkip((nPagina-1) * nTamanho))
EndIf
//Adiciona os dados para a meta
jJsonMeta := JsonObject():New()
jJsonMeta['total'] := nTotal
jJsonMeta['current_page'] := nPagina
jJsonMeta['total_page'] := nPags
jJsonMeta['total_items'] := nTamanho
jResponse['meta'] := jJsonMeta
//Percorre os registros
While ! QRY_TAB->(EoF())
nAtual++
//Se ultrapassar o limite, encerra o laço
If nAtual > nTamanho
Exit
EndIf
//Posiciona o registro e adiciona no retorno
DbSelectArea(cAliasWS)
(cAliasWS)->(DbGoTo(QRY_TAB->TABREC))
oRegistro := JsonObject():New()
oRegistro['cod'] := (cAliasWS)->B1_COD
oRegistro['desc'] := (cAliasWS)->B1_DESC
oRegistro['tipo'] := (cAliasWS)->B1_TIPO
oRegistro['um'] := (cAliasWS)->B1_UM
oRegistro['locpad'] := (cAliasWS)->B1_LOCPAD
aAdd(jResponse['objects'], oRegistro)
QRY_TAB->(DbSkip())
EndDo
EndIf
QRY_TAB->(DbCloseArea())
//Define o retorno
Self:SetContentType('application/json')
Self:SetResponse(EncodeUTF8(jResponse:toJSON()))
Return lRet
/*/{Protheus.doc} WSMETHOD POST NEW
Cria um novo registro na tabela
@author Atilio
@since 17/05/2025
@version 1.0
@type method
@obs Codigo gerado automaticamente pelo Autumn Code Maker
Abaixo um exemplo do JSON que deverá vir no body
* 1: Para campos do tipo Numérico, informe o valor sem usar as aspas
* 2: Para campos do tipo Data, informe uma string no padrão 'YYYY-MM-DD'
{
"cod": "conteudo",
"desc": "conteudo",
"tipo": "conteudo",
"um": "conteudo",
"locpad": "conteudo"
}
@see http://autumncodemaker.com
/*/
WSMETHOD POST NEW WSRECEIVE WSSERVICE zWsProdutos
Local lRet := .T.
Local aDados := {}
Local jJson := Nil
Local cJson := Self:GetContent()
Local cError := ''
Local nLinha := 0
Local cDirLog := '\x_logs\'
Local cArqLog := ''
Local cErrorLog := ''
Local aLogAuto := {}
Local nCampo := 0
Local jResponse := JsonObject():New()
Local cAliasWS := 'SB1'
Private lMsErroAuto := .F.
Private lMsHelpAuto := .T.
Private lAutoErrNoFile := .T.
//Se não existir a pasta de logs, cria
IF ! ExistDir(cDirLog)
MakeDir(cDirLog)
EndIF
//Definindo o conteúdo como JSON, e pegando o content e dando um parse para ver se a estrutura está ok
Self:SetContentType('application/json')
jJson := JsonObject():New()
cError := jJson:FromJson(cJson)
//Se tiver algum erro no Parse, encerra a execução
IF ! Empty(cError)
//SetRestFault(500, 'Falha ao obter JSON') //caso queira usar esse comando, você não poderá usar outros retornos, como os abaixo
Self:setStatus(500)
jResponse['errorId'] := 'NEW004'
jResponse['error'] := 'Parse do JSON'
jResponse['solution'] := 'Erro ao fazer o Parse do JSON'
Else
DbSelectArea(cAliasWS)
//Adiciona os dados do ExecAuto
aAdd(aDados, {'B1_COD', jJson:GetJsonObject('cod'), Nil})
aAdd(aDados, {'B1_DESC', jJson:GetJsonObject('desc'), Nil})
aAdd(aDados, {'B1_TIPO', jJson:GetJsonObject('tipo'), Nil})
aAdd(aDados, {'B1_UM', jJson:GetJsonObject('um'), Nil})
aAdd(aDados, {'B1_LOCPAD', jJson:GetJsonObject('locpad'), Nil})
//Percorre os dados do execauto
For nCampo := 1 To Len(aDados)
//Se o campo for data, retira os hifens e faz a conversão
If GetSX3Cache(aDados[nCampo][1], 'X3_TIPO') == 'D'
aDados[nCampo][2] := StrTran(aDados[nCampo][2], '-', '')
aDados[nCampo][2] := sToD(aDados[nCampo][2])
EndIf
Next
//Chama a inclusão automática
MsExecAuto({|x, y| MATA010(x, y)}, aDados, 3)
//Se houve erro, gera um arquivo de log dentro do diretório da protheus data
If lMsErroAuto
//Monta o texto do Error Log que será salvo
cErrorLog := ''
aLogAuto := GetAutoGrLog()
For nLinha := 1 To Len(aLogAuto)
cErrorLog += aLogAuto[nLinha] + CRLF
Next nLinha
//Grava o arquivo de log
cArqLog := 'zWsProdutos_New_' + dToS(Date()) + '_' + StrTran(Time(), ':', '-') + '.log'
MemoWrite(cDirLog + cArqLog, cErrorLog)
//Define o retorno para o WebService
//SetRestFault(500, cErrorLog) //caso queira usar esse comando, você não poderá usar outros retornos, como os abaixo
Self:setStatus(500)
jResponse['errorId'] := 'NEW005'
jResponse['error'] := 'Erro na inclusão do registro'
jResponse['solution'] := 'Nao foi possivel incluir o registro, foi gerado um arquivo de log em ' + cDirLog + cArqLog + ' '
lRet := .F.
//Senão, define o retorno
Else
jResponse['note'] := 'Registro incluido com sucesso'
EndIf
EndIf
//Define o retorno
Self:SetResponse(EncodeUTF8(jResponse:toJSON()))
Return lRet
Bom pessoal, por hoje é só.
Abraços e até a próxima.
Ótimo material
Obrigado.
Bom dia Fabricio, tudo joia?
Opa, obrigado pelo feedback e comentário.
Ficamos felizes que tenha gostado.
Tenha uma ótima e abençoada semana.
Um forte abraço.
Como posso fazer na versão 2410? no webapp
Bom dia João, tudo joia?
Nesse tutorial, ele já está rodando na 2410 usando WebApp.
Agora caso você tenha dúvida de como configurar o VSCode para rodar com o WebApp, nesse outro link é demonstrado: https://terminaldeinformacao.com/2025/04/09/instalar-o-vscode-criar-funcoes-e-testar-no-protheus-com-web-agent-e-web-app-ti-especial-0002/
Tenha um ótimo e abençoado fim de semana.
Um grande abraço.
Como faço para chamar esse fonte rest em outro fonte qualquer meu? Por exemplo, estou criando uma rotina para gerar etiquetas. Esse fonte é gerado um arquivo pdf e salvo em uma pasta temporária dentro do protheus_data. Precisaria, no final desse fonte que é criado a etiqueta, baixar ela. Tenho um fonte rest que faz isso e está funcionando para baixar o arquivo que preciso, mas queria chamar ele dentro desse fonte que cria a etiqueta. Nesse caso como que eu faria?
Bom dia Lucas, tudo joia?
Para acionar, você pode usar FWRest() dentro de uma User Function consumindo esse endpoint que você criou.
Ou uma outra forma, seria essa parte do PDF, você cria uma User Function genérica, que pode ser acionada tanto dentro do endpoint como dentro de outras funções.
Tenham uma ótima e abençoada sexta feira.
Um forte abraço.