# 01 · Seu primeiro CRUD sem código

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

Objetivos:
- Entender o que é o Dicionário da Aplicação e por que ele existe
- Criar tabelas e colunas no banco sem escrever SQL
- Escolher a referência certa para cada tipo de campo
- Montar uma janela com cabeçalho e linhas e publicá-la no menu

## Antes de começar

- Módulo 00 concluído: servidor rodando e login como `System` funcionando.
- Deixe o `psql` aberto num terminal, conectado ao banco. Você vai conferir no banco cada coisa que criar na tela:

```bash
psql -h localhost -U adempiere idempiere
```

## O que você vai construir

A primeira versão do projeto que acompanha todo o guia: o controle de **visitas técnicas** da GardenWorld. Uma janela com:

- **Visita** (cabeçalho): cliente, técnico responsável, data, tipo de visita e se é urgente
- **Linhas**: produtos usados na visita, com quantidade

Sem nenhuma linha de Java. Nos próximos módulos, o mesmo projeto ganha regras, código, relatórios e fluxo de aprovação.

## O Dicionário da Aplicação, em 1 minuto

No iDempiere, telas, campos, validações e menus não são programados: são **cadastrados**. Tudo isso fica em tabelas do próprio banco, com prefixo `AD_` (de *Application Dictionary*). Quando você abre uma janela, o iDempiere lê essas definições e monta a tela na hora.

Consequências práticas:

- Um CRUD completo (listar, filtrar, criar, editar, excluir, anexar, auditar, exportar) sai de graça ao cadastrar a janela.
- Mudar um rótulo ou esconder um campo não exige compilar nem reiniciar.
- O código Java fica reservado para as regras de negócio, que vêm a partir do módulo 04.

O Dicionário só pode ser alterado no perfil **System**. Entre com `System` / `System` e escolha o perfil **System Administrator**.

> **Ligue o registro de migração agora.** No menu do seu usuário (canto superior direito), abra **Preference** e marque **Log Migration Script**. Salve. A partir daqui, o iDempiere grava um arquivo `.sql` com tudo o que você alterar no Dicionário. É assim que as suas mudanças chegam em outro ambiente, e o módulo 03 trata disso. Se esquecer agora, vai ter de refazer o trabalho depois.
>
> Essa opção só aparece para os usuários **System** e **SuperUser**. Rodando pelo Eclipse, os arquivos vão para `migration/iD14/postgresql/` dentro do repositório, com nomes como `202609231530_PlaceholderForTicket.sql`.

## Passo a passo

### 1. Tipo de entidade: diga de quem é a customização

**Conceito:** todo registro do Dicionário tem um **Entity Type** (tipo de entidade). Ele diz a quem aquilo pertence: `D` é o core do iDempiere, e cada plugin ou customização deve ter o seu. É o que permite separar o que é seu do que é do core, exportar só o seu trabalho e gerar as classes Java no pacote certo (módulo 04).

Abra a janela **Entity Type** e crie:

| Campo | Valor |
|---|---|
| Entity Type | `EDU` |
| Name | `Guia iDempiere` |

Salve. Se o `EDU` não aparecer nas listas dos próximos passos, saia e entre de novo: a lista de tipos de entidade fica em cache.

> Nunca use `D` nas suas customizações. Um registro com `D` é tratado como core e pode ser sobrescrito na próxima atualização do iDempiere.

### 2. Tabela da visita

**Conceito:** a janela **Table and Column** (Tabela e Coluna) é a definição de uma tabela do banco dentro do Dicionário. Você cadastra a tabela e as colunas ali, e o iDempiere gera o `CREATE TABLE` e o `ALTER TABLE`.

Abra **Table and Column** e crie um registro:

| Campo | Valor |
|---|---|
| DB Table Name | `EDU_Visita` |
| Name | `Visita Técnica` |
| Data Access Level | `Client+Organization` |
| Entity Type | `EDU` |

Salve. Agora clique na engrenagem (processos) e rode **Create/Complete Table**. Na janela de parâmetros, deixe marcado só **Create KeyColumn** e confirme.

Esse processo cadastra a chave primária (`EDU_Visita_ID`), a coluna de UUID (`EDU_Visita_UU`) e as colunas padrão que **toda** tabela do iDempiere tem:

| Coluna | Para quê |
|---|---|
| `AD_Client_ID` | Empresa (tenant) dona do registro |
| `AD_Org_ID` | Organização dentro da empresa |
| `IsActive` | Desativar sem excluir |
| `Created`, `CreatedBy` | Quando e quem criou |
| `Updated`, `UpdatedBy` | Quando e quem alterou por último |

> **Data Access Level** define em que nível os dados vivem. `Client+Organization` serve para dados de negócio comuns. `System` é para configurações globais, que só o perfil System vê.

Ainda não existe nada no banco. Confira no `psql`:

```sql
\d edu_visita
-- Did not find any relation named "edu_visita".
```

O processo só criou a **definição**. A tabela física nasce ao sincronizar a primeira coluna (passo 6).

### 3. Elemento: o significado de um campo

**Conceito:** um **Element** (elemento) define o nome da coluna no banco e o rótulo que o usuário vê, com as traduções. Colunas com o mesmo elemento, em qualquer tabela, têm o mesmo nome e o mesmo rótulo. Por isso, **antes de criar um elemento, procure se o core já tem um**: `C_BPartner_ID`, `Description`, `Qty` e `M_Product_ID` já existem e são reaproveitados.

Na janela **Element**, crie três elementos novos, todos com Entity Type `EDU`:

| DB Column Name | Name | Print Text |
|---|---|---|
| `EDU_DataVisita` | Data da Visita | Data |
| `EDU_TipoVisita` | Tipo de Visita | Tipo |
| `EDU_IsUrgente` | Urgente | Urgente |

Convenção: colunas booleanas começam com `Is`, e colunas que apontam para outra tabela terminam com `_ID`. O iDempiere depende dessa segunda regra: é pelo sufixo `_ID` que ele descobre a tabela referenciada.

### 4. Validação dinâmica: filtrar o que aparece

**Conceito:** uma **Dynamic Validation** é um trecho de `WHERE` aplicado a um campo de referência. Serve para mostrar só os registros que fazem sentido.

No cliente, só interessam parceiros que são clientes. Abra **Validation Rules** (Regras de Validação) e crie:

| Campo | Valor |
|---|---|
| Name | `EDU - Somente Clientes` |
| Type | `SQL` |
| Validation code | `C_BPartner.IsCustomer='Y'` |
| Entity Type | `EDU` |

Você vai usar essa regra na coluna `C_BPartner_ID`, no passo 6.

Para o técnico não é preciso criar nada. O core já tem uma **referência** pronta, `AD_User - Internal`, que lista só usuários ligados a funcionários ou representantes de vendas. Validação dinâmica filtra uma referência; uma referência de tabela já pode vir filtrada de fábrica. Antes de criar, procure o que o core já oferece.

### 5. Lista de valores fixos

**Conceito:** uma referência do tipo **List** tem as opções cadastradas no Dicionário. No banco fica gravado só o código (`IN`); o usuário vê o nome (`Instalação`), já traduzido.

Abra a janela **Reference** e crie:

| Campo | Valor |
|---|---|
| Name | `EDU_TipoVisita` |
| Validation Type | `List Validation` |
| Entity Type | `EDU` |

Na aba **List Validation**, cadastre:

| Search Key | Name |
|---|---|
| `IN` | Instalação |
| `MA` | Manutenção |
| `OR` | Orçamento |

Essa referência vai no campo **Reference Key** da coluna `EDU_TipoVisita`, no próximo passo.

### 6. Colunas da visita e a escolha da referência

**Conceito:** a **Reference** (referência) de uma coluna define duas coisas ao mesmo tempo: o tipo de dado no banco e o componente de tela. É a decisão mais importante ao criar uma coluna.

As referências que você vai usar agora:

| Referência | Tela | Banco | Quando usar |
|---|---|---|---|
| **String** | caixa de texto | `VARCHAR` | texto curto |
| **Date** | calendário | `TIMESTAMP` | datas |
| **Yes-No** | checkbox | `CHAR(1)` Y/N | booleanos |
| **List** | lista suspensa | `VARCHAR` | opções fixas definidas no Dicionário |
| **Table Direct** | lista suspensa | `NUMERIC` (FK) | o nome da coluna é igual ao da tabela + `_ID` (`C_BPartner_ID` → `C_BPartner`) e há poucos registros |
| **Table** | lista suspensa | `NUMERIC` (FK) | o nome da coluna **não** bate com a tabela (`SalesRep_ID` → `AD_User`); o campo *Reference Key* aponta uma referência que diz qual é a tabela |
| **Search** | campo com lupa | `NUMERIC` (FK) | a tabela tem muitos registros; abre uma janela de busca em vez de carregar tudo numa lista |

Na aba **Column** da tabela `EDU_Visita`, crie as colunas abaixo. Depois de salvar **cada uma**, clique em **Synchronize Column**: é esse botão que roda o `ALTER TABLE` no banco (na primeira vez, o `CREATE TABLE`).

| DB Column Name | Reference | Detalhes |
|---|---|---|
| `C_BPartner_ID` | Search | Mandatory. Dynamic Validation: `EDU - Somente Clientes` |
| `SalesRep_ID` | Table | Reference Key: `AD_User - Internal` |
| `EDU_DataVisita` | Date | Mandatory. Default Logic: `@#Date@` |
| `EDU_TipoVisita` | List | Length: `2`. Mandatory. Reference Key: `EDU_TipoVisita` |
| `EDU_IsUrgente` | Yes-No | Default Logic: `N` |
| `Description` | String | Length: `255` |

Em `C_BPartner_ID` e `EDU_DataVisita`, marque também **Identifier** (Sequence `1` e `2`). O identificador é o que aparece quando outra tela aponta para este registro: em vez de `1000001`, o usuário vê `Joe Block_2026-09-23`.

> **Default Logic** é o valor inicial do campo. `@#Date@` é uma variável de contexto (a data de login). O módulo 02 é todo sobre isso.

> **Sempre** defina um valor padrão em colunas Yes-No e em colunas obrigatórias criadas numa tabela que já tem dados. Sem isso, o `ALTER TABLE ... NOT NULL` falha nos registros existentes.

Agora confira no `psql`:

```sql
\d edu_visita
```

A tabela existe, com as colunas padrão e as suas.

### 7. Tabela das linhas

Repita o passo 2 para a tabela `EDU_VisitaLinha` (Name `Linha da Visita`, mesmo Access Level e Entity Type) e rode o **Create/Complete Table** com **Create KeyColumn**.

Crie as colunas:

| DB Column Name | Reference | Detalhes |
|---|---|---|
| `EDU_Visita_ID` | Table Direct | Mandatory. **Parent link column** marcado |
| `Line` | Integer | Mandatory. Default Logic: `@SQL=SELECT COALESCE(MAX(Line),0)+10 FROM EDU_VisitaLinha WHERE EDU_Visita_ID=@EDU_Visita_ID@` |
| `M_Product_ID` | Search | Mandatory. Identifier |
| `Qty` | Quantity | Mandatory. Default Logic: `1` |
| `Description` | String | Length: `255` |

**Parent link column** diz ao iDempiere que esta coluna liga a linha ao cabeçalho. É ela que faz a aba de linhas mostrar só as linhas da visita selecionada.

A Default Logic de `Line` numera as linhas de 10 em 10, como fazem os pedidos do core. Com `@SQL=`, o valor padrão vem de uma consulta.

### 8. A janela

**Conceito:** uma **Window** (janela) tem uma ou mais **Tabs** (abas), e cada aba mostra os **Fields** (campos) de uma tabela. O **Tab Level** (nível) define a hierarquia: nível 0 é o cabeçalho, nível 1 é filho do nível 0, e assim por diante.

Abra **Window, Tab and Field** e crie:

| Campo | Valor |
|---|---|
| Name | `Visita Técnica` |
| Window Type | `Maintain` |
| Entity Type | `EDU` |

Na aba **Tab**, crie duas abas:

| Name | Table | Tab Level | Sequence |
|---|---|---|---|
| Visita | `EDU_Visita` | 0 | 10 |
| Linhas | `EDU_VisitaLinha` | 1 | 20 |

Em cada aba, clique em **Create Fields**. O iDempiere cria um campo para cada coluna da tabela.

O rótulo de cada campo vem do elemento. O elemento `SalesRep_ID` do core se chama *Sales Representative*, mas aqui ele é o técnico. Para trocar o rótulo só nesta janela, abra o campo na aba **Field**, desmarque **Centrally maintained** e mude o **Name** para `Técnico`. O elemento do core continua intacto.

Para organizar a tela, use a engrenagem da aba e abra o **Tab Editor**: arraste os campos até a posição desejada. Deixe `Cliente` e `Data` na primeira linha, `Tipo`, `Técnico` e `Urgente` na segunda, e `Description` ocupando a largura toda.

Por fim, volte a **Table and Column** e preencha o campo **Window** das duas tabelas com `Visita Técnica`. É isso que faz o *zoom* (clicar num campo que aponta para uma visita e abrir a janela certa) funcionar.

### 9. Menu e acesso

Abra a janela **Menu** e crie:

| Campo | Valor |
|---|---|
| Name | `Visita Técnica` |
| Action | `Window` |
| Window | `Visita Técnica` |
| Entity Type | `EDU` |

Salve e, na árvore do menu, arraste a entrada para dentro de uma pasta (por exemplo, **Partner Relations**). Não deixe itens soltos na raiz: um menu desorganizado é o primeiro sinal de uma instalação malcuidada.

A janela ainda não aparece para o GardenAdmin porque o perfil dele não tem acesso a ela. Saia do System, entre como **GardenAdmin**, rode o processo **Role Access Update** e depois saia e entre de novo.

### 10. Teste

Como GardenAdmin, abra **Visita Técnica** e cadastre uma visita:

1. No Cliente, clique na lupa. Só clientes aparecem (confirme que fornecedores ficam de fora).
2. Escolha o Tipo, marque Urgente e salve.
3. Vá para a aba **Linhas** e adicione dois produtos. Repare que `Line` vem preenchido com 10 e depois 20.
4. Volte ao cabeçalho e veja o que vem de graça: busca, histórico de alterações, anexos, exportação e modo grade.

Confira no banco:

```sql
SELECT v.edu_visita_id, bp.name, v.edu_tipovisita, v.edu_isurgente
FROM edu_visita v JOIN c_bpartner bp USING (c_bpartner_id);
```

## Checkpoint

- [ ] O tipo de entidade EDU existe
- [ ] As tabelas EDU_Visita e EDU_VisitaLinha existem no banco (confira no psql)
- [ ] O campo Cliente só lista parceiros que são clientes
- [ ] O campo Tipo de Visita mostra as três opções da lista
- [ ] A janela Visita Técnica aparece no menu do GardenAdmin
- [ ] Consigo salvar uma visita com duas linhas de produto
- [ ] Sei explicar a diferença entre as referências Table Direct, Table e Search

## Erros comuns

| Sintoma | Causa provável | Correção |
|---|---|---|
| A tabela não existe no banco | Nenhuma coluna foi sincronizada | Clique em **Synchronize Column** em qualquer coluna |
| Erro ao sincronizar coluna obrigatória | A tabela já tem registros e a coluna não tem valor padrão | Preencha **Default Logic** e sincronize de novo |
| A janela não aparece no menu | Faltou o **Role Access Update** ou o novo login | Rode o processo como GardenAdmin e entre de novo |
| A aba Linhas mostra linhas de todas as visitas | `EDU_Visita_ID` sem **Parent link column** | Marque o campo e recrie os campos da aba |
| A lista de técnicos vem vazia | Nenhum usuário está ligado a um parceiro marcado como funcionário ou representante de vendas | Na janela **Business Partner**, marque **Employee** ou **Sales Representative** no parceiro do usuário |
| O lookup mostra números em vez de nomes | A tabela referenciada não tem **Identifier** | Marque o identificador nas colunas certas |
| O `EDU` não aparece nas listas de Entity Type | Cache | Saia e entre de novo |
| `@SQL=` da coluna Line dá erro | Erro de digitação no SQL | Teste a consulta no `psql`, trocando `@EDU_Visita_ID@` por um ID real |

## Desafio

Adicione à visita um campo **Endereço de Atendimento** que aponte para os endereços do parceiro (`C_BPartner_Location_ID`) e mostre **apenas os endereços do cliente escolhido** no cabeçalho.

Dica: a validação dinâmica pode usar o valor de outro campo da mesma tela com `@NomeDaColuna@`. Procure antes se o core já tem uma regra pronta para isso.

## Referência

### Campos importantes da coluna

| Campo | Efeito |
|---|---|
| Mandatory | `NOT NULL` no banco e obrigatório na tela. Se marcado depois que a coluna já tem dados, vale só na tela |
| Updatable | Se desmarcado, o valor não pode mudar depois de salvo |
| Identifier | Compõe o texto que representa o registro em outras telas |
| Parent link column | Liga a aba filha à aba pai |
| Default Logic | Valor inicial (constante, `@variável@` ou `@SQL=`) |
| Synchronize Column | Aplica a definição no banco (`CREATE`/`ALTER TABLE`) |

Para remover uma coluna do banco, rode o processo **Drop Database Column** na aba Column **antes** de apagar o registro. Se apagar só o registro, a coluna fica órfã no banco.

### Tipos de janela

| Window Type | Comportamento |
|---|---|
| Maintain | Cadastros: mostra todos os registros |
| Transaction | Movimentos: ao abrir, mostra só os pendentes ou alterados recentemente |
| Query Only | Somente leitura |

### Atalho: Create Window, Tab and Field from Table

Depois de dominar o caminho manual, o processo **Create Window, Tab and Field from Table**, na janela Table and Column, cria janela, aba e menu de uma vez. Use o atalho só depois de ter feito o caminho manual pelo menos uma vez: quando algo der errado, você vai precisar saber o que ele fez.

### Mais referências

A lista completa (Amount, Integer, Number, Text Long, Image, Button, Location, Chosen Multiple Selection e outras) está na janela **Reference**, filtrando por Validation Type = `DataType`.

## Próximo módulo

[02 · Janelas inteligentes: contexto e lógicas](https://muriloht.com/idempiere/02-contexto-e-logicas): campos que aparecem, somem, travam ou se preenchem conforme o que o usuário faz.
