# 05 · Regras de negócio: callout e event handler

Fonte: https://muriloht.com/idempiere/05-callout-e-event-handler (Guia iDempiere, Murilo H. Torquato). Versão alvo: iDempiere 14.

Objetivos:
- Evoluir o Dicionário e distribuir a mudança como um 2Pack novo
- Reagir ao que o usuário digita com um callout
- Saber quando uma regra vai no callout e quando vai no beforeSave
- Reagir a gravações de tabelas do core com um event handler

## Antes de começar

- Módulo 04 concluído: o plugin `com.gardenworld.visitas` sobe como `ACTIVE`, e a `MVisita` barra a conclusão sem linhas.
- Servidor em modo Debug. Neste módulo você vai reiniciar algumas vezes.

## O que você vai construir

Três comportamentos novos, cada um no lugar certo:

- **O técnico precisa saber aonde ir.** A visita ganha o campo **Endereço**. Ao escolher o cliente, o endereço de entrega dele aparece na hora (callout).
- **O endereço nunca fica vazio,** mesmo quando a visita é criada por código, sem tela (`beforeSave`).
- **Cliente com visita em aberto não pode ser desativado.** A regra fica na tabela de parceiros, que é do core (event handler).

## Onde cada regra mora

Este é o conceito central do módulo. Antes de escrever código, decida onde a regra vive:

| Mecanismo | Roda quando | Roda sem tela? | Use para |
|---|---|---|---|
| **Lógicas do Dicionário** (módulo 02) | O usuário mexe na janela | Não | Mostrar, esconder, travar, valor inicial |
| **Callout** | O usuário muda um campo na janela, antes de salvar | **Não** | Conveniência: preencher outros campos na hora |
| **`beforeSave` da classe M** (módulo 04) | Qualquer gravação da **sua** tabela | Sim | Regras e valores garantidos da sua tabela |
| **Event handler** | Qualquer gravação de **qualquer** tabela, inclusive do core | Sim | Reagir a tabelas que não são suas |

A pergunta que resolve quase todos os casos: **"se esse registro for criado por uma importação ou por uma API, a regra precisa valer?"** Se sim, callout não serve. Ele é para o usuário, não para o dado.

## Passo a passo

### 1. Uma coluna nova e o pacote 1.0.1

Como **System**, crie uma validação dinâmica (janela **Validation Rules**, como no módulo 01):

| Campo | Valor |
|---|---|
| Name | `EDU - Endereço do Cliente` |
| Type | `SQL` |
| Validation code | `C_BPartner_Location.C_BPartner_ID=@C_BPartner_ID@` |
| Entity Type | `EDU` |

Na tabela `EDU_Visita`, crie a coluna e sincronize:

| DB Column Name | Reference | Detalhes |
|---|---|---|
| `C_BPartner_Location_ID` | Table Direct | Dynamic Validation: `EDU - Endereço do Cliente` |

O elemento `C_BPartner_Location_ID` já existe no core (é o mesmo **Partner Location** dos pedidos), então não é preciso criar um. Não marque Mandatory: as visitas que já existem não têm endereço.

Na janela **Window, Tab and Field**, aba **Visita**, rode **Create Fields** e posicione o campo logo abaixo do cliente no **Tab Editor**.

Agora distribua a mudança. Abra a janela **Pack Out**, no registro `EDU_Visitas` do módulo 03:

1. Na aba de detalhes, inclua a validação nova com Seq `35` (depois da outra validação, antes das tabelas).
2. No cabeçalho, mude **Package Version** para `1.0.1`.
3. Exporte e salve o zip em `com.gardenworld.visitas/META-INF/2Pack_1.0.1.zip`.

**Não apague o `2Pack_1.0.0.zip`.** Um ambiente novo aplica os dois, em ordem; o seu ambiente, que já tem o 1.0.0, aplica só o 1.0.1. É o mesmo raciocínio dos scripts de migração do core.

> O pacote é cumulativo: o 1.0.1 descreve o estado completo dos objetos que ele lista, não só a diferença. Por isso incluir o que não mudou não faz mal, e esquecer um objeto novo faz.

### 2. Regenere as classes

Rode o **Generate Model** de novo, com os mesmos parâmetros do módulo 04. A `X_EDU_Visita` ganha `getC_BPartner_Location_ID()` e `setC_BPartner_Location_ID()`. A `MVisita` não é tocada: é por isso que a regra fica nela.

Aperte **F5** no plugin e confira que tudo compila.

### 3. O endereço padrão, num lugar só

A mesma lógica ("qual é o endereço padrão deste cliente?") vai ser usada pelo callout e pelo `beforeSave`. Escreva uma vez, na `MVisita`:

```java
/** Endereço padrão do cliente: o primeiro de entrega; se não houver, o primeiro ativo. */
public static int getEnderecoPadrao(Properties ctx, int C_BPartner_ID, String trxName) {
    return new Query(ctx, MBPartnerLocation.Table_Name,
            "C_BPartner_ID=? AND IsActive='Y'", trxName)
        .setParameters(C_BPartner_ID)
        .setOrderBy("IsShipTo DESC, C_BPartner_Location_ID")
        .firstId();
}
```

`firstId()` devolve `-1` quando não encontra nada. Adicione o import `org.compiere.model.MBPartnerLocation`.

Agora garanta o valor no `beforeSave`, antes da regra do módulo 04:

```java
@Override
protected boolean beforeSave(boolean newRecord) {
    if (getC_BPartner_Location_ID() <= 0
            || is_ValueChanged(COLUMNNAME_C_BPartner_ID) && !is_ValueChanged(COLUMNNAME_C_BPartner_Location_ID)) {
        setC_BPartner_Location_ID(getEnderecoPadrao(getCtx(), getC_BPartner_ID(), get_TrxName()));
    }

    if (isEDU_IsConcluida()
            && is_ValueChanged(COLUMNNAME_EDU_IsConcluida)
            && getLinhas().isEmpty()) {
        log.saveError("Error", "Inclua pelo menos uma linha antes de concluir a visita");
        return false;
    }
    return true;
}
```

A segunda condição cobre quem troca o cliente e esquece o endereço: o endereço antigo seria de outro parceiro. O setter gerado grava `null` quando recebe um valor menor que 1, então o `-1` vira campo vazio.

### 4. O callout

**Conceito:** um callout é uma classe que o iDempiere chama quando o usuário muda um campo na janela. Ela recebe a aba (`GridTab`) e pode ler e alterar os outros campos antes de o registro ser salvo. Nada vai para o banco até o usuário salvar.

Crie `com.gardenworld.visitas.callout.CalloutVisitaCliente`:

```java
package com.gardenworld.visitas.callout;

import java.util.Properties;

import org.adempiere.base.IColumnCallout;
import org.adempiere.base.annotation.Callout;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;

import com.gardenworld.visitas.model.MVisita;

@Callout(tableName = MVisita.Table_Name, columnName = MVisita.COLUMNNAME_C_BPartner_ID)
public class CalloutVisitaCliente implements IColumnCallout {

    @Override
    public String start(Properties ctx, int WindowNo, GridTab mTab, GridField mField,
            Object value, Object oldValue) {
        Integer enderecoId = null;
        if (value instanceof Integer bpartnerId && bpartnerId > 0) {
            int id = MVisita.getEnderecoPadrao(ctx, bpartnerId, null);
            if (id > 0)
                enderecoId = id;
        }
        mTab.setValue(MVisita.COLUMNNAME_C_BPartner_Location_ID, enderecoId);
        return "";
    }
}
```

O retorno é a mensagem de erro: `""` significa que deu tudo certo. Repare que o callout **reaproveita** a regra da `MVisita` em vez de repeti-la, e usa `null` como transação: ainda não existe gravação em andamento.

Falta a factory, igual à de modelo do módulo 04. Crie `com.gardenworld.visitas.VisitasCalloutFactory`:

```java
package com.gardenworld.visitas;

import org.adempiere.base.AnnotationBasedColumnCalloutFactory;
import org.adempiere.base.IColumnCalloutFactory;
import org.osgi.service.component.annotations.Component;

@Component(immediate = true, service = IColumnCalloutFactory.class)
public class VisitasCalloutFactory extends AnnotationBasedColumnCalloutFactory {

    @Override
    protected String[] getPackages() {
        return new String[] {"com.gardenworld.visitas.callout"};
    }
}
```

> Existe um jeito antigo de registrar callout: escrever o nome da classe no campo **Callout** da coluna, no Dicionário. Ainda funciona, mas amarra o Dicionário a uma classe Java. Em código novo, use a anotação.

### 5. O event handler

**Conceito:** toda gravação no iDempiere dispara eventos (antes de inserir, depois de alterar, antes de excluir...). Um event handler escuta os eventos de uma tabela e pode barrar a gravação lançando uma exceção. Funciona para qualquer tabela, e é assim que um plugin muda o comportamento do core sem tocar nele.

Primeiro, o plugin precisa enxergar a API de eventos do OSGi. No `MANIFEST.MF`, acrescente ao `Import-Package`:

```
Import-Package: org.osgi.framework,
 org.osgi.service.component.annotations;resolution:=optional,
 org.osgi.service.event
```

Crie `com.gardenworld.visitas.event.ClienteComVisitaAberta`:

```java
package com.gardenworld.visitas.event;

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.po.BeforeChange;
import org.adempiere.exceptions.AdempiereException;
import org.compiere.model.MBPartner;
import org.compiere.model.Query;
import org.osgi.service.event.Event;

import com.gardenworld.visitas.model.MVisita;

@EventTopicDelegate
@ModelEventTopic(modelClass = MBPartner.class)
public class ClienteComVisitaAberta extends ModelEventDelegate<MBPartner> {

    public ClienteComVisitaAberta(MBPartner po, Event event) {
        super(po, event);
    }

    @BeforeChange
    public void impedirDesativacao() {
        MBPartner cliente = getModel();
        if (cliente.isActive() || !cliente.is_ValueChanged(MBPartner.COLUMNNAME_IsActive))
            return;

        boolean temVisitaAberta = new Query(cliente.getCtx(), MVisita.Table_Name,
                "C_BPartner_ID=? AND EDU_IsConcluida='N'", cliente.get_TrxName())
            .setParameters(cliente.getC_BPartner_ID())
            .match();

        if (temVisitaAberta)
            throw new AdempiereException("Este cliente tem visitas técnicas em aberto. Conclua as visitas antes de desativá-lo.");
    }
}
```

E o gerenciador que registra os delegates do pacote, `com.gardenworld.visitas.VisitasEventManager`:

```java
package com.gardenworld.visitas;

import org.adempiere.base.AnnotationBasedEventManager;
import org.osgi.service.component.annotations.Component;

@Component(immediate = true)
public class VisitasEventManager extends AnnotationBasedEventManager {

    @Override
    public String[] getPackages() {
        return new String[] {"com.gardenworld.visitas.event"};
    }
}
```

O padrão é o mesmo das factories: a classe base varre o pacote atrás de `@EventTopicDelegate`, e a anotação no método (`@BeforeChange`) diz em qual evento ele roda. Uma instância do delegate é criada a cada evento, com o registro em `getModel()`.

Duas regras de ouro de event handler:

- **Use sempre `get_TrxName()` do registro** nas consultas. O handler roda dentro da transação da gravação.
- **Seja rápido.** Um handler em `C_BPartner` roda em toda gravação de parceiro do sistema inteiro, inclusive nas do core. Saia cedo (`return`) quando o evento não interessa, como no `if` do início.

### 6. Teste

Reinicie o servidor: há factories e um gerenciador novos. No console, confira `ss com.gardenworld` e, na aba **Package Installation** da janela **Pack In**, o registro da versão `1.0.1`.

Como GardenAdmin:

1. **Callout:** crie uma visita e escolha o cliente `Joe Block`. O endereço aparece antes de salvar. Troque o cliente: o endereço troca junto.
2. **Validação:** abra a lista do campo Endereço. Só aparecem endereços do cliente escolhido.
3. **beforeSave:** apague o endereço e salve. Ao recarregar o registro (**F5** na janela), o endereço padrão voltou.
4. **Event handler:** deixe uma visita de `Joe Block` sem marcar Concluída. Abra a janela **Business Partner**, desmarque **Active** em `Joe Block` e salve. A gravação é recusada com a sua mensagem.

Coloque breakpoints no callout e no delegate e repita os testes para ver a ordem das chamadas.

### 7. Commit

```bash
cd ~/sources/guia-idempiere-projeto
git add .
git commit -m "Visitas 1.0.1: endereço do cliente, callout e bloqueio de desativação"
```

## Checkpoint

- [ ] A coluna Endereço existe na visita e só lista endereços do cliente escolhido
- [ ] O pacote 1.0.1 está em META-INF e aparece como instalado na aba Package Installation
- [ ] Ao escolher o cliente na tela, o endereço é preenchido na hora
- [ ] Uma visita salva sem endereço recebe o endereço padrão do cliente
- [ ] Não consigo desativar um cliente que tem visita em aberto
- [ ] Sei explicar por que o callout não basta para garantir uma regra

## Erros comuns

| Sintoma | Causa provável | Correção |
|---|---|---|
| O callout não é chamado | Factory não registrada ou pacote errado em `getPackages()` | Confira `OSGI-INF/` e o nome do pacote; reinicie |
| O callout roda, mas o endereço não aparece | O campo Endereço não está na aba | Rode **Create Fields** na aba Visita |
| O plugin fica `INSTALLED` depois deste módulo | Faltou `org.osgi.service.event` no `Import-Package` | Passo 5 |
| O event handler nunca dispara | O `VisitasEventManager` não foi registrado | Confira `OSGI-INF/` e reinicie |
| A desativação passa mesmo com visita aberta | Visita concluída, ou de outro cliente | Confira `EDU_IsConcluida` no banco |
| O pacote 1.0.1 não é aplicado | O nome não segue `2Pack_x.y.z.zip`, ou a versão do cabeçalho do Pack Out ficou 1.0.0 | Confira o nome do arquivo e a versão |
| Lista de endereços vazia | A validação usa `@C_BPartner_ID@` e o cliente ainda não foi escolhido | Escolha o cliente primeiro |

## Desafio

Duas regras para colocar no lugar certo:

1. Ao marcar **Urgente** na tela, preencha o **Motivo da Urgência** com o texto `Descreva o problema:` se ele estiver vazio. Callout ou beforeSave?
2. Impeça que um produto seja desativado se ele aparece numa linha de visita em aberto. Qual tabela o handler escuta, e qual consulta ele faz?

## Referência

### Eventos de modelo

| Anotação | Momento |
|---|---|
| `@BeforeNew` | Antes do `INSERT` |
| `@AfterNew` | Depois do `INSERT`, na mesma transação |
| `@BeforeChange` | Antes do `UPDATE` |
| `@AfterChange` | Depois do `UPDATE`, na mesma transação |
| `@BeforeDelete` | Antes do `DELETE` |
| `@AfterDelete` | Depois do `DELETE` |
| `@PostCreate`, `@PostUpdate`, `@PostDelete` | Depois do **commit**, fora da transação e de forma assíncrona |

Os de documento (completar, anular, estornar) estão no pacote `org.adempiere.base.event.annotations.doc` e aparecem no módulo 09.

Use `After*` para mexer em outros registros dentro da mesma transação (se algo falhar, tudo volta). Use `Post*` para efeitos externos que só devem acontecer se a gravação foi confirmada, como avisar outro sistema (módulo 11).

### Ordem de execução numa gravação

1. `beforeSave` da classe M
2. Event handlers `Before*`
3. `INSERT` / `UPDATE` no banco
4. `afterSave` da classe M
5. Event handlers `After*`
6. Commit
7. Event handlers `Post*`

### Callout: o que dá para fazer com o `GridTab`

| Chamada | Para quê |
|---|---|
| `mTab.getValue("Coluna")` | Ler outro campo da aba |
| `mTab.setValue("Coluna", valor)` | Alterar outro campo |
| `Env.getContextAsInt(ctx, WindowNo, "Coluna")` | Ler o contexto da janela, inclusive de abas acima |
| `return "mensagem"` | Mostrar um erro ao usuário |

Callout é código de tela: não acesse o banco em excesso e não grave registros nele. Quem grava é o `beforeSave` ou um processo.

## Próximo módulo

[06 · Processos](https://muriloht.com/idempiere/06-processos): gerar visitas de manutenção em lote, com parâmetros, a partir do menu.
