Como criar um WebService REST para Notas Fiscais no Protheus usando ADVPL

Aprenda como estruturar um WebService RESTful em ADVPL para consultar notas fiscais no Protheus, utilizando boas práticas de performance, segurança e organização de código.

Introdução

Com a crescente demanda por integrações em tempo real com ERPs, criar WebServices RESTful dentro do TOTVS Protheus se tornou uma habilidade essencial. Neste artigo, vamos mostrar como funciona uma implementação prática para consulta de notas fiscais via REST, utilizando recursos da linguagem ADVPL e do framework do Protheus.

Definição da Função Inicial

#INCLUDE "PROTHEUS.CH"
#INCLUDE "TBICONN.CH"
...
User Function FBNOT01()
Return

Essa é a função de inicialização que pode ser usada para testes ou registros no dicionário de dados.

Definindo o WebService: NOTASFISCAIS

O bloco abaixo cria a estrutura do WebService com os parâmetros que serão recebidos via URL:

WSRESTFUL NOTASFISCAIS DESCRIPTION "Serviço REST para manipulação de notas fiscais"
WSDATA SEGKEY   As String
WSDATA VENDEDOR As String
...
WSMETHOD GET DESCRIPTION "Retorna os cabeçalhos das notas fiscais"

Implementando o Método GET

Dentro do método GET, é feita a leitura dos parâmetros recebidos, montagem do filtro SQL e execução da query paginada. Veja um trecho do início da função:

WSMETHOD GET WSSERVICE NOTASFISCAIS
Local cNUMNOTADE := Self:NUMNOTADE
Local cNUMNOTAATE := Self:NUMNOTAATE
...
::SetContentType("application/json")

Exemplo de Montagem de Filtro

If !Empty(cNUMNOTADE) .AND. !Empty(cNUMNOTAATE)
  cFiltro += "AND SF2.F2_DOC BETWEEN '" + cNUMNOTADE + "' AND '" + cNUMNOTAATE + "' " + BREAKLINE
EndIf

Isso permite consultas flexíveis, com filtros por número de nota, série, emissão, cliente, entre outros.

Consulta SQL Otimizada com Join

cQuery := "SELECT C5_FILIAL, F2_DOC, F2_SERIE, C5_NUM, ..."
cQuery += "FROM " + RetSQLName("SF2") + " SF2 " + ...
cQuery += "INNER JOIN " + RetSQLName("SC5") + " C5 ..."

O cQuery é montado dinamicamente, considerando banco Oracle ou MSSQL e utilizando o recurso de paginação.

Serialização e Retorno JSON

Ao final, os dados consultados são transformados em JSON para envio ao cliente.

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

Código Completo

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

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

//5152
//6152

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

/*/{Protheus.doc} NOTASFISCAIS
Definição da estrutura do webservice
@author Fernando Bueno
@since 13/04/2017
@type class
/*/
WSRESTFUL NOTASFISCAIS DESCRIPTION "Serviço REST para manipulação de notas fiscais"
WSDATA SEGKEY  			As String //String que vamos receber via URL
WSDATA VENDEDOR  		As String //String que vamos receber via URL  // Gabriel Soares 04/03/2019 - Colocado parametro para Vendedor
WSDATA CLIDE  			As String //String que vamos receber via URL  // Gabriel Soares 04/03/2019 - Colocado parametro Cliende De
WSDATA CLIATE  			As String //String que vamos receber via URL  // Gabriel Soares 04/03/2019 - Colocado parametro Cliende Até
WSDATA CLIENTE  		As String //String que vamos receber via URL
WSDATA LOJA			  	As String //String que vamos receber via URL
WSDATA CLIENTES  		As String //String que vamos receber via URL
WSDATA NUMNOTADE  		As String //String que vamos receber via URL
WSDATA NUMNOTAATE  		As String //String que vamos receber via URL
WSDATA SERIEDE  		As String //String que vamos receber via URL
WSDATA SERIEATE  		As String //String que vamos receber via URL
WSDATA EMISSAODE  		As String //String que vamos receber via URL
WSDATA EMISSAOATE  		As String //String que vamos receber via URL
WSDATA ULTIMOS  		As String //String que vamos receber via URL
WSDATA PAGINA  			As String //String que vamos receber via URL
WSDATA ORDER 			As String //String que vamos receber via URL
WSDATA DIR  			As String //String que vamos receber via URL
WSDATA PAGINATION		As String //String que vamos receber via URL
 
WSMETHOD GET DESCRIPTION "Retorna os cabeçalhos dos pedidos de venda informado na URL" WSSYNTAX "/NOTASFISCAIS || /NOTASFISCAIS/{NUMNOTADE}{NUMNOTAATE}{SERIEDE}{SERIEATE}{EMISSAODE}{EMISSAOATE}{ULTIMOS}{PAGINA}" //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
@param oSelf, object, Objeto contendo dados da requisição efetuada pelo cliente, tais como:
   - Parâmetros querystring (parâmetros informado via URL)
   - Objeto JSON caso o requisição seja efetuada via Request Post
   - Header da requisição
   - entre outras ...
@type Method
/*/
WSMETHOD GET WSSERVICE NOTASFISCAIS
//--> Recuperamos o produto informado via URL 
//--> Podemos fazer dessa forma ou utilizando o atributo ::aUrlParms, que é um array com os parâmetros recebidos via URL (QueryString)
Local cNUMNOTADE 	:= Self:NUMNOTADE
Local cNUMNOTAATE 	:= Self:NUMNOTAATE
Local cSERIEDE 		:= Self:SERIEDE
Local cSERIEATE 	:= Self:SERIEATE
Local cEMISSAODE 	:= Self:EMISSAODE
Local cEMISSAOATE 	:= Self:EMISSAOATE
Local cULTIMOS 		:= Self:ULTIMOS
Local cPAGINA 		:= Self:PAGINA
Local cORDER 		:= Self:ORDER
Local cDIR 			:= Self:DIR
Local cPAGINATION 	:= Self:PAGINATION
Local cPag := "10"

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

	If !u_FBSegApp(Self:SEGKEY)
		Return
	EndIf
	
	// define o tipo de retorno do método
	::SetContentType("application/json")
	
	cFiltro := "" 
	
	If !Empty(cNUMNOTADE) .AND. !Empty(cNUMNOTAATE)
		cFiltro += "AND SF2.F2_DOC BETWEEN '" + cNUMNOTADE + "' AND '" + cNUMNOTAATE + "' " + BREAKLINE
	EndIf
	If !Empty(cSERIEDE) .AND. !Empty(cSERIEATE)
		cFiltro += "AND SF2.F2_SERIE BETWEEN '" + cSERIEDE + "' AND '" + cSERIEATE + "' " + BREAKLINE
	EndIf
	If !Empty(cULTIMOS)
		cFiltro += "AND SF2.F2_EMISSAO >= '" + DTOS((DATE() - Val(cULTIMOS))) + "' " + BREAKLINE  
	ElseIf !Empty(cEMISSAODE) .AND. !Empty(cEMISSAOATE)
		cFiltro += "AND SF2.F2_EMISSAO BETWEEN '" + DTOS(CTOD(cEMISSAODE)) + "' AND '" + DTOS(CTOD(cEMISSAOATE)) + "' " + BREAKLINE
	EndIf
	
	if ! Empty(Self:VENDEDOR) // Gabriel Soares 04/03/2019 - Colocado teste para Vendedor
		cFiltro += u_FBFILTER("NOTASFISCAIS", Self:VENDEDOR)
	endif
	
	if ! Empty(Self:CLIENTE) //.AND. ! Empty(Self:LOJA) // Gabriel Soares 04/03/2019 - Colocado teste para Cliente
		cFiltro += u_FBFILTER("NOTASFISCAIS", Self:CLIENTE, "CC", Self:CLIENTES)
	endif
	
	if ! Empty(Self:CLIDE) // Gabriel Soares 04/03/2019 - Colocado teste para Cliente De
		cFiltro += "AND SF2.F2_CLIENTE >= '" + Self:CLIDE + "' " + BREAKLINE
	endif
	
	if ! Empty(Self:CLIATE) // Gabriel Soares 04/03/2019 - Colocado teste para Cliente Até
		cFiltro += "AND SF2.F2_CLIENTE <= '" + Self:CLIATE + "' " + BREAKLINE
	endif

	If cPAGINATION <> 'OFF'
		//--NOTASFISCAIS
		//--num_nota, ser_nota, num_carga, cliente, total_peso, total_merc, status, motivo, sequencia
		cQuery := "SELECT COUNT(C5_FILIAL) TOTALREG " + BREAKLINE 
		cQuery += "		FROM " + RetSQLName("SF2") + " SF2 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " " + BREAKLINE 
		cQuery += "		INNER JOIN " + RetSQLName("SC5") + " C5 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( F2_FILIAL = C5_FILIAL AND F2_DOC = C5_NOTA AND F2_SERIE = C5_SERIE AND SF2.D_E_L_E_T_ = ' ' ) " + BREAKLINE
		cQuery += "		INNER JOIN " + RetSQLName("SC6") + " C6 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C5_FILIAL = C6_FILIAL AND C5_NUM = C6_NUM AND C6.D_E_L_E_T_ = ' ') " + BREAKLINE
		cQuery += "		INNER JOIN " + RetSQLName("SB1") + " B1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C6_PRODUTO = B1_COD AND B1.D_E_L_E_T_ = ' ') " + BREAKLINE				
		cQuery += "		INNER JOIN " + RetSQLName("SA1") + " A1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C5_CLIENTE = A1_COD AND C5_LOJACLI = A1_LOJA AND A1.D_E_L_E_T_ = ' ') " + BREAKLINE
		cQuery += "WHERE C5.D_E_L_E_T_ = ' '  " + BREAKLINE
		cQuery += cFiltro
		cQuery += "GROUP BY C5_FILIAL " + BREAKLINE
		
		u_FBCONS("[FBWEB][NOTASFISCAIS] - " + cQuery)
		
		//Executando consulta e setando o total da régua
		TCQuery cQuery New Alias "QRY_CNT"
		
		If !QRY_CNT->(Eof())
			nTot := QRY_CNT->TOTALREG
			u_FBCONS("NOTASFISCAIS nTot: " + cValToChar(nTot))
		EndIf
		
		QRY_CNT->(dbCloseArea())
		
	EndIf

	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	

	//--NOTASFISCAIS
	//--num_nota, ser_nota, num_carga, cliente, total_peso, total_merc, status, motivo, sequencia
	cQuery := "SELECT " + IIF(cBD == 'MSSQL', "TOP " + cPag, "") + " C5_FILIAL, F2_DOC, F2_SERIE, C5_NUM, F2_EMISSAO, F2_FIMP, SUM(C6_VALOR) C6_VALOR, SUM(B1_PESO) B1_PESO, " + BREAKLINE
	cQuery += "		A1_COD, A1_LOJA, A1_NOME, A1_PESSOA, A1_CGC, A1_END, A1_BAIRRO, A1_MUN, A1_EST, A1_TEL, A1_EMAIL, F2_CHVNFE, C6_NUM " + BREAKLINE 
	cQuery += "		FROM " + RetSQLName("SF2") + " SF2 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " " + BREAKLINE 
	cQuery += "		INNER JOIN " + RetSQLName("SC5") + " C5 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( F2_FILIAL = C5_FILIAL AND F2_DOC = C5_NOTA AND F2_SERIE = C5_SERIE AND SF2.D_E_L_E_T_ = ' ' ) " + BREAKLINE
	cQuery += "		INNER JOIN " + RetSQLName("SC6") + " C6 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C5_FILIAL = C6_FILIAL AND C5_NUM = C6_NUM AND C6.D_E_L_E_T_ = ' ') " + BREAKLINE
	cQuery += "		INNER JOIN " + RetSQLName("SB1") + " B1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C6_PRODUTO = B1_COD AND B1.D_E_L_E_T_ = ' ') " + BREAKLINE				
	cQuery += "		INNER JOIN " + RetSQLName("SA1") + " A1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C5_CLIENTE = A1_COD AND C5_LOJACLI = A1_LOJA AND A1.D_E_L_E_T_ = ' ') " + BREAKLINE
	cQuery += "WHERE C5.D_E_L_E_T_ = ' '  " + BREAKLINE
	If !Empty(cPAGINA) .AND. cBD == 'MSSQL'
		cQuery += "AND SF2.R_E_C_N_O_ NOT IN (SELECT TOP " + cLim + " F2.R_E_C_N_O_ FROM " + RetSQLName("SF2") + " F2 WHERE F2.D_E_L_E_T_ = ' ' " + cFiltro + " " + u_FBORDERBY("NOTASFISCAIS", cORDER, cDIR) + ") " + BREAKLINE
	EndIf	
	
	cQuery += cFiltro
	
	cQuery += "GROUP BY C5_FILIAL, F2_DOC, F2_SERIE, C5_NUM, F2_EMISSAO, F2_FIMP, A1_COD, A1_LOJA, A1_NOME, A1_PESSOA, A1_CGC, A1_END, A1_BAIRRO, A1_MUN, A1_EST, A1_TEL, A1_EMAIL, F2_CHVNFE, C6_NUM " + BREAKLINE
	
	cOrderBy := u_FBORDERBY("NOTASFISCAIS", cORDER, cDIR) 
	
	cQuery += cOrderBy

	If cBD == 'ORACLE'
	
		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

	u_FBCONS("[FBWEB][NOTASFISCAIS] - " + cQuery)
	
	//Executando consulta e setando o total da régua
	TCQuery cQuery New Alias "QRY_AUX"
	Count to nTotal
	
	QRY_AUX->(DbGoTop())

	u_FBCONS("NOTASFISCAIS nTot2: " + cValToChar(nTot))
	
	While ! QRY_AUX->(Eof())
	
		aAdd(aDados, {	QRY_AUX->C5_FILIAL,; 
						QRY_AUX->F2_DOC,; 
						QRY_AUX->F2_SERIE,; 
						QRY_AUX->C5_NUM,; 
						QRY_AUX->F2_EMISSAO,; 
						QRY_AUX->C6_VALOR,; 
						QRY_AUX->B1_PESO,; 
						QRY_AUX->A1_COD,; 
						QRY_AUX->A1_LOJA,; 
						QRY_AUX->A1_NOME,; 
						QRY_AUX->F2_CHVNFE,; 
						cValToChar(nTot),;
						QRY_AUX->F2_FIMP,;
						QRY_AUX->A1_PESSOA,;
						QRY_AUX->A1_CGC,;
						QRY_AUX->A1_END,;
						QRY_AUX->A1_BAIRRO,;
						QRY_AUX->A1_MUN,;
						QRY_AUX->A1_EST,;
						QRY_AUX->A1_TEL,;
						QRY_AUX->A1_EMAIL;
						})
						
		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

Esse exemplo prático mostra como estruturar um serviço REST no Protheus para consulta de notas fiscais com segurança, performance e manutenção facilitada. Ele é ideal para integrações com ERPs, BI, ferramentas de automação ou portais de clientes.

Precisa de ajuda com integração REST no Protheus? Fale comigo.