# Flujo: subida automática de pedidos de Odoo a JDE

Documento de estudio del endpoint `POST /odoo/subirPedidosJDE`, que es el que sube presupuestos/pedidos de Odoo (`sale.order`) a JDE como pedido de venta. Incluye también el endpoint hermano `consultarPlazosEntrega` (calcula plazos de entrega, no sube el pedido) porque comparte piezas del mismo dominio.

Archivos implicados:

- `src/routes/odoo.js` — rutas
- `src/controllers/odoo.js` — lógica (`ctrl.subirPedidosJDE`, `ctrl.consultarPlazosEntrega`)
- `src/utils/jde_API.js` — cliente HTTP hacia el orchestrator de JDE (Studio)
- `src/utils/Odoo.js` — cliente JSON-2 hacia Odoo (no incluido aquí, ya documentado en CLAUDE.md)
- `src/middleware/apiGuard.js` — autenticación/autorización de la petición entrante

---

## 1. Quién dispara esto

No hay cron ni polling en esta API. El disparo es **Odoo → esta API** vía webhook:

1. En Odoo hay una automatización (server action / automation rule) sobre `sale.order` que, ante un evento (botón manual o cambio de estado — confirmarlo en Odoo, no está en este repo), hace un `POST` HTTP a `https://<esta-api>/odoo/subirPedidosJDE` con `{ "id": <id del sale.order> }`.
2. Esa llamada se autentica con el token de tabla del usuario **"Odoo"** (`api_user` con prefix `Odoo`), mandado como `Authorization: Bearer`, `?token=` o cabecera `x-odoo-webhook-token` (las tres formas existen porque las automatizaciones de Odoo no siempre pueden mandar cabeceras custom con facilidad).
3. Esa petición entra en el middleware compartido de `src/app.js`: `decryptRequest` → `encryptResponse` → `apiGuard` (montado en el router `/odoo`) → controlador.

`apiGuard` (`src/middleware/apiGuard.js`) hace, en orden:

- Comprueba bloqueos activos por IP/usuario (`api_block`) → 429 si está bloqueado.
- Resuelve el token: SHA-256 del valor recibido, busca en `api_token`. Si no existe/está revocado/caducado → 401, y registra el intento fallido en `api_auth_attempt` (puede generar bloqueo por fuerza bruta).
- Resuelve permisos contra `api_endpoint` (match exacto de `/odoo/subirPedidosJDE`) + `api_user_endpoint`/`api_user_scope`. El usuario "Odoo" necesita tener scope/permiso concedido sobre este endpoint (o ser admin).
- Aplica rate limit del usuario.
- Cuelga `req.apiUser` / `req.apiToken` / `req.apiEndpoint` y registra la petición en `api_request_log` al terminar (`res.finish`).

Importante: **`ctrl.subirPedidosJDE` no vuelve a comprobar `req.apiUser`** (a diferencia de `pushNotifyHandler`, que sí lo hace explícitamente). Si `apiGuard` deja pasar la petición es porque ya autenticó; el controlador confía en eso sin doble check.

---

## 2. Punto de entrada del controlador

```
ctrl.subirPedidosJDE = async (req, res) => {
  const id = Number(req.body?.id || req.body?._id)
  ...
}
```

- Acepta `id` o `_id` en el body (Odoo, según la versión del webhook/automation, puede mandar uno u otro).
- Si no hay `id` → `400`.
- Todo el cuerpo va en un único `try/catch`. Si algo falla en cualquier punto, cae al `catch` final, que:
  - Extrae mensaje de error (prioriza `error.response?.data?.message`, que es el formato de error que devuelve el orchestrator de JDE).
  - Postea el error en el chatter del `sale.order` (`safePostError`, ver más abajo) — **esto es lo que ve el usuario de Odoo cuando algo falla**, no un log.
  - Devuelve `500` con el mensaje.

---

## 3. Paso a paso de la lógica (líneas 895–1013 de `src/controllers/odoo.js`)

### 3.1 Leer el presupuesto

```js
odoo.json2SearchRead('sale.order', [['id', '=', id]],
  ['name', 'client_order_ref', 'x_studio_descripcion', 'partner_id', 'commercial_partner_id', 'order_line'])
```

Si no existe → `404`. Si Odoo devuelve `{ error }` (fallo de JSON-2 RPC, no "sin resultados") → se lanza excepción genérica y cae al catch (mensaje poco específico: "Error leyendo presupuesto en Odoo").

### 3.2 Resolver el código de cliente JDE

- Saca `partner_id` y `commercial_partner_id` del pedido (many2one → id numérico via `extractMany2oneId`).
- Lee `x_studio_cod_cliente_jde` de ambos contactos en una sola llamada `res.partner` con `id in [...]`.
- **Prioridad**: usa el código JDE del contacto exacto del pedido; si no lo tiene, cae al de la empresa comercial padre.
- Si ninguno de los dos tiene código JDE → no se sube nada, se postea error en el chatter ("El cliente no tiene Cod Cliente JDE") y se responde `400`. Esto es una validación de negocio real, no un bug: sin `CodCliente` JDE rechazaría el pedido igualmente.

### 3.3 Leer líneas del presupuesto y sus productos

- `order_line` (ids) → `sale.order.line` trae `product_id`, `product_uom_qty`, `x_studio_opcionales_tags`.
- De los productos (`product.product`) solo se pide `product_tmpl_id` — es decir, **el "Equipo" que se manda a JDE es siempre el nombre del `product.template`, nunca el `default_code`/referencia interna**. Esto es una decisión explícita y documentada también en `consultarPlazosEntrega` (comentario línea ~1373): "se casa por `product.template.name`, nunca por `default_code`". Coincide con la memoria guardada sobre casado Odoo↔JDE.

### 3.4 Resolver opcionales → segmentos JDE

Esta es la parte más indirecta del flujo:

1. Se recogen todos los ids de `x_studio_opcionales_tags` de todas las líneas.
2. Se leen en Odoo (`x_opcionales` → `x_name`) para tener el **nombre** de cada opcional (no hay campo que enlace directamente el id de Odoo con IntarLAB).
3. `resolveSegmentsForOpcionales(names)` consulta **IntarLAB** (no Odoo, no JDE). El vínculo opcional↔segmento ya no es 1:1 en `ilab_opcional`: vive en la tabla puente genérica `ilab_opcional_jde_segmento` (M:N, un opcional puede matchear varias filas del catálogo; la misma tabla también guarda accesorios vía `config.acc_uuid`, aquí solo se usan las filas con `config.opcional_uuid`), más combos de 2 miembros en `ilab_jde_segmento_combo`:
   ```sql
   SELECT ojs.config, s.jde_numsegmento, s.jde_codvalor
   FROM ilab_opcional_jde_segmento ojs
   JOIN ilab_jde_segmentos s ON s.uuid = ojs.jde_segmento_uuid
   WHERE ojs.member_uuid IN (?)
   ```
   El match es **por nombre de texto**, no por id/uuid — es el propio comentario del código el que lo admite: "no existe vínculo directo Odoo↔ilab_opcional todavía". Esto es un punto frágil: si el nombre del opcional en Odoo (`x_opcionales.x_name`) no coincide carácter a carácter con `ilab_opcional.opcional_name`, el segmento no se resuelve y **no hay error**, solo un `logger.warn('Opcionales sin segmento JDE vinculado: [...]')` — la línea se sube a JDE sin ese segmento, en silencio.

### 3.5 Construir `GridVentas`

Por cada línea de pedido:

- `Articulo` = nombre del `product.template`.
- Los opcionales de la línea se ordenan por `numSegmento` y se vuelcan como `Segmento1/Valor1`, `Segmento2/Valor2`, ... (`Segmento` con padding a 2 dígitos).
- **Clave**: `product_uom_qty` se redondea (`Math.round`) y se genera **una fila del grid por unidad** (`Array.from({length: qty}, ...)`). Si una línea pide 5 unidades, entran 5 filas idénticas en `GridVentas`. Esto es coherente con cómo JDE modela pedidos de equipos serializados (una línea = una unidad física), pero significa que **cantidades grandes generan payloads grandes** y que una `qty` mal introducida en Odoo (ej. 500 por error de tecleo) generaría un grid gigante sin ningún tope/validación previo.

### 3.6 Payload final a JDE

```js
{
  CodCliente: codCliente,
  Referencia: order.client_order_ref || order.x_studio_descripcion || '',
  NumCRM: order.name,
  GridVentas: gridVentas,
}
```

### 3.7 Llamada a JDE

`jdeApi.postPedidoVenta(payload)` → ver sección 4.

### 3.8 Resultado

- Si todo va bien: postea en el chatter del `sale.order` "Presupuesto enviado a JDE (Referencia <name>)" y responde `200` con `{ success: true, payload, jdeMessage }`.
- **Nota**: el mensaje de éxito en el chatter no incluye el número de pedido JDE devuelto ni el contenido de `jdeMessage` — solo confirma que se envió, no el resultado real que dio JDE. Si JDE acepta la llamada HTTP pero el orchestrator devuelve un mensaje de negocio negativo dentro de `jde__simpleMessage` (200 OK con error de negocio embebido, patrón típico en Orchestrator Studio), **esto se trataría como éxito** salvo que se revise el chatter/log manualmente.

---

## 4. Cliente JDE (`src/utils/jde_API.js`)

Clase `JDE_API`, instanciada una vez por proceso (`const jdeApi = new JDE_API()` en `odoo.js`, singleton de facto).

- **Auth**: Basic Auth (`JDE_USER`/`JDE_PASSWORD` de `.env`) contra `POST {JDE_API_URL}/jderest/v2/tokenrequest`. Devuelve `userInfo.token`.
- **Cache de token**: se guarda en memoria de la instancia (`this.token`, `this.tokenExpiration`), con TTL de ~9m30s (10 min menos 30s de margen). Como es un singleton in-process, el token se comparte entre todas las peticiones concurrentes de subida de pedidos — no hay lock, así que dos subidas simultáneas justo cuando el token caduca podrían disparar dos `authenticate()` en paralelo (no es grave, solo pide dos tokens, pero no está serializado).
- **Reintento por token caducado**: `callOrchestrator` intenta la llamada; si falla con `401`/`444`/`E1LoginException` (`isTokenError`), reautentica una vez y reintenta. Si falla por cualquier otro motivo, no reintenta, propaga el error tal cual (con log).
- **Orchestrator invocado**: `59OR_CreacionPedidoIntarconVariasLineas`, vía `POST {JDE_API_URL}/jderest/orchestrator/59OR_CreacionPedidoIntarconVariasLineas`, con el token metido en el body (`{ ...data, token }`), no en cabecera — es el formato que exige JDE Orchestrator Studio.
- `postPedidoVenta(data)` es azúcar sobre `callOrchestrator(OR_SUBIR_PEDIDO, data)`.

No hay timeout explícito configurado en el `axios.post` de este archivo (a diferencia de `subirReservaSala` en `odoo.js`, que sí pone `timeout: 15000`). Si JDE se queda colgado, la petición podría no acabar nunca.

---

## 5. Manejo de errores y visibilidad

Todo error de negocio o técnico dentro de `subirPedidosJDE` termina en `safePostError(id, message, 'sale.order')`:

```js
async function safePostError(recordId, errorMessage, model = 'res.partner') {
  try {
    await postChatterMessage(recordId, `Error: ${truncate(errorMessage, 250)}`, model)
  } catch { logger.error(...) }
}
```

- `postChatterMessage` llama a `message_post` de Odoo (JSON-2 `json2CallMethod`), truncando el mensaje a 250 caracteres.
- Esto significa que **el canal principal de feedback para el usuario de Odoo es una nota en el chatter del presupuesto**, no un email, no una notificación push (esa sí existe para otros flujos — `pushNotifyHandler` — pero no se usa aquí).
- Si el propio `postChatterMessage` falla (Odoo caído, permisos, etc.), el error solo queda en el log del servidor — el usuario de Odoo no se entera de nada, el pedido simplemente no aparece como enviado ni como fallido.

---

## 6. Endpoint hermano: `consultarPlazosEntrega`

No sube el pedido a JDE, pero vive en el mismo archivo y comparte convenciones (mismo casado por `product.template.name`, mismo patrón de leer `sale.order`/`sale.order.line`). Sirve para calcular y escribir en cada línea el plazo de fabricación estimado (`x_studio_plazo_dias` / `x_studio_plazo_semanas`), cruzando JDE (componentes críticos, pedidos de compra, saturación de producto) con calendario laboral e IntarLAB. Se dispara igual, por webhook desde un botón en Odoo. Detalle relevante para no confundir flujos: **esto no crea ni modifica nada en JDE**, solo lee de JDE y escribe en Odoo.

Está documentado con más profundidad de comentarios en el propio código (líneas 1191–1604) porque el cálculo es bastante más largo; si quieres, puedo hacer un documento aparte solo para ese cálculo de plazos.

---

## 7. Puntos a revisar / posibles mejoras (para ir corrigiendo)

1. **Match de opcionales por nombre de texto** (`resolveSegmentsForOpcionales`): sin vínculo id/uuid real Odoo↔IntarLAB, cualquier cambio de nombre en cualquiera de los dos lados rompe el segmento en silencio (solo warning en log, el pedido se sube igual, incompleto).
2. **Sin tope de cantidad** en la generación de `GridVentas`: una `product_uom_qty` grande por error genera un payload proporcionalmente grande sin validación previa.
3. **Éxito HTTP ≠ éxito de negocio en JDE**: no se inspecciona `jde__simpleMessage` para detectar que el orchestrator haya devuelto un error de negocio con status 200; se marca como éxito en el chatter igualmente.
4. **Sin timeout** en la llamada del orchestrator (`jde_API.js`), a diferencia de otras llamadas externas del mismo controlador que sí lo tienen.
5. **Sin reintento/cola** si JDE está caído en el momento del webhook: la automatización de Odoo debería reintentar por su cuenta (confirmar si Odoo tiene retry configurado) o el pedido queda "no subido" hasta que alguien pulse otra vez el botón.
6. **`apiGuard` no se revalida dentro del controlador** (a diferencia de `pushNotifyHandler`) — es coherente con cómo está montado el middleware, pero conviene tenerlo presente si algún día se cambia el montaje de la ruta.
7. **Idempotencia**: no hay ningún campo en Odoo que marque "ya subido a JDE con éxito" antes de reintentar — si se pulsa el botón dos veces, se manda dos veces el pedido completo a JDE. Confirmar si el orchestrator de JDE deduplica por `NumCRM` o si esto puede generar pedidos duplicados en JDE.
