A integração entre sistemas é uma necessidade cada vez mais comum nas empresas que utilizam o ERP TOTVS Protheus. Neste artigo, vamos demonstrar, com um exemplo prático e completo, como criar um WebService RESTful em ADVPL para consultar pedidos de compras, com filtros flexíveis e retorno em JSON.

O Que Você Vai Aprender

  • Como estruturar um WebService REST no Protheus com WSRESTFUL
  • Como receber parâmetros via URL (GET)
  • Como montar dinamicamente uma query SQL com filtros
  • Como retornar os resultados em formato JSON
  • Como aplicar paginação e ordenação no resultado

Parâmetros Recebidos via URL

O serviço aceita diversos parâmetros GET como filtros:

Parâmetro Descrição
PRODUTO Código do produto
FORNECE Código do fornecedor
CONTATO Código do contato
EMISSAO Data de emissão
TES Código de TES
CLVL Faixa de cliente inicial
QUANT Faixa de cliente final
PAGINA Página atual (paginação)
ORDER Campo de ordenação
DIR Direção da ordenação
LIMIT Quantidade de registros
PAGINATION ON/OFF para controle
SEGKEY Chave de segurança

Criação da Classe RESTful

WSRESTFUL PEDIDODECOMPRAS DESCRIPTION "Serviço REST para manipulação de pedidos de compras"
WSDATA PRODUTO      As String
WSDATA FORNECE      As String
...
WSMETHOD GET DESCRIPTION "Retorna os cabeçalhos dos pedidos de compras com base nos filtros" WSSYNTAX "/PEDIDODECOMPRAS"
END WSRESTFUL

Validação de Segurança

Logo no início do método GET, o código valida uma chave de segurança:

If !u_FBSegApp(Self:SEGKEY)
    Return
EndIf

Montagem Dinâmica da Query

Os filtros recebidos via GET são utilizados para montar dinamicamente a cláusula WHERE da SQL:

If !Empty(Self:PRODUTO)
    cFiltro += "AND SC7.C7_PRODUTO = '" + Self:PRODUTO + "' " + BREAKLINE
EndIf

Consulta SQL com Paginação

A consulta principal utiliza JOIN entre SC7, SA2, SD1 e SF1. A paginação é feita com TOP (MSSQL) ou ROWNUM (Oracle):

SELECT C7_NUM, C7_CONTATO, ...
FROM SC7
INNER JOIN SA2 ON ...
...
WHERE SC7.D_E_L_E_T_ = ' '

Resposta em JSON

Os resultados são serializados e enviados como JSON:

oObjCom := Objetos():New(aDados)
cJson := FWJsonSerialize(oObjCom)
::SetResponse(cJson)

Exemplo de Requisição GET

GET /rest/PEDIDODECOMPRAS?FORNECE=000123&PAGINA=1&PAGINATION=ON&SEGKEY=XYZ123

Exemplo de Retorno JSON

[
  {
    "C7_NUM": "000123",
    "A2_NOME": "Fornecedor XYZ",
    "TOTAL_PRODUTO": 120.00,
    ...
  }
]

Código Completo

#INCLUDE "PROTHEUS.CH"
#INCLUDE "RWMAKE.CH"   
#INCLUDE "TBICONN.CH" 
#INCLUDE "COLORS.CH"
#INCLUDE "RPTDEF.CH"
#INCLUDE "FWPrintSetup.ch"
#Include 'parmtype.ch'
#Include "RestFul.CH"
#Include "TopConn.CH"
#INCLUDE "FILEIO.ch"
#INCLUDE "PRTOPDEF.CH"


#Define BREAKLINE		Chr(13)+Chr(10)

/*/{Protheus.doc} FBTIT01
Ensinando Rest em ADVPL
@author Fernando Bueno
@since 13/04/2017
@version undefined
@type function
/*/
User Function FBCOMP01()	
Return

/*/{Protheus.doc} PEDIDODECOMPRAS
Definição da estrutura do webservice
@author Fernando Bueno
@since 13/04/2017
@type class
/*/
WSRESTFUL PEDIDODECOMPRAS DESCRIPTION "Serviço REST para manipulação de notas fiscais"
WSDATA PRODUTO     	As String //String que vamos receber via URL
WSDATA SEGKEY   	As String //String que vamos receber via URL
WSDATA TIPO   		As String //String que vamos receber via URL
WSDATA FORNECE   	As String //String que vamos receber via URL
WSDATA CONTATO   	As String //String que vamos receber via URL
WSDATA QUANT   		As String //String que vamos receber via URL
WSDATA EMISSAO   	As String //String que vamos receber via URL
WSDATA QUJE   		As String //String que vamos receber via URL
WSDATA TES   		As String //String que vamos receber via URL
WSDATA CLVL   		As String //String que vamos receber via URL
WSDATA PAGINA   	As String //String que vamos receber via URL
WSDATA DIR   		As String //String que vamos receber via URL
WSDATA ORDER   		As String //String que vamos receber via URL
WSDATA NUMLIQ   	As String //String que vamos receber via URL
WSDATA PAGINATION 	As String //String que vamos receber via URL
WSDATA LIMIT   		As String //String que vamos receber via URL
 
WSMETHOD GET DESCRIPTION "Retorna os cabeçalhos dos pedidos de venda informado na URL" WSSYNTAX "/PEDIDODECOMPRAS || /PEDIDODECOMPRAS/{TIPO}{FORNECE}{CONTATO}{QUANT}{EMISSAO}{QUJE}{TES}{CLVL}{PAGINA}{DIR}{NUMLIQ}" //Disponibilizamos um método do tipo GET
 
END WSRESTFUL

/*/{Protheus.doc} GET
Processa as informações e retorna o json
@author Fernando Bueno
@since 13/04/2017
@version undefined
@type Method
/*/
WSMETHOD GET WSSERVICE PEDIDODECOMPRAS

Local cPROD         := Self:PRODUTO
Local cFORNECE   	:= Self:FORNECE
Local cCONTATO   	:= Self:CONTATO
Local cEMISSAO   	:= Self:EMISSAO
Local cQUJE   		:= Self:QUJE
Local cTES   		:= Self:TES
Local cCLVL   		:= Self:CLVL
Local cPAGINA 		:= Self:PAGINA
Local cDIR 		:= Self:DIR
Local cORDER 		:= Self:ORDER
Local cPag := "10"
Local cPAGINATION 	:= Self:PAGINATION 

Local aArea		:= GetArea()
Local cJson		:= ""
Local cQuery    := ""
Local aDados    := {}
Local nTot		:= 0
Local cBD := AllTrim(GetMV("MV_FBWEB08"))

Private cLIMIT 		:= Self:LIMIT

If !u_FBSegApp(Self:SEGKEY)
	Return
EndIf

// define o tipo de retorno do método
::SetContentType("application/json")

cFiltro := ""

If !Empty(cPROD)
	cFiltro += "AND SC7.C7_PRODUTO = '" + cPROD + "' " + BREAKLINE
EndIf
If !Empty(cFORNECE)
	cFiltro += "AND SC7.C7_FORNECE = '" + cFORNECE + "' " + BREAKLINE
EndIf
If !Empty(cCONTATO)
	cFiltro += "AND SC7.C7_CONTATO = '" + cCONTATO + "' " + BREAKLINE
EndIf
If !Empty(cEMISSAO)
	cFiltro += "AND SC7.C7_EMISSAO = '" + cEMISSAO + "' " + BREAKLINE
EndIf
If !Empty(cQUJE)
	cFiltro += "AND SC7.C7_QUJE = '" + cQUJE + "' " + BREAKLINE
EndIf
If !Empty(cTES)
	cFiltro += "AND SC7.C7_TES = '" + cTES + "' " + BREAKLINE
EndIf
If !Empty(cCLVL)
	cFiltro += "AND SC7.C7_CLVL = '" + cCLVL + "' " + BREAKLINE
EndIf

If Self:TIPO == "N"

		//conout("hdsjkadhshadkjshdhkashkdshakdhsakhkdshaskdhkashdksahdkashdkashhdk")

		if ! Empty(Self:FORNECE) // Gabriel Soares 04/03/2019 - Colocado teste para Fornecedor
			cFiltro += u_FBFILTER("PEDIDODECOMPRAS", Self:FORNECE)
		endif

		conout("PEDIDODECOMPRAS:clientes1")
		if ! Empty(Self:CONTATO) /* .AND. !Empty(Self:LOJA)*/ // Gabriel Soares 04/03/2019 - Colocado teste para Contato
			conout("PEDIDODECOMPRAS:clientes2")
			cFiltro += u_FBFILTER("PEDIDODECOMPRAS", Self:CONTATO, "CC", Self:CONTATOS)
		endif

	EndIf	
	
	if ! Empty(Self:CLVL) // Gabriel Soares 04/03/2019 - Colocado teste para Cliente Nivel
		cFiltro += "AND SC7.C7_CLIENTE >= '" + Self:CLVL + "' " + BREAKLINE
	endif
	
	if ! Empty(Self:QUANT) // Gabriel Soares 04/03/2019 - Colocado teste para Quantidade
		cFiltro += "AND SC7.C7_CLIENTE <= '" + Self:QUANT + "' " + BREAKLINE
	endif

	If !Empty(cEMISSAO)
		cFiltro += "AND SC7.C7_TIPO = '" + cEMISSAO + "' " + BREAKLINE
		If cEMISSAO == "NCC"
			cFiltro += " AND SC7.C7_SALDO > 0 " + BREAKLINE
		EndIf
	EndIf

	//--PEDIDODECOMPRAS
	//--num_nota, ser_nota, num_carga, cliente, total_peso, total_merc, status, motivo, sequencia
	cQuery := " SELECT COUNT(DISTINCT(C7_NUM)) TOTALREG " + BREAKLINE 
	cQuery += "  FROM " + RetSQLName("SC7") + " SC7 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " " + BREAKLINE 
	cQuery += " INNER JOIN " + RetSQLName("SA2") + " A2 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C7_FORNECE = A2_COD AND C7_LOJA = A2_LOJA AND A2.D_E_L_E_T_ = ' ') " + BREAKLINE
	cQuery += " INNER JOIN " + RetSQLName("SD1") + " D1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON (C7_NUM = D1_PEDIDO AND D1.D_E_L_E_T_ = ' ') " + BREAKLINE
	cQuery += " INNER JOIN " + RetSQLName("SF1") + " F1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + "	ON (D1_FILIAL = F1_FILIAL AND D1_DOC = F1_DOC AND D1_SERIE = F1_SERIE AND D1_FORNECE = F1_FORNECE AND F1.D_E_L_E_T_ = ' ') " + BREAKLINE
	cQuery += " WHERE SC7.D_E_L_E_T_ = ' ' " + BREAKLINE
	cQuery += " AND 1 = 1 "

	cQuery += cFiltro

	//cQuery += "GROUP BY C7_NUM"	

	//cQuery += "GROUP BY C7_FILIAL " + BREAKLINE
	
	u_FBCONS("CONTADOR PEDIDO = " + cQuery)

	//Executando consulta e setando o total da régua
	TCQuery cQuery New Alias "QRY_CNT"
	
	If !QRY_CNT->(Eof())
		nTot := QRY_CNT->TOTALREG
	EndIf
	
	QRY_CNT->(dbCloseArea())

	If !Empty(cPAGINA)
	
		If cPAGINA == '1'
			cPAGINA := ''
		Else	
			cLim := cValToChar(Val(cPag)*(Val(cPAGINA)-1))
			
			If Val(cLim) > nTot
				cLim := cValToChar(Val(cLim) - Val(cPag)) 
			EndIf
		EndIf	
		
	Else
		cLim := cPag	
	EndIf	

	conout("cPAGINATION:" + cPAGINATION)

	cLimitCond := IIF(Empty(cLIMIT), "", "TOP " + cLIMIT)

	//--PEDIDODECOMPRAS
	//--num_nota, ser_nota, num_carga, cliente, total_peso, total_merc, status, motivo, sequencia
	cQuery := "SELECT C7_NUM, C7_CONTATO, C7_TIPO, MAX(C7_EMISSAO) C7_EMISSAO, A2_COD, A2_LOJA, A2_NOME, " + CRLF
	cQuery += "A2_TPESSOA, A2_CGC, A2_END, A2_BAIRRO, A2_MUN, A2_EST, " + CRLF
	cQuery += "A2_TEL, A2_EMAIL, SUM(D1_QUANT) TOTAL_PRODUTO, MAX(D1_EMISSAO) D1_EMISSAO, F1_STATUS " + CRLF
	cQuery += "FROM " + RetSQLName("SC7") + " SC7 WITH (NOLOCK) " + CRLF
	cQuery += "INNER JOIN " + RetSQLName("SA2") + " A2 WITH (NOLOCK) ON (C7_FORNECE = A2_COD AND C7_LOJA = A2_LOJA AND A2.D_E_L_E_T_ = ' ') " + CRLF
	cQuery += "INNER JOIN " + RetSQLName("SD1") + " D1 WITH (NOLOCK) ON (C7_FILIAL = D1_FILIAL AND C7_NUM = D1_PEDIDO AND D1.D_E_L_E_T_ = ' ') " + CRLF
	cQuery += "INNER JOIN " + RetSQLName("SF1") + " F1 WITH (NOLOCK) ON (D1_FILIAL = F1_FILIAL AND D1_DOC = F1_DOC AND D1_SERIE = F1_SERIE AND D1_FORNECE = F1_FORNECE AND F1.D_E_L_E_T_ = ' ') " + CRLF
	cQuery += "WHERE SC7.D_E_L_E_T_ = ' ' " + CRLF

	If cPAGINATION <> 'OFF' .AND. !Empty(cPAGINA) .AND. cBD == 'MSSQL'
		cQuery += "AND SC7.R_E_C_N_O_ NOT IN (SELECT TOP " + cLim + " SC7.R_E_C_N_O_ FROM " + RetSQLName("SC7") + " SC7 " + BREAKLINE
		cQuery += "WHERE SC7.D_E_L_E_T_ = ' ' " + cFiltro + " " + u_FBORDERBY("PEDIDODECOMPRAS", cORDER, cDIR) + ") " + BREAKLINE
	EndIf    

	cQuery += cFiltro + BREAKLINE

	cQuery += "GROUP BY C7_NUM, C7_CONTATO, C7_TIPO, " + CRLF
	cQuery += "A2_COD, A2_LOJA, A2_NOME, A2_TPESSOA, A2_CGC, A2_END, A2_BAIRRO, A2_MUN, A2_EST, " + CRLF
	cQuery += "A2_TEL, A2_EMAIL, F1_STATUS " + CRLF

	cQuery += "ORDER BY C7_NUM DESC " + CRLF
	
	cOrderBy := u_FBORDERBY("PEDIDODECOMPRAS", cORDER, cDIR)
	
	cQuery += cOrderBy
	
	If cBD == 'ORACLE' .AND. cPAGINATION <> 'OFF'

		cQueryORA := " SELECT ROWNUM + (" + cPag + " * (PAGINA - 1)) AS LINNUM " + BREAKLINE
		cQueryORA += "	      ,B.* " + BREAKLINE
		cQueryORA += "	  FROM (SELECT COUNT(*) OVER() AS TOTALREG " + BREAKLINE
		cQueryORA += "	              ,TRUNC((ROW_NUMBER() OVER(" + cOrderBy + ") - 1) / " + cPag + ") + 1 AS PAGINA " + BREAKLINE
		cQueryORA += "	              ,TMP.* " + BREAKLINE
		cQueryORA += "	          FROM ( " + BREAKLINE
		
		cQueryORA += cQuery
		
		cQueryORA += ") TMP " + BREAKLINE
	    cQueryORA += ") B " + BREAKLINE
	    
	    If !Empty(cPAGINA)
	    	cQueryORA += " WHERE PAGINA = " + cPAGINA + " " + BREAKLINE
	    EndIf	
	    
	    cQuery := cQueryORA
	
	EndIf

CONOUT("PEDIDODECOMPRAS cQuery principal: " 	+ cQuery)

If Select("QRY_AUX") > 0
	QRY_AUX->(dbCloseArea())
EndIf
	
//Executando consulta e setando o total da régua
TCQuery cQuery New Alias "QRY_AUX"
Count to nTotal

QRY_AUX->(DbGoTop())

While ! QRY_AUX->(Eof())
	
	aAdd(aDados, { 	QRY_AUX->C7_NUM,;  
					QRY_AUX->C7_CONTATO,;  
					QRY_AUX->C7_TIPO,;  
					QRY_AUX->C7_EMISSAO,;  
					QRY_AUX->A2_COD,;  
					QRY_AUX->A2_LOJA,;  
					QRY_AUX->A2_NOME,;  
					QRY_AUX->A2_TPESSOA,;  
					QRY_AUX->A2_CGC,;  
					QRY_AUX->A2_END,;  
					QRY_AUX->A2_BAIRRO,;  
					QRY_AUX->A2_MUN,;  
					QRY_AUX->A2_EST,;  
					QRY_AUX->A2_TEL,;  
					QRY_AUX->A2_EMAIL,;  
					QRY_AUX->TOTAL_PRODUTO,; 
					QRY_AUX->D1_EMISSAO,;
					QRY_AUX->F1_STATUS,;
					cValToChar(nTot);
					})
					
	QRY_AUX->(dbSkip())	

EndDo

QRY_AUX->(dbCloseArea())

oObjCom := Objetos():New(aDados) //Cria um objeto da classe produtos para fazer a serialização na função FWJSONSerialize
//::SetContentType("application/json")
// --> Transforma o objeto de produtos em uma string json
//cJson := "receive([" + FWJsonSerialize(oObjCom) + "])"
cJson := FWJsonSerialize(oObjCom)
//u_FBCONS(cJson)
// --> Envia o JSON Gerado para a aplicação Client
::SetResponse(cJson)
RestArea(aArea)

Return(.T.)

 

Conclusão

Criar um WebService REST em ADVPL para consulta de pedidos de compras no Protheus é totalmente viável e extremamente útil para integrações externas, dashboards, sistemas parceiros ou análise de dados.

Essa abordagem modular, segura e escalável pode ser reaproveitada para outros cadastros como pedidos de vendas, clientes, produtos e muito mais.

Quer criar um framework REST para seu Protheus? Posso te ajudar a desenvolver endpoints reutilizáveis e seguros. Fale comigo!