Módulos do guia

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

ver .md

Antes de começar

  • Módulo 09 concluído: a visita é um documento com o botão Document Action.
  • curl e nc (netcat) no terminal. O nc faz 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çãoQuem iniciaFerramentaQuando usar
EntradaO outro sistemaAPI REST (plugin)O outro sistema precisa ler ou gravar no iDempiere
SaídaO iDempiereEvent handler + HTTPO 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.

  1. Entre como System e abra o formulário Extension Management.
  2. Localize iDempiere REST API e clique para instalar. O iDempiere baixa o jar, instala e inicia o plugin.
  3. Confira no console do Eclipse: ss com.trekglobal.idempiere.rest.api deve mostrar ACTIVE. 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âmetroExemploFaz
$filterProcessed eq falseFiltra (eq, ne, gt, lt, and, or, in)
$selectDocumentNo,DocStatusEscolhe as colunas
$orderbyEDU_DataVisita descOrdena
$top / $skip10 / 20Pagina
$expandEDU_VisitaLinhaTraz 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:

CampoValor
NameEDU_VISITA_WEBHOOK_URL
DescriptionURL que recebe um POST quando uma visita é concluída. Vazio desliga.
Configured Valuehttp://localhost:9999/visitas
Configuration LevelClient
Entity TypeEDU

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:

  1. @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 a MVisita chama fireDocValidate (módulo 09).
  2. O envio espera o commit. O @AfterComplete roda 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. O TrxEventListener adia o envio para depois do commit, e só se ele deu certo.
  3. O envio é assíncrono e tolerante a falha. sendAsync nã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

0 de 6 itens

Erros comuns

SintomaCausa provávelCorreção
Extension Management diz que o repositório não está configuradoO servidor subiu sem -DIDEMPIERE_EXTENSION_REPOSITORYUse a configuração server.product do repositório, que já traz o parâmetro
401 em toda chamadaToken ausente, vencido ou sem o prefixo BearerGere outro token; confira o cabeçalho
Login pela API recusadoPerfil com Role Type diferente de vazio ou WebServiceUse outro perfil, ou um perfil próprio para a integração
POST falha com coluna obrigatóriaCampo que a tela preenche sozinha (Default Logic) ficou fora do JSONInforme no JSON, ou calcule no beforeSave
Visita criada, mas não completadoc-action recusado por uma regraLeia a mensagem na resposta: é a mesma da tela
Nada chega no ncURL vazia, cache antigo, ou o handler não foi registradoConfira a configuração, Cache Reset, e OSGI-INF/
O nc recebe, mas a visita não completouEnvio feito antes do commitConfira o TrxEventListener: nunca chame fora dele

Desafio

  1. Crie a MVisitaLinha com um beforeSave que calcula o Line quando ele vem vazio, e repita o POST do passo 4 sem Line.
  2. Acrescente um cabeçalho X-Assinatura com 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

RecursoO que éQuando usar
api/v1/processes (plugin REST)Roda processos e relatórios pela APIGerar o relatório do módulo 07 de fora
api/v1/windows (plugin REST)Grava pela janela, com callouts e Default LogicO cliente da API quer o comportamento exato da tela
Serviço OSGi próprioUma interface Java publicada pelo seu plugin e consumida por outrosDois 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.