Nível 1 · Código · Módulo 06
Processos
Duração estimada: 2h a 3h
Você vai sair sabendo
- →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
Checkpoint: 0 de 6
Antes de começar
- Módulo 05 concluído: a visita tem endereço, e a
MVisitapreenche o endereço padrão nobeforeSave. - 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.
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:
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:
@Processmarca a classe para a factory do próximo passo encontrar.@Parameterpreenche o campo com o parâmetro de mesmo nome, antes doprepare(). O prefixop_é opcional; o nome depois dele é o DB Column Name do passo 2. Parâmetro vazio chega como0(número) ounull(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
MVisitacuida dele nobeforeSave. saveEx()e nãosave(): se algo der errado, a exceção interrompe o processo em vez de seguir em silêncio.addBufferLoggrava 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:
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.
7. Teste
Reinicie o servidor (factory nova). Como GardenAdmin:
- Conclua ou apague as visitas de teste dos módulos anteriores, para ver o efeito com clareza.
- Abra Gerar Visitas de Manutenção no menu. A data e o técnico já vêm preenchidos.
- 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.
- Clique numa linha do log: a visita abre na janela Visita Técnica, com tipo Manutenção e endereço preenchido.
- Rode de novo. O resultado é
0: todos os clientes já têm visita em aberto. - Coloque um breakpoint no
saveEx()e rode com um grupo de clientes para acompanhar uma visita sendo criada. Siga com Step Into até obeforeSavedaMVisita.
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 |
cd ~/sources/guia-idempiere-projeto
git add .
git commit -m "Visitas 1.0.2: processo de geração de visitas de manutenção"
Checkpoint
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:
- Cadastre o processo sem parâmetros.
- Adicione-o como botão na aba Visita da janela Visita Técnica (aba Toolbar Button da janela Window, Tab and Field, com Action
Processe o processo escolhido). - No Java, pegue a visita com
getRecord_ID(), marque como concluída e salve comsaveEx().
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
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: visitas por técnico e por período, com views e Jasper.