# 06 · Processos

Fonte: https://muriloht.com/idempiere/06-processos (Guia iDempiere, Murilo H. Torquato). Versão alvo: iDempiere 14.

Objetivos:
- Cadastrar um processo com parâmetros no Dicionário
- Escrever o processo em Java e registrá-lo por anotação
- Entender a transação de um processo: tudo ou nada
- Deixar um log que leva o usuário aos registros criados
- Chamar um processo a partir do código

## Antes de começar

- Módulo 05 concluído: a visita tem endereço, e a `MVisita` preenche o endereço padrão no `beforeSave`.
- Servidor em modo Debug.

## O que você vai construir

A GardenWorld quer visitar todos os clientes periodicamente. Hoje isso significaria criar uma visita por vez, à mão.

Você vai criar o processo **Gerar Visitas de Manutenção**: o usuário escolhe a data, o técnico e, se quiser, um grupo de clientes. O processo cria uma visita de manutenção para cada cliente que ainda não tem visita em aberto, e mostra no fim a lista do que criou.

É o primeiro código do guia que **cria** registros em vez de reagir a eles. E é aqui que as regras dos módulos 04 e 05 se pagam: como o processo usa a `MVisita`, elas valem sem nenhuma linha a mais.

## O que é um processo

Um processo é uma ação que o usuário dispara: pelo menu, por um botão na janela ou por agendamento. No iDempiere ele tem duas metades:

| Metade | Onde fica | O que define |
|---|---|---|
| **Dicionário** | Janela **Report and Process** | Nome, parâmetros, quem pode rodar, onde aparece |
| **Java** | Classe que herda de `SvrProcess` | O que acontece quando roda |

A tela de parâmetros, a execução em segundo plano, o log e o controle de acesso vêm prontos. Você escreve só a regra.

## Passo a passo

### 1. Cadastre o processo

Como **System**, abra a janela **Report and Process** e crie:

| Campo | Valor |
|---|---|
| Search Key | `EDU_GerarVisitasManutencao` |
| Name | `Gerar Visitas de Manutenção` |
| Data Access Level | `Client+Organization` |
| Entity Type | `EDU` |
| Report | desmarcado |
| Classname | `com.gardenworld.visitas.process.GerarVisitasManutencao` |

O **Classname** é o elo entre as duas metades: precisa ser exatamente o nome completo da classe que você vai criar.

### 2. Os parâmetros

Na aba **Parameter**, crie:

| Seq | Name | DB Column Name | Reference | Detalhes |
|---|---|---|---|---|
| 10 | Data da Visita | `EDU_DataVisita` | Date | Mandatory. Default Logic: `@#Date@` |
| 20 | Técnico | `SalesRep_ID` | Table | Reference Key: `AD_User - Internal`. Mandatory. Default Logic: `@#AD_User_ID@` |
| 30 | Grupo de Clientes | `C_BP_Group_ID` | Table Direct | Opcional |

Em cada parâmetro, escolha em **System Element** o elemento com o mesmo nome da coluna. É o mesmo raciocínio das colunas do módulo 01: o elemento dá o significado, e a referência dá o tipo e o componente de tela.

> O **DB Column Name** do parâmetro não precisa existir em tabela nenhuma. Ele é o nome com que o parâmetro chega ao Java. Usar o nome de uma coluna que já existe só facilita: o elemento e a ajuda vêm prontos.

### 3. Menu e acesso

Na janela **Menu**, crie uma entrada com **Action** `Process` e **Process** `Gerar Visitas de Manutenção`, arraste para perto da **Visita Técnica** na árvore e salve.

Como **GardenAdmin**, rode **Role Access Update**, como no módulo 01.

### 4. A classe do processo

Crie `com.gardenworld.visitas.process.GerarVisitasManutencao`:

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

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;

import org.adempiere.base.annotation.Parameter;
import org.adempiere.base.annotation.Process;
import org.compiere.model.MBPartner;
import org.compiere.model.Query;
import org.compiere.process.SvrProcess;

import com.gardenworld.visitas.model.MVisita;

@Process
public class GerarVisitasManutencao extends SvrProcess {

    @Parameter
    private Timestamp p_EDU_DataVisita;

    @Parameter
    private int p_SalesRep_ID;

    @Parameter
    private int p_C_BP_Group_ID;

    @Override
    protected void prepare() {
        // os parâmetros já chegam preenchidos pelas anotações @Parameter
    }

    @Override
    protected String doIt() throws Exception {
        StringBuilder where = new StringBuilder("IsCustomer='Y'")
            .append(" AND NOT EXISTS (SELECT 1 FROM EDU_Visita v")
            .append("  WHERE v.C_BPartner_ID=C_BPartner.C_BPartner_ID AND v.EDU_IsConcluida='N')");
        List<Object> params = new ArrayList<>();
        if (p_C_BP_Group_ID > 0) {
            where.append(" AND C_BP_Group_ID=?");
            params.add(p_C_BP_Group_ID);
        }

        List<MBPartner> clientes = new Query(getCtx(), MBPartner.Table_Name, where.toString(), get_TrxName())
            .setParameters(params)
            .setClient_ID()
            .setOnlyActiveRecords(true)
            .setOrderBy(MBPartner.COLUMNNAME_Name)
            .list();

        for (MBPartner cliente : clientes) {
            MVisita visita = new MVisita(getCtx(), 0, get_TrxName());
            visita.setC_BPartner_ID(cliente.getC_BPartner_ID());
            visita.setSalesRep_ID(p_SalesRep_ID);
            visita.setEDU_DataVisita(p_EDU_DataVisita);
            visita.setEDU_TipoVisita("MA"); // Manutenção
            visita.setEDU_IsUrgente(false);
            visita.setEDU_IsConcluida(false);
            visita.setDescription("Manutenção periódica");
            visita.saveEx();

            addBufferLog(visita.getEDU_Visita_ID(), p_EDU_DataVisita, null,
                cliente.getName(), MVisita.Table_ID, visita.getEDU_Visita_ID());
        }

        return "@Created@ = " + clientes.size();
    }
}
```

Linha por linha, o que importa:

- **`@Process`** marca a classe para a factory do próximo passo encontrar.
- **`@Parameter`** preenche o campo com o parâmetro de mesmo nome, antes do `prepare()`. O prefixo `p_` é opcional; o nome depois dele é o **DB Column Name** do passo 2. Parâmetro vazio chega como `0` (número) ou `null` (data, texto).
- **`NOT EXISTS`** é o que torna o processo seguro para rodar de novo: cliente com visita em aberto fica de fora.
- **`setClient_ID()`** filtra pelo cliente logado (GardenWorld). Sem isso, a consulta pegaria parceiros de todos os clientes do banco.
- **Tudo é preenchido explicitamente.** A Default Logic do Dicionário é aplicada pela **tela**. Um objeto criado em Java nasce só com os campos padrão (cliente, organização, ativo). O endereço é a exceção, porque a `MVisita` cuida dele no `beforeSave`.
- **`saveEx()`** e não `save()`: se algo der errado, a exceção interrompe o processo em vez de seguir em silêncio.
- **`addBufferLog`** grava uma linha de log com a tabela e o registro. É isso que torna a linha clicável.
- **`@Created@`** entre arroba é traduzido pelo iDempiere para o idioma do usuário.

### 5. A factory de processos

Crie `com.gardenworld.visitas.VisitasProcessFactory`, a terceira factory do plugin:

```java
package com.gardenworld.visitas;

import org.adempiere.base.AnnotationBasedProcessFactory;
import org.adempiere.base.IProcessFactory;
import org.osgi.service.component.annotations.Component;

@Component(immediate = true, service = IProcessFactory.class,
           property = {"service.ranking:Integer=1"})
public class VisitasProcessFactory extends AnnotationBasedProcessFactory {

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

A essa altura o padrão deve estar claro: **uma factory por tipo de extensão, cada uma apontando para um pacote do plugin.** Modelo, callout, evento e processo seguem a mesma ideia.

### 6. A transação: tudo ou nada

Um processo roda dentro de **uma** transação, e o `get_TrxName()` é ela. O iDempiere faz o commit quando o `doIt()` termina sem erro, e o rollback se ele lançar uma exceção.

Consequência prática: se o processo criar 30 visitas e falhar na 31ª, **nenhuma** fica gravada. Para uma geração em lote, é o que você quer: rodar de novo não deixa metade feita.

> Se um dia precisar do contrário (gravar o que deu certo e seguir), faça `commitEx()` a cada registro e trate o erro de cada um. É exceção, não regra: use só quando cada registro for independente e o log mostrar claramente o que falhou.

### 7. Teste

Reinicie o servidor (factory nova). Como GardenAdmin:

1. Conclua ou apague as visitas de teste dos módulos anteriores, para ver o efeito com clareza.
2. Abra **Gerar Visitas de Manutenção** no menu. A data e o técnico já vêm preenchidos.
3. Rode sem grupo. O resultado mostra quantas visitas foram criadas e uma linha por cliente. O GardenWorld vem com 7 clientes ativos: sem visitas abertas, são 7 visitas.
4. Clique numa linha do log: a visita abre na janela **Visita Técnica**, com tipo Manutenção e endereço preenchido.
5. Rode de novo. O resultado é `0`: todos os clientes já têm visita em aberto.
6. Coloque um breakpoint no `saveEx()` e rode com um grupo de clientes para acompanhar uma visita sendo criada. Siga com **Step Into** até o `beforeSave` da `MVisita`.

### 8. Commit

O Dicionário mudou (processo, parâmetros e menu). Inclua o processo no Pack Out e gere o `2Pack_1.0.2.zip`:

| Seq | Type | Objeto |
|---|---|---|
| 80 | Process/Report | `Gerar Visitas de Manutenção` |
| 90 | Application or Module | o item de menu `Gerar Visitas de Manutenção` |

```bash
cd ~/sources/guia-idempiere-projeto
git add .
git commit -m "Visitas 1.0.2: processo de geração de visitas de manutenção"
```

## Checkpoint

- [ ] O processo Gerar Visitas de Manutenção aparece no menu do GardenAdmin
- [ ] A tela de parâmetros abre com a data de hoje e o usuário logado como técnico
- [ ] Rodar o processo cria uma visita de manutenção para cada cliente sem visita em aberto
- [ ] As visitas criadas já vêm com endereço, graças ao beforeSave do módulo 05
- [ ] Clico numa linha do log e a visita abre
- [ ] Rodar de novo, no mesmo dia, não cria visitas duplicadas

## Erros comuns

| Sintoma | Causa provável | Correção |
|---|---|---|
| `ClassNotFoundException` ou "processo não encontrado" ao rodar | **Classname** diferente do nome da classe, ou factory não registrada | Compare letra por letra; confira `OSGI-INF/` e reinicie |
| Parâmetro sempre `0` ou `null` | Nome do campo não bate com o **DB Column Name** | `p_` + DB Column Name, ou exatamente o DB Column Name |
| O processo não aparece no menu | Faltou o **Role Access Update**, ou o Data Access Level não permite o perfil | Rode o processo como GardenAdmin; confira o nível |
| Cria visitas de clientes de outra empresa | Faltou `setClient_ID()` | Passo 4 |
| Erro de coluna obrigatória ao salvar | Algum campo obrigatório não foi preenchido no código | Preencha tudo explicitamente; Default Logic é só da tela |
| Rodar duas vezes duplica visitas | Faltou o `NOT EXISTS` | Passo 4 |

## Desafio

Crie um segundo processo, **Concluir Visita**, que roda a partir da própria janela, sobre a visita aberta, em vez do menu:

1. Cadastre o processo sem parâmetros.
2. Adicione-o como botão na aba **Visita** da janela **Visita Técnica** (aba **Toolbar Button** da janela **Window, Tab and Field**, com **Action** `Process` e o processo escolhido).
3. No Java, pegue a visita com `getRecord_ID()`, marque como concluída e salve com `saveEx()`.

Teste numa visita sem linhas. A regra do módulo 04 barra a conclusão, e a mensagem chega ao usuário, sem nenhum código a mais no processo.

## Referência

### Campos importantes do processo

| Campo | Para quê |
|---|---|
| Data Access Level | Quais perfis podem rodar (mesma lógica da tabela) |
| Classname | Classe Java do processo |
| Report | Marcado quando o processo gera um relatório (módulo 07) |
| Allow Concurrent Execution | Controla se o processo pode ser disparado de novo enquanto uma execução anterior ainda está rodando |
| Show Help | Mostra ou não a ajuda na tela de parâmetros |

### Parâmetros especiais

| Recurso | Como |
|---|---|
| Intervalo (de/até) | Marque **Range** no parâmetro; declare um segundo campo com sufixo `_To` (`p_DateDoc` e `p_DateDoc_To`) |
| Valor padrão | **Default Logic**, igual à coluna (módulo 02) |
| Lógica de exibição | **Display Logic** no parâmetro, com `@NomeDoParametro@` |
| Ler sem anotação | `for (ProcessInfoParameter p : getParameter())` no `prepare()`, como no código do core |

### Chamando um processo pelo código

```java
int processId = MProcess.getProcess_ID("EDU_GerarVisitasManutencao", null);
ProcessInfo pi = new ProcessInfo("Gerar Visitas de Manutenção", processId);
pi.setAD_Client_ID(Env.getAD_Client_ID(getCtx()));
pi.setAD_User_ID(Env.getAD_User_ID(getCtx()));
pi.setParameter(new ProcessInfoParameter[] {
    new ProcessInfoParameter("EDU_DataVisita", dataVisita, null, null, null),
    new ProcessInfoParameter("SalesRep_ID", tecnicoId, null, null, null)
});
ServerProcessCtl.process(pi, null);
if (pi.isError())
    throw new AdempiereException(pi.getSummary());
```

Com `null` como transação, o processo chamado roda na transação dele e faz o próprio commit. Busque sempre pelo **Search Key**, nunca pelo ID: o ID muda de um banco para outro, pelo mesmo motivo do módulo 03.

### Agendamento

Um processo pode rodar sozinho, sem usuário, pela janela **Scheduler**: escolha o processo, os parâmetros e a frequência. É o caminho natural para a geração de visitas rodar todo mês.

## Próximo módulo

[07 · Relatórios](https://muriloht.com/idempiere/07-relatorios): visitas por técnico e por período, com views e Jasper.
