# 08 · Consulta e navegação

Fonte: https://muriloht.com/idempiere/08-consulta-e-navegacao (Guia iDempiere, Murilo H. Torquato). Versão alvo: iDempiere 14.

Objetivos:
- Criar uma Info Window para buscar visitas
- Tirar os textos do código e colocá-los em mensagens traduzíveis
- Tornar uma regra configurável por empresa, com o System Configurator
- Criar índices pelo Dicionário, para que viajem no 2Pack
- Mostrar informação de contexto numa linha de status
- Ligar e consultar o histórico de alterações de uma tabela

## Antes de começar

- Módulo 07 concluído, com visitas de vários clientes e técnicos no banco.
- Este módulo é quase todo Dicionário. O Java aparece só no passo 3, para usar mensagens e configuração.

## O que você vai construir

Com dezenas de visitas, a janela **Visita Técnica** deixa de ser o melhor jeito de encontrar alguma coisa. Neste módulo, você cuida de quem **consulta**:

- Uma **Info Window**: busca rápida de visitas, com filtros, que também serve de lookup.
- **Mensagens** no Dicionário: os erros do plugin passam a ser traduzíveis e editáveis sem recompilar.
- Uma **configuração** para a regra "visita concluída precisa de linha", que cada empresa pode ligar ou desligar.
- Um **índice** para as consultas que o plugin faz o tempo todo.
- Uma **linha de status** no cadastro do cliente: "3 visitas técnicas em aberto".
- O **histórico de alterações** das visitas.

## Passo a passo

### 1. A Info Window

**Conceito:** uma **Info Window** é uma tela de busca: filtros em cima, resultado em grade embaixo. Não edita nada. Serve para o usuário encontrar registros e, a partir deles, abrir a janela ou rodar um processo.

Como **System**, na janela **Info Window**, crie:

| Campo | Valor |
|---|---|
| Name | `Visitas Técnicas` |
| Table | `EDU_Visita` |
| Sql FROM | `EDU_Visita v INNER JOIN C_BPartner bp ON (bp.C_BPartner_ID=v.C_BPartner_ID)` |
| Sql ORDER BY | `v.EDU_DataVisita DESC` |
| Entity Type | `EDU` |
| Window | `Visita Técnica` |
| Default | marcado |

O campo **Window** é o destino do zoom: o duplo clique numa linha abre a visita nessa janela. **Default** faz desta a Info Window padrão da tabela `EDU_Visita`.

Na aba **Column**, crie as colunas. **Sql SELECT** é a expressão, com o alias do `FROM`; **Query Criteria** marca as que viram filtro:

| Seq | Name | DB Column Name | Sql SELECT | Reference | Query Criteria | Detalhes |
|---|---|---|---|---|---|---|
| 10 | Visita | `EDU_Visita_ID` | `v.EDU_Visita_ID` | ID | | **Key column** marcado, **Displayed** desmarcado |
| 20 | Data | `EDU_DataVisita` | `v.EDU_DataVisita` | Date | sim | **Range** marcado |
| 30 | Cliente | `C_BPartner_ID` | `v.C_BPartner_ID` | Search | sim | Operador `=` |
| 40 | Nome do cliente | `Name` | `bp.Name` | String | sim | Operador `Full Like` |
| 50 | Técnico | `SalesRep_ID` | `v.SalesRep_ID` | Table | sim | Reference Key: `AD_User - Internal` |
| 60 | Tipo | `EDU_TipoVisita` | `v.EDU_TipoVisita` | List | sim | Reference Key: `EDU_TipoVisita` |
| 70 | Concluída | `EDU_IsConcluida` | `v.EDU_IsConcluida` | Yes-No | sim | Operador `=` |

Salve e clique em **Validate**, na aba **Window**. O iDempiere monta o SQL e testa no banco; se estiver certo, marca **Valid**. Uma Info Window inválida não abre.

Crie a entrada de menu com **Action** `Info` e **Info Window** `Visitas Técnicas`, e rode **Role Access Update** como GardenAdmin.

Teste como GardenAdmin: busque as visitas em aberto de um cliente, depois as de um técnico no mês. Dê duplo clique numa linha: a visita abre na janela.

> Por ser a padrão da tabela, a mesma Info Window aparece quando o usuário busca uma visita num campo do tipo Search que aponta para `EDU_Visita`. Uma Info Window por tabela importante é uma das formas mais baratas de melhorar a vida do usuário.

### 2. Mensagens no Dicionário

Hoje o plugin tem dois textos em português escritos no Java. Isso tem três problemas: não dá para traduzir, não dá para corrigir sem recompilar e ninguém encontra o texto procurando no Dicionário.

Na janela **Message**, crie:

| Search Key | Message Type | Message Text |
|---|---|---|
| `EDU_VisitaSemLinhas` | Error | `Inclua pelo menos uma linha antes de concluir a visita.` |
| `EDU_ClienteComVisitaAberta` | Error | `Este cliente tem visitas técnicas em aberto. Conclua as visitas antes de desativá-lo.` |
| `EDU_VisitasEmAberto` | Information | `Visitas técnicas em aberto: {0}` |

Entity Type `EDU` em todas. A terceira é para o passo 5. O `{0}` é um marcador do `MessageFormat` do Java.

> A aba **Translation** guarda a mesma mensagem em outros idiomas. O iDempiere mostra a tradução do idioma de login, quando existe.

### 3. Uma regra configurável

**Conceito:** o **System Configurator** guarda parâmetros que mudam o comportamento do sistema sem mexer no código. Cada parâmetro tem um nível: pode valer para o sistema todo, ou ser sobrescrito por empresa ou por organização.

Na janela **System Configurator**, crie:

| Campo | Valor |
|---|---|
| Name | `EDU_VISITA_EXIGE_LINHA` |
| Description | `Y: só conclui visita com pelo menos uma linha. N: permite concluir sem linhas.` |
| Configured Value | `Y` |
| Configuration Level | `Client` |
| Entity Type | `EDU` |

Com o nível `Client`, a GardenWorld pode ter o próprio registro com outro valor, e ele vale só para ela.

Agora o Java. Na `MVisita`, troque a regra de conclusão:

```java
public static final String SYSCONFIG_EXIGE_LINHA = "EDU_VISITA_EXIGE_LINHA";

@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)
            && MSysConfig.getBooleanValue(SYSCONFIG_EXIGE_LINHA, true, getAD_Client_ID())
            && getLinhas().isEmpty()) {
        log.saveError("Error", Msg.getMsg(getCtx(), "EDU_VisitaSemLinhas"));
        return false;
    }
    return true;
}
```

Imports: `org.compiere.model.MSysConfig` e `org.compiere.util.Msg`.

E no `ClienteComVisitaAberta`, do módulo 05:

```java
if (temVisitaAberta)
    throw new AdempiereException(Msg.getMsg(cliente.getCtx(), "EDU_ClienteComVisitaAberta"));
```

Dois detalhes:

- **O valor padrão do `getBooleanValue` é `true`**: se alguém apagar a configuração, a regra continua valendo. Escolha o padrão mais seguro.
- **O nome da configuração vira constante.** Ele é usado no código e digitado no Dicionário; uma constante evita que os dois se desencontrem.

Reinicie o servidor e teste: com `Y`, a regra funciona como antes. Mude o valor para `N`, conclua uma visita sem linhas e veja passar. O valor é lido em cache: se a mudança não aparecer, use **Cache Reset** no menu. Volte para `Y` no fim.

### 4. Índices pelo Dicionário

O plugin consulta visitas por cliente e situação em três lugares: o event handler do módulo 05, o processo do módulo 06 e a linha de status do próximo passo. Com poucas visitas, tanto faz. Com cem mil, é a diferença entre instantâneo e travado.

Como **System**, na janela **Table and Column**, tabela `EDU_Visita`, aba **Table Index**:

| Campo | Valor |
|---|---|
| Name | `EDU_Visita_Cliente` |
| Unique | desmarcado |
| Entity Type | `EDU` |

Na aba **Index Column**:

| Sequence | Column |
|---|---|
| 10 | `C_BPartner_ID` |
| 20 | `EDU_IsConcluida` |

Volte à aba **Table Index** e clique em **Index Validate**. Confira no `psql`:

```sql
\d edu_visita
```

Pelo Dicionário, e não com `CREATE INDEX` direto no banco, pelo mesmo motivo da view do módulo 07: o índice vai no 2Pack junto com a tabela.

> Marcando **Unique**, o índice vira uma regra: o banco recusa duplicados. Com **Message** preenchido, o usuário vê a sua mensagem em vez do erro do PostgreSQL.

### 5. A linha de status no cliente

**Conceito:** uma **Status Line** executa uma consulta e mostra o resultado formatado por uma mensagem. Pode aparecer na barra de status de uma janela ou no painel de ajuda ao lado.

Na janela **Status Line**, crie:

| Campo | Valor |
|---|---|
| Name | `EDU Visitas em aberto do cliente` |
| Message | `EDU_VisitasEmAberto` |
| SQL Expression/Statement | `SELECT COUNT(*) FROM EDU_Visita WHERE C_BPartner_ID=@C_BPartner_ID@ AND EDU_IsConcluida='N' AND IsActive='Y'` |
| Entity Type | `EDU` |

Na aba **Used In**:

| Campo | Valor |
|---|---|
| Window | `Business Partner` |
| Tab | `Business Partner` |
| Status Line | marcado |

O `@C_BPartner_ID@` é lido do contexto da janela, como nas lógicas do módulo 02. O resultado entra no lugar do `{0}` da mensagem.

Teste como GardenAdmin: abra **Business Partner**, navegue até um cliente com visitas e veja a contagem na barra de status. Mude de cliente e ela muda junto.

> Com **Status Line** desmarcado, a informação aparece no painel de ajuda da janela, como um "quick info". Use a barra de status para um número; o painel para algo maior.

### 6. Histórico de alterações

**Conceito:** o iDempiere pode registrar cada mudança de valor, com usuário, data, valor antigo e novo. Vem desligado por padrão, tabela por tabela, porque ocupa espaço.

Na tabela `EDU_Visita`, marque **Maintain Change Log** e salve.

Como GardenAdmin:

1. Abra uma visita, mude o **Tipo** e salve.
2. Clique com o botão direito no campo **Tipo** e escolha **Change Log**. Aparece a mudança, com quem fez e quando.
3. Para ver tudo o que mudou no sistema, use a janela **Change Audit**.

> A opção do botão direito só aparece para perfis com **Show Change Log** marcado (o GardenWorld Admin tem). Ligue o log só nas tabelas em que alguém vai precisar dele: é uma linha de `AD_ChangeLog` por campo alterado.

### 7. Commit

Inclua no Pack Out a Info Window (tipo **Info Window**), as três mensagens (tipo **Message**) e o item de menu. A tabela `EDU_Visita` entra de novo, porque ganhou índice e **Maintain Change Log**.

A Status Line e a configuração não têm tipo próprio: vão como **Data**, com a tabela e uma consulta em **SQL Expression/Statement**:

| Type | Table | SQL Expression/Statement |
|---|---|---|
| Data | `AD_StatusLine` | `SELECT * FROM AD_StatusLine WHERE EntityType='EDU'` |
| Data | `AD_StatusLineUsedIn` | `SELECT * FROM AD_StatusLineUsedIn WHERE EntityType='EDU'` |
| Data | `AD_SysConfig` | `SELECT * FROM AD_SysConfig WHERE Name='EDU_VISITA_EXIGE_LINHA'` |

Gere o `2Pack_1.0.4.zip`.

```bash
git add .
git commit -m "Visitas 1.0.4: info window, mensagens, configuração, índice, status line e change log"
```

## Checkpoint

- [ ] A Info Window Visitas Técnicas busca por cliente, técnico, período e situação
- [ ] Clicar numa visita da Info Window abre a janela Visita Técnica
- [ ] As mensagens de erro vêm da janela Message, não do código
- [ ] Com EDU_VISITA_EXIGE_LINHA = N, consigo concluir visita sem linhas
- [ ] O índice EDU_Visita_Cliente existe no banco
- [ ] A janela Business Partner mostra quantas visitas o cliente tem em aberto
- [ ] Vejo quem mudou o tipo de uma visita, e quando

## Erros comuns

| Sintoma | Causa provável | Correção |
|---|---|---|
| A Info Window não abre, ou não aparece no menu | **Valid** desmarcado, ou faltou **Role Access Update** | Clique em **Validate** e leia o erro; rode o processo de acesso |
| **Validate** acusa erro de SQL | Alias do `Sql SELECT` diferente do `Sql FROM` | Use `v.` e `bp.` como no `FROM` |
| Duplo clique não abre a visita | Faltou **Window** na Info Window ou **Key column** na coluna do ID | Passo 1 |
| A mensagem aparece como `EDU_VisitaSemLinhas` | A mensagem não existe ou a Search Key está diferente | Confira a Search Key, letra por letra |
| Mudei a configuração e nada aconteceu | Valor em cache | **Cache Reset** |
| **Index Validate** falha | Nome de índice já existe ou coluna errada | Confira no `\d edu_visita` |
| A linha de status não aparece | **Status Line** desmarcado em Used In, ou Window/Tab errados | Passo 5 |
| Não aparece **Change Log** no botão direito | Perfil sem **Show Change Log**, ou tabela sem **Maintain Change Log** | Passo 6 |

## Desafio

Adicione à Info Window um processo: **Gerar Visitas de Manutenção** (módulo 06), na aba **Process** da Info Window. Depois, faça a linha de status mostrar também a data da próxima visita em aberto do cliente (dica: `{1}` na mensagem e uma segunda coluna no `SELECT`).

## Referência

### Níveis do System Configurator

| Configuration Level | Quem pode sobrescrever |
|---|---|
| System | Ninguém: vale o registro do System |
| Client | Cada empresa, com um registro próprio |
| Organization | Cada organização, com um registro próprio |

No código, o método recebe o nível mais específico que você conhece: `getBooleanValue(nome, padrão)`, `getBooleanValue(nome, padrão, AD_Client_ID)` ou `getBooleanValue(nome, padrão, AD_Client_ID, AD_Org_ID)`. Existem as mesmas variações para `getValue`, `getIntValue` e `getBigDecimalValue`.

### Mensagens no código

| Situação | Como |
|---|---|
| Texto simples | `Msg.getMsg(ctx, "Chave")` |
| Com valores | `Msg.getMsg(ctx, "Chave", new Object[] {valor})`, com `{0}`, `{1}` na mensagem |
| Em exceção | `new AdempiereException("@Chave@")`: o texto entre arrobas é traduzido ao exibir |
| Em `beforeSave` | `log.saveError("Error", Msg.getMsg(getCtx(), "Chave"))` |

### Operadores da Info Column

`=`, `!=`, `>`, `>=`, `<`, `<=`, `Like` (começa com) e `Full Like` (contém). Para ignorar maiúsculas, use `UPPER(bp.Name)` no **Sql SELECT**. Colunas com **Range** pedem "de" e "até".

## Próximo módulo

[09 · Documentos e aprovação](https://muriloht.com/idempiere/09-documentos-e-aprovacao): a visita vira um documento, com número, status, completar, anular e aprovação.
