Nível 4 · Produção · Módulo 11
Integrações
Duração estimada: 2h30 a 3h30
Você vai sair sabendo
- →Expor as visitas para outros sistemas pela API REST do iDempiere
- →Criar e completar uma visita por HTTP, com as mesmas regras da tela
- →Avisar outro sistema quando uma visita é concluída
- →Fazer chamadas externas fora da transação, sem travar o usuário
- →Escolher entre entrada (API) e saída (notificação) em cada integração
Checkpoint: 0 de 6
Antes de começar
- Módulo 09 concluído: a visita é um documento com o botão Document Action.
curlenc(netcat) no terminal. Oncfaz o papel do "outro sistema" que recebe a notificação.- Acesso à internet no servidor de desenvolvimento, para o Extension Manager baixar o plugin REST.
O que você vai construir
A GardenWorld tem um aplicativo de campo que os técnicos usam no celular, e um sistema de atendimento que precisa saber quando uma visita termina. Duas direções de integração:
- Entrada: o aplicativo cria e consulta visitas no iDempiere, pela API REST.
- Saída: quando uma visita é concluída, o iDempiere avisa o sistema de atendimento, com um POST para uma URL configurável.
| Direção | Quem inicia | Ferramenta | Quando usar |
|---|---|---|---|
| Entrada | O outro sistema | API REST (plugin) | O outro sistema precisa ler ou gravar no iDempiere |
| Saída | O iDempiere | Event handler + HTTP | O outro sistema precisa saber de algo assim que acontece |
Parte 1: a API REST
1. Instale o plugin REST
O iDempiere não traz API REST no core. Ela vem de um plugin mantido pela comunidade, bxservice/idempiere-rest, com documentação em bxservice.github.io/idempiere-rest-docs. Ele segue o padrão OData para filtros e consultas.
O iDempiere 14 instala esse tipo de plugin sem compilar nada, pelo Extension Manager: uma loja de extensões revisadas pela comunidade, publicada no repositório idempiere-extension-repository. A configuração server.product do Eclipse já aponta para ele.
- Entre como System e abra o formulário Extension Management.
- Localize iDempiere REST API e clique para instalar. O iDempiere baixa o jar, instala e inicia o plugin.
- Confira no console do Eclipse:
ss com.trekglobal.idempiere.rest.apideve mostrarACTIVE. A janela Extension Registry lista o que está instalado.
2. Autentique
Toda chamada leva um token. Pegue um para o GardenAdmin, já escolhendo empresa, perfil e organização:
curl -s -X POST http://localhost:8080/api/v1/auth/tokens \
-H "Content-Type: application/json" \
-d '{
"userName": "GardenAdmin",
"password": "GardenAdmin",
"parameters": { "clientId": 11, "roleId": 102, "organizationId": 11, "warehouseId": 103 }
}'
11 é a GardenWorld, 102 o perfil GardenWorld Admin, 11 a organização HQ e 103 o armazém HQ Warehouse, os IDs do banco de demonstração. A resposta traz um token, válido por uma hora. Guarde:
TOKEN="cole o token aqui"
3. Consulte visitas
O recurso models dá acesso a qualquer tabela pelo nome, respeitando o acesso do perfil:
curl -s -G http://localhost:8080/api/v1/models/EDU_Visita \
--data-urlencode '$filter=C_BPartner_ID eq 118' \
--data-urlencode '$select=DocumentNo,EDU_DataVisita,DocStatus' \
-H "Authorization: Bearer $TOKEN"
118 é o Joe Block. O -G com --data-urlencode monta a URL com os espaços codificados. A resposta é um JSON com as visitas dele. Os parâmetros com $ seguem o OData:
| Parâmetro | Exemplo | Faz |
|---|---|---|
$filter | Processed eq false | Filtra (eq, ne, gt, lt, and, or, in) |
$select | DocumentNo,DocStatus | Escolhe as colunas |
$orderby | EDU_DataVisita desc | Ordena |
$top / $skip | 10 / 20 | Pagina |
$expand | EDU_VisitaLinha | Traz os registros filhos junto |
4. Crie e complete uma visita
curl -s -X POST http://localhost:8080/api/v1/models/EDU_Visita \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"C_BPartner_ID": 118,
"SalesRep_ID": 101,
"EDU_DataVisita": "2026-10-15",
"EDU_TipoVisita": "MA",
"EDU_IsUrgente": false,
"Description": "Criada pelo aplicativo de campo",
"EDU_VisitaLinha": [
{ "Line": 10, "M_Product_ID": 123, "Qty": 2 }
],
"doc-action": "CO"
}'
Três coisas acontecem numa chamada só: o cabeçalho é gravado, a linha vai junto (o nome da tabela filha vira uma lista dentro do JSON) e doc-action roda o Complete, como o botão da janela. Abra a visita no iDempiere: está lá, completa, com endereço preenchido pelo beforeSave.
Agora prove que as regras valem. Repita a chamada sem EDU_VisitaLinha. A visita é criada, mas o Complete falha com a mensagem EDU_VisitaSemLinhas, a mesma da tela.
Esse é o resultado dos módulos 04 a 09: a regra mora na MVisita, então a API não precisa reimplementar nada. Se a regra estivesse num callout, a API passaria por cima dela.
Parte 2: avisar outro sistema
5. A configuração
Na janela System Configurator, como System:
| Campo | Valor |
|---|---|
| Name | EDU_VISITA_WEBHOOK_URL |
| Description | URL que recebe um POST quando uma visita é concluída. Vazio desliga. |
| Configured Value | http://localhost:9999/visitas |
| Configuration Level | Client |
| Entity Type | EDU |
Com o nível Client, cada empresa aponta para o próprio sistema, ou deixa vazio para não integrar.
6. O event handler de documento
No MANIFEST.MF, acrescente com.google.gson ao Require-Bundle: é a biblioteca de JSON que o próprio core usa.
Crie com.gardenworld.visitas.event.NotificarVisitaConcluida, no mesmo pacote do event handler do módulo 05 (o VisitasEventManager já registra tudo o que estiver nele):
package com.gardenworld.visitas.event;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.logging.Level;
import org.adempiere.base.annotation.EventTopicDelegate;
import org.adempiere.base.annotation.ModelEventTopic;
import org.adempiere.base.event.annotations.ModelEventDelegate;
import org.adempiere.base.event.annotations.doc.AfterComplete;
import org.compiere.model.MSysConfig;
import org.compiere.util.CLogger;
import org.compiere.util.Trx;
import org.compiere.util.TrxEventListener;
import org.compiere.util.Util;
import org.osgi.service.event.Event;
import com.gardenworld.visitas.model.MVisita;
import com.google.gson.JsonObject;
@EventTopicDelegate
@ModelEventTopic(modelClass = MVisita.class)
public class NotificarVisitaConcluida extends ModelEventDelegate<MVisita> {
public static final String SYSCONFIG_URL = "EDU_VISITA_WEBHOOK_URL";
private static final CLogger log = CLogger.getCLogger(NotificarVisitaConcluida.class);
private static final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
public NotificarVisitaConcluida(MVisita po, Event event) {
super(po, event);
}
@AfterComplete
public void aoCompletar() {
MVisita visita = getModel();
String url = MSysConfig.getValue(SYSCONFIG_URL, "", visita.getAD_Client_ID());
if (Util.isEmpty(url, true))
return; // integração desligada nesta empresa
// Monta o conteúdo agora, enquanto o registro está em memória
JsonObject json = new JsonObject();
json.addProperty("evento", "visita.concluida");
json.addProperty("visitaId", visita.getEDU_Visita_ID());
json.addProperty("numero", visita.getDocumentNo());
json.addProperty("cliente", visita.get_DisplayValue(MVisita.COLUMNNAME_C_BPartner_ID, true));
json.addProperty("data", String.valueOf(visita.getEDU_DataVisita()));
String corpo = json.toString();
// Envia só depois do commit: se a transação falhar, ninguém é avisado
Trx trx = Trx.get(visita.get_TrxName(), false);
if (trx == null) {
enviar(url, corpo);
return;
}
trx.addTrxEventListener(new TrxEventListener() {
@Override
public void afterCommit(Trx t, boolean success) {
if (success)
enviar(url, corpo);
}
@Override
public void afterRollback(Trx t, boolean success) {
}
@Override
public void afterClose(Trx t) {
}
});
}
private static void enviar(String url, String corpo) {
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(corpo))
.build();
// Assíncrono: a tela do usuário não espera o outro sistema responder
http.sendAsync(request, HttpResponse.BodyHandlers.discarding())
.whenComplete((resposta, erro) -> {
if (erro != null)
log.log(Level.WARNING, "Falha ao notificar " + url, erro);
else if (resposta.statusCode() >= 300)
log.warning("Notificação recusada por " + url + ": HTTP " + resposta.statusCode());
});
}
}
Este é o código mais importante do módulo, e não pelo HTTP. Pelas três decisões:
@AfterComplete, e não@AfterChange. O evento de documento dispara exatamente quando a visita completa, venha da tela, da API ou de um processo. Isso só funciona porque aMVisitachamafireDocValidate(módulo 09).- O envio espera o commit. O
@AfterCompleteroda dentro da transação. Se você chamasse o outro sistema ali e, logo depois, algo desse rollback, o sistema de atendimento saberia de uma visita que nunca foi concluída. OTrxEventListeneradia o envio para depois do commit, e só se ele deu certo. - O envio é assíncrono e tolerante a falha.
sendAsyncnão prende a tela do usuário esperando a resposta; uma falha vira aviso no log, não erro na tela. A visita foi concluída de qualquer jeito: a notificação é consequência, não condição.
7. Teste com o netcat
Num terminal, deixe o "sistema de atendimento" escutando:
nc -l 9999
Reinicie o servidor. Complete uma visita pela janela (ou pela API, passo 4). O terminal do nc mostra a requisição chegando:
POST /visitas HTTP/1.1
Content-Type: application/json
...
{"evento":"visita.concluida","visitaId":1000012,"numero":"VT-1000012","cliente":"Joe Block","data":"2026-10-15 00:00:00.0"}
O nc não responde, então o log do servidor vai mostrar um aviso de falha depois do tempo limite: é o esperado. Um receptor de verdade responde 200.
Agora teste o desligamento: apague o valor da configuração, faça Cache Reset e complete outra visita. Nada chega.
8. Commit
Inclua no Pack Out a configuração nova (Data, AD_SysConfig, filtrando pelo nome). Gere o 2Pack_1.0.7.zip.
git add .
git commit -m "Visitas 1.0.7: notificação de visita concluída"
Checkpoint
Erros comuns
| Sintoma | Causa provável | Correção |
|---|---|---|
| Extension Management diz que o repositório não está configurado | O servidor subiu sem -DIDEMPIERE_EXTENSION_REPOSITORY | Use a configuração server.product do repositório, que já traz o parâmetro |
401 em toda chamada | Token ausente, vencido ou sem o prefixo Bearer | Gere outro token; confira o cabeçalho |
| Login pela API recusado | Perfil com Role Type diferente de vazio ou WebService | Use outro perfil, ou um perfil próprio para a integração |
POST falha com coluna obrigatória | Campo que a tela preenche sozinha (Default Logic) ficou fora do JSON | Informe no JSON, ou calcule no beforeSave |
| Visita criada, mas não completa | doc-action recusado por uma regra | Leia a mensagem na resposta: é a mesma da tela |
Nada chega no nc | URL vazia, cache antigo, ou o handler não foi registrado | Confira a configuração, Cache Reset, e OSGI-INF/ |
O nc recebe, mas a visita não completou | Envio feito antes do commit | Confira o TrxEventListener: nunca chame fora dele |
Desafio
- Crie a
MVisitaLinhacom umbeforeSaveque calcula oLinequando ele vem vazio, e repita oPOSTdo passo 4 semLine. - Acrescente um cabeçalho
X-Assinaturacom um HMAC-SHA256 do corpo, usando um segredo guardado numa segunda configuração. É assim que o receptor confere que a notificação veio mesmo do iDempiere.
Referência
Outros pontos de integração
| Recurso | O que é | Quando usar |
|---|---|---|
api/v1/processes (plugin REST) | Roda processos e relatórios pela API | Gerar o relatório do módulo 07 de fora |
api/v1/windows (plugin REST) | Grava pela janela, com callouts e Default Logic | O cliente da API quer o comportamento exato da tela |
| Serviço OSGi próprio | Uma interface Java publicada pelo seu plugin e consumida por outros | Dois plugins seus precisam conversar sem depender um do outro |
Garantias da notificação deste módulo
É no máximo uma vez: se o outro sistema estiver fora do ar, a notificação se perde (fica o aviso no log). Quando perder não é aceitável, o padrão é gravar a notificação numa tabela própria, na mesma transação da visita, e ter um processo agendado (módulo 06) que envia e reenvia até receber 200. É mais trabalho, e é o que se faz para integrações que movimentam dinheiro ou estoque.
Próximo módulo
12 · Rumo à produção: compilar o plugin como jar, instalar num servidor, boas práticas de desempenho e como contribuir com o projeto.