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
1 2 3 4 5 |
#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:
1 2 3 4 5 |
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:
1 2 3 4 5 |
WSMETHOD GET WSSERVICE NOTASFISCAIS Local cNUMNOTADE := Self:NUMNOTADE Local cNUMNOTAATE := Self:NUMNOTAATE ... ::SetContentType("application/json") |
Exemplo de Montagem de Filtro
1 2 3 |
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
1 2 3 |
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.
1 2 3 |
oObjCom := Objetos():New(aDados) cJson := FWJsonSerialize(oObjCom) ::SetResponse(cJson) |
Código Completo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
#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.
Fernando Bueno
Atuando desde 2005 no mercado de tecnologia, desenvolvendo e implantando e sistemas gerenciais, sistemas e sites web e ecommerce.
Siga-me no Linked In