# 04 · Plugin e classe de modelo

Fonte: https://muriloht.com/idempiere/04-plugin-e-modelo (Guia iDempiere, Murilo H. Torquato). Versão alvo: iDempiere 14.

Objetivos:
- Entender por que toda customização no iDempiere vive num plugin OSGi
- Criar um plugin no Eclipse e subir junto com o servidor
- Fazer o plugin aplicar o 2Pack sozinho quando sobe
- Gerar as classes das suas tabelas e registrar uma model factory
- Escrever a primeira regra de negócio em Java, com beforeSave

## Antes de começar

- Módulos 01 a 03 concluídos. Você vai precisar do `2Pack_1.0.0.zip` gerado no módulo 03.
- Servidor rodando em modo **Debug** pelo Eclipse (**Run > Debug Configurations > server.product**). A partir daqui, o debug deixa de ser opcional.
- Este é o primeiro módulo com Java. O que se espera: saber criar classe, herança, e ler uma stack trace.

## O que você vai construir

Até o módulo 03, a janela **Visita Técnica** funcionava sem nenhuma linha de Java. O iDempiere salvava os registros com uma classe genérica (`GenericPO`), que sabe gravar qualquer tabela mas não conhece nenhuma regra.

Neste módulo você cria o plugin `com.gardenworld.visitas` e coloca nele três coisas:

- **O 2Pack do módulo 03**, aplicado automaticamente quando o plugin sobe. Instalar o plugin passa a instalar tabelas, janela e menu.
- **As classes das suas tabelas**, geradas a partir do Dicionário.
- **A primeira regra de negócio:** uma visita só pode ser concluída se tiver pelo menos uma linha.

## Por que um plugin

O iDempiere roda sobre **OSGi**: o servidor é um conjunto de módulos (bundles) que sobem, param e se enxergam por contratos. O core é um desses bundles; o seu plugin é outro.

A regra da comunidade é simples: **customização nunca altera o core**. Tudo o que você escreve fica no seu plugin e conversa com o core por **pontos de extensão** (factories, event handlers, serviços). É isso que permite atualizar o iDempiere sem reaplicar customizações à mão.

> Se você vem do Protheus: o plugin cumpre o papel dos fontes customizados com pontos de entrada, com uma diferença. Aqui o ponto de extensão é um contrato Java registrado no OSGi, não uma função com nome reservado.

## Passo a passo

### 1. Crie o plugin

No Eclipse, **File > New > Other > Plug-in Development > Plug-in Project**.

| Campo | Valor |
|---|---|
| Project name | `com.gardenworld.visitas` |
| Use default location | **desmarcado**. Location: `~/sources/guia-idempiere-projeto/com.gardenworld.visitas` |
| Target Platform | **an OSGi framework: Equinox** |
| Execution environment | `JavaSE-17` |

Na página seguinte:

| Campo | Valor |
|---|---|
| Version | `1.0.0.qualifier` |
| Name | `Visitas Técnicas` |
| Generate an activator | marcado, com a classe `com.gardenworld.visitas.Activator` |

Clique em **Finish**. O plugin fica dentro do repositório do projeto (o do módulo 03), mas aparece no workspace do Eclipse ao lado do core.

### 2. Declare as dependências

Abra `META-INF/MANIFEST.MF`, vá para a aba **MANIFEST.MF** (o texto puro) e deixe assim:

```
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Visitas Técnicas
Bundle-SymbolicName: com.gardenworld.visitas;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.gardenworld.visitas.Activator
Bundle-RequiredExecutionEnvironment: JavaSE-17
Require-Bundle: org.adempiere.base;bundle-version="14.0.0",
 org.adempiere.plugin.utils;bundle-version="14.0.0"
Import-Package: org.osgi.framework,
 org.osgi.service.component.annotations;resolution:=optional
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: com.gardenworld.visitas
```

O que importa aqui:

- **`Require-Bundle`**: o plugin enxerga as classes do core (`org.adempiere.base`) e os utilitários de plugin (`org.adempiere.plugin.utils`), onde fica o ativador que aplica 2Pack.
- **`org.osgi.service.component.annotations`**: as anotações `@Component` que você vai usar no passo 6. Só existem na compilação, por isso `resolution:=optional`.
- **`singleton:=true`**: só pode existir uma versão do plugin ativa por vez.

> O manifesto é sensível a formato: as linhas de continuação começam com **um espaço**, e o arquivo termina com uma linha em branco. Um erro aqui faz o plugin simplesmente não subir.

### 3. O plugin aplica o 2Pack sozinho

Mova o pacote do módulo 03 para dentro do plugin:

```bash
cd ~/sources/guia-idempiere-projeto
git mv 2pack/2Pack_1.0.0.zip com.gardenworld.visitas/META-INF/2Pack_1.0.0.zip
```

Troque o conteúdo de `Activator.java`:

```java
package com.gardenworld.visitas;

import org.adempiere.plugin.utils.Incremental2PackActivator;

public class Activator extends Incremental2PackActivator {
}
```

Isso é tudo. Quando o plugin sobe, o `Incremental2PackActivator` procura arquivos `META-INF/2Pack_*.zip`, compara as versões com o que já foi instalado para este plugin e aplica só o que falta, em ordem de versão. O controle fica na tabela `AD_Package_Imp`, com o nome do plugin (`com.gardenworld.visitas`).

Quando mudar o Dicionário, gere `2Pack_1.0.1.zip` e coloque ao lado. O 1.0.0 nunca é reaplicado.

> No seu banco de desenvolvimento, as tabelas já existem. O pacote vai ser aplicado mesmo assim, uma vez, e só atualiza os registros: o 2Pack identifica tudo pelo UUID, que é o mesmo.

### 4. Suba o plugin junto com o servidor

O `server.product` não inclui plugins novos sozinho.

1. **Run > Debug Configurations > server.product**, aba **Plug-ins**.
2. Na lista **Workspace**, marque `com.gardenworld.visitas`.
3. Na coluna **Auto-Start** do plugin, escolha `true`.
4. Clique em **Validate Plug-ins**. Não pode haver erro.
5. **Debug**.

Sem o **Auto-Start**, o plugin é carregado mas não é iniciado: o ativador não roda e nada do que você registrar no passo 6 existe.

Com o servidor no ar, digite no console do Eclipse:

```
ss com.gardenworld
```

O plugin deve aparecer como `ACTIVE`. Se aparecer `INSTALLED`, faltou alguma dependência: rode `diag <id>` com o número da primeira coluna para ver qual.

Agora confira o 2Pack: entre como **System**, abra a janela **Pack In** e vá para a aba **Package Installation**. Deve haver um registro `com.gardenworld.visitas`, versão `1.0.0`, com status de concluído.

### 5. Gere as classes das tabelas

**Conceito:** para cada tabela, o iDempiere trabalha com três classes:

| Classe | Quem escreve | O que tem |
|---|---|---|
| `I_EDU_Visita` | gerada | Interface com o nome da tabela, das colunas (`COLUMNNAME_...`) e as assinaturas dos getters e setters |
| `X_EDU_Visita` | gerada | Implementação dos getters e setters, herdando de `PO` (a classe que lê e grava no banco) |
| `MVisita` | **você** | Herda da X e concentra as regras de negócio |

A separação existe porque as classes I e X são geradas de novo toda vez que uma coluna muda. Se você escrever regra nelas, perde tudo na próxima geração.

Primeiro, diga ao iDempiere qual é o pacote Java do tipo de entidade: como **System**, na janela **Entity Type**, abra `EDU` e preencha **Model Package** com `com.gardenworld.visitas.model`. O gerador usa isso quando uma tabela sua aponta para outra tabela sua.

Depois, rode o processo **Generate Model** (busque no menu):

| Parâmetro | Valor |
|---|---|
| Folder | o caminho **absoluto** da pasta `src` do plugin, por exemplo `/home/voce/sources/guia-idempiere-projeto/com.gardenworld.visitas/src` |
| DB Table Name | `EDU_%` |
| Package Name | `com.gardenworld.visitas.model` |
| Table Entity Type | `EDU` |
| Column Entity Type | em branco (todas as colunas) |
| Generate Interface | marcado |
| Generate Class | marcado |

O processo roda no servidor e grava os arquivos na pasta informada, criando as subpastas do pacote. Como o servidor está na sua máquina, o caminho é o da sua máquina.

No Eclipse, clique no plugin e aperte **F5** para ver os quatro arquivos novos: `I_EDU_Visita`, `X_EDU_Visita`, `I_EDU_VisitaLinha` e `X_EDU_VisitaLinha`.

Abra `X_EDU_Visita` e repare na primeira linha da classe:

```java
@org.adempiere.base.Model(table="EDU_Visita")
public class X_EDU_Visita extends PO implements I_EDU_Visita, I_Persistent
```

É essa anotação que liga a classe à tabela. Repare também no nome dos getters: a coluna `EDU_IsConcluida` vira `isEDU_IsConcluida()`, e `EDU_DataVisita` vira `getEDU_DataVisita()`.

> Nunca edite uma classe I ou X à mão. Mudou uma coluna, gere de novo.

### 6. Registre a model factory

O core ainda não sabe que essas classes existem. Quem diz ao iDempiere "para a tabela `EDU_Visita`, use esta classe" é uma **model factory**.

Crie `com.gardenworld.visitas.VisitasModelFactory`:

```java
package com.gardenworld.visitas;

import org.adempiere.base.AnnotationBasedModelFactory;
import org.adempiere.base.IModelFactory;
import org.osgi.service.component.annotations.Component;

@Component(immediate = true, service = IModelFactory.class,
           property = {"service.ranking:Integer=1"})
public class VisitasModelFactory extends AnnotationBasedModelFactory {

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

A classe base varre o pacote informado atrás de classes com `@Model` e monta o mapa tabela → classe. Quando encontra uma X que tem uma subclasse (a sua M), usa a subclasse.

O `@Component` registra a factory como serviço OSGi. O Eclipse transforma a anotação num arquivo em `OSGI-INF/` e adiciona o cabeçalho `Service-Component` ao manifesto. Confira os dois depois de salvar.

> Se a pasta `OSGI-INF` não aparecer, ligue **Window > Preferences > Plug-in Development > DS Annotations > Generate descriptors from annotated sources**.

### 7. A classe M e a primeira regra

Crie `com.gardenworld.visitas.model.MVisita`:

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

import java.sql.ResultSet;
import java.util.List;
import java.util.Properties;

import org.compiere.model.Query;

public class MVisita extends X_EDU_Visita {

    private static final long serialVersionUID = 1L;

    public MVisita(Properties ctx, int EDU_Visita_ID, String trxName) {
        super(ctx, EDU_Visita_ID, trxName);
    }

    public MVisita(Properties ctx, String EDU_Visita_UU, String trxName) {
        super(ctx, EDU_Visita_UU, trxName);
    }

    public MVisita(Properties ctx, ResultSet rs, String trxName) {
        super(ctx, rs, trxName);
    }

    /** Linhas da visita, em ordem. */
    public List<X_EDU_VisitaLinha> getLinhas() {
        return new Query(getCtx(), X_EDU_VisitaLinha.Table_Name,
                X_EDU_VisitaLinha.COLUMNNAME_EDU_Visita_ID + "=?", get_TrxName())
            .setParameters(getEDU_Visita_ID())
            .setOrderBy(X_EDU_VisitaLinha.COLUMNNAME_Line)
            .list();
    }

    @Override
    protected boolean beforeSave(boolean newRecord) {
        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;
    }
}
```

Pontos que valem para qualquer classe M:

- **Os três construtores são obrigatórios.** A factory cria os objetos por reflexão: pelo ID, pelo UUID e a partir de uma linha de `ResultSet`. Faltou um, e algum caminho do core falha ao criar o objeto.
- **`beforeSave` devolve `false` para barrar o salvamento.** O `log.saveError` define a mensagem que o usuário vê.
- **`get_TrxName()` em toda consulta.** A consulta roda dentro da mesma transação do registro sendo salvo. Sem isso, você lê o banco "de fora" e não enxerga o que ainda não foi confirmado.
- **`is_ValueChanged`** evita que a regra dispare em todo salvamento. Aqui ela só roda quando alguém marca Concluída.

O nome segue o costume do core: a classe M tira o prefixo da tabela (`X_C_Order` → `MOrder`). Uma M por tabela é o normal; a `EDU_VisitaLinha` não precisa de uma por enquanto.

### 8. Teste

O Eclipse recompila ao salvar e, em modo Debug, costuma aplicar a mudança sem reiniciar. Para uma classe nova e uma factory nova, **reinicie o servidor**.

1. Como GardenAdmin, crie uma visita, **sem linhas**, e salve.
2. Marque **Concluída** e salve. A mensagem da sua regra aparece e o registro não é gravado.
3. Desmarque, inclua uma linha, volte ao cabeçalho, marque **Concluída** e salve. Agora grava.

Por fim, coloque um breakpoint na primeira linha do `beforeSave` e salve qualquer visita. O Eclipse para ali. Inspecione `this`: é um `MVisita`, não um `GenericPO`. A factory está funcionando.

### 9. Commit

```bash
cd ~/sources/guia-idempiere-projeto
git add .
git commit -m "Plugin com.gardenworld.visitas: 2Pack, classes de modelo e regra de conclusão"
```

Não versione a pasta `bin/` nem `target/`: crie um `.gitignore` com as duas.

## Checkpoint

- [ ] O plugin aparece como ACTIVE no console OSGi
- [ ] O pacote 1.0.0 do plugin aparece como instalado na aba Package Installation da janela Pack In
- [ ] As classes I_ e X_ das duas tabelas foram geradas dentro do plugin
- [ ] A classe MVisita compila sem erros
- [ ] Tentar concluir uma visita sem linhas mostra o erro da regra
- [ ] Um breakpoint no beforeSave para a execução quando salvo uma visita

## Erros comuns

| Sintoma | Causa provável | Correção |
|---|---|---|
| `ss` mostra o plugin como `INSTALLED` | Dependência não resolvida | `diag <id>` no console mostra qual; confira o `Require-Bundle` |
| O plugin nem aparece no `ss` | Não foi marcado no `server.product` | Passo 4 |
| `ACTIVE`, mas o 2Pack não foi aplicado | O zip não está em `META-INF/` ou o nome não segue `2Pack_x.y.z.zip` | Confira o nome e o local; veja o log do servidor procurando `com.gardenworld.visitas` |
| O Generate Model termina sem gerar nada | `DB Table Name` ou `Table Entity Type` errado; o filtro diferencia maiúsculas | Use `EDU_%` e `EDU`, exatamente |
| `Source folder doesn't exists` | Caminho relativo, com `~` ou com erro de digitação | Use o caminho absoluto da pasta `src` |
| A regra não dispara e o breakpoint não para | A factory não foi registrada | Confira `OSGI-INF/` e o `Service-Component` no manifesto; confira o Auto-Start |
| A regra dispara em todo salvamento | Faltou `is_ValueChanged` | Passo 7 |

## Desafio

No módulo 02, a Mandatory Logic obriga o **Motivo da Urgência** quando a visita é urgente. Mas essa lógica é da tela: uma visita criada por um processo, uma importação ou uma API passa direto por ela.

Repita a regra no `beforeSave` da `MVisita`, para que valha em qualquer caminho. Dica: `isEDU_IsUrgente()` e `getEDU_MotivoUrgencia()`.

## Referência

### Eventos da classe de modelo

| Método | Quando roda | Uso típico |
|---|---|---|
| `beforeSave(boolean newRecord)` | Antes do `INSERT`/`UPDATE` | Validar e completar campos. `false` barra o salvamento |
| `afterSave(boolean newRecord, boolean success)` | Depois do `INSERT`/`UPDATE`, na mesma transação | Atualizar registros relacionados |
| `beforeDelete()` | Antes do `DELETE` | Impedir exclusão |
| `afterDelete(boolean success)` | Depois do `DELETE` | Limpar registros relacionados |

Os quatro rodam **antes** dos event handlers de outros plugins (módulo 05). Regra da própria tabela fica na M; reação a tabelas de outros (inclusive do core) fica em event handler.

### Métodos úteis de `PO`

| Método | Para quê |
|---|---|
| `is_ValueChanged(coluna)` | A coluna mudou neste salvamento? |
| `get_ValueOld(coluna)` | Valor antes da mudança |
| `is_new()` | Registro ainda não gravado |
| `get_TrxName()` | Transação atual, para repassar a consultas e a outros objetos |
| `saveEx()` | Salva e lança exceção em caso de erro (prefira a `save()` no seu código) |

### Alternativa: o model.generator do Eclipse

O workspace do iDempiere tem uma configuração de execução **model.generator** (**Run > Run Configurations > Eclipse Application**) que gera as mesmas classes sem subir o servidor. O processo **Generate Model**, usado no passo 5, existe desde o iDempiere 11 e dispensa essa configuração.

### Leitura oficial

- [Plugin: Model Factory](https://docs.idempiere.org/docs/basic-development/plugin-development/plugin-modelfactory) (em inglês)

## Próximo módulo

[05 · Regras de negócio: callout e event handler](https://muriloht.com/idempiere/05-callout-e-event-handler): reagir ao que o usuário digita, antes de salvar, e ao que outros plugins e o core gravam.
