Introducción
Este documento describe la integración entre el TPV externo y el ERP Odoo 18 gestionado por AdemWeb. La integración es unidireccional: el TPV externo envía los datos de ventas a Odoo. Odoo no envía catálogo, precios ni empleados al TPV externo (al menos en esta primera versión).
Odoo es el sistema contable. El TPV externo es el master de ventas: el pedido se cobra en el TPV y luego se replica a Odoo para generar los asientos contables, el cierre de caja y los informes de gestión.
Resumen de endpoints
| Método | URL | Descripción |
|---|---|---|
| POST | /pos/ext/sessions/open |
Abrir sesión de caja |
| POST | /pos/ext/sessions/close |
Cerrar sesión de caja |
| POST | /pos/ext/orders |
Enviar pedido cobrado |
Autenticación
Cada establecimiento tiene un PdV (pos.config) en Odoo
con su propia API Key. El software del TPV debe incluirla en la
cabecera X-API-Key de todas las peticiones.
Adicionalmente, Odoo usa autenticación de sesión estándar. El software del TPV externo debe autenticarse primero con las credenciales del usuario de servicio, obtener la cookie de sesión y usarla en las llamadas.
Paso 1 — Obtener cookie de sesión
POST https://odoo.ejemplo.com/web/session/authenticate
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "call",
"params": {
"db": "nombre_base_datos",
"login": "tpv_service_user",
"password": "contraseña_usuario_servicio"
}
}
La respuesta incluye una cookie session_id.
Incluirla en todas las llamadas siguientes.
Paso 2 — Cabeceras de cada llamada
POST /pos/ext/orders HTTP/1.1
Host: odoo.ejemplo.com
Content-Type: application/json
Cookie: session_id=<cookie-de-sesion>
X-API-Key: <api-key-del-establecimiento>
Si la X-API-Key es incorrecta o no coincide con el
config_id enviado, Odoo responde 403.
Nunca exponer la API Key en logs ni en el código fuente.
Formato de las peticiones
- Todas las peticiones usan HTTP POST.
- El body siempre es JSON con
Content-Type: application/json. - Las respuestas son también JSON plano (no JSON-RPC).
- Las fechas usan formato ISO 8601:
2026-05-14T21:30:00. - Los importes son números decimales en euros:
12.50.
Códigos de respuesta
| Código | Significado | Acción recomendada |
|---|---|---|
| 200 | Éxito | — |
| 400 | Datos inválidos (campo obligatorio ausente, producto no encontrado, etc.) | Revisar el body enviado. No reintentar sin corrección. |
| 403 | API Key incorrecta o usuario sin permisos | Verificar credenciales con AdemWeb. |
| 404 | Sesión no encontrada (en close) | Comprobar el ext_session_uid enviado. |
| 500 | Error interno de Odoo | Reintentar con back-off exponencial. Notificar a AdemWeb si persiste. |
Todos los errores incluyen un campo "error" en el body con
la descripción: {"error": "Descripción del problema"}.
POST /pos/ext/sessions/open
Abre una sesión de caja en Odoo al inicio del turno.
Si la sesión con ese ext_session_uid ya existe y está abierta,
se devuelve la misma (idempotente).
Parámetros del body
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
config_code |
string | Recomendado | Código estable del PdV configurado en Odoo (p. ej. "SALA_1"). No cambia con restores de BD. Preferible a config_id. |
config_id |
integer | Opcional* | ID numérico del PdV en Odoo. Usar solo si no se tiene config_code. Puede cambiar tras un restore. |
ext_session_uid |
string (UUID) | ✔ Sí | Identificador único de la sesión generado por el TPV externo. Se usa para idempotencia. |
opening_cash |
number | Opcional | Importe en caja al abrir (fondo de caja). Por defecto 0. |
date_open |
string (ISO 8601) | Opcional | Fecha/hora de apertura en el dispositivo del TPV. |
Ejemplo de petición
{
"config_code": "SALA_1",
"ext_session_uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"opening_cash": 150.00,
"date_open": "2026-05-14T09:00:00"
}
Respuesta exitosa 200
{
"session_id": 42,
"name": "TPV Externo/E00001",
"state": "opened"
}
POST /pos/ext/sessions/close
Cierra la sesión de caja al final del turno.
Odoo genera el asiento de cierre de forma automática.
Si la sesión ya estaba cerrada, devuelve "already_closed": true
sin error (idempotente).
Parámetros del body
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
config_code |
string | Recomendado | Código estable del PdV (preferible a config_id) |
config_id |
integer | Opcional* | ID numérico del PdV en Odoo |
ext_session_uid |
string (UUID) | ✔ Sí | El mismo UUID enviado en el open de esta sesión |
closing_cash |
number | Opcional | Efectivo contado al cierre. Por defecto 0. |
date_close |
string (ISO 8601) | Opcional | Fecha/hora de cierre en el dispositivo del TPV. |
Ejemplo de petición
{
"config_code": "SALA_1",
"ext_session_uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"closing_cash": 187.50,
"date_close": "2026-05-14T23:30:00"
}
Respuesta exitosa 200
{
"ok": true,
"session_id": 42,
"name": "TPV Externo/E00001"
}
Sesión ya cerrada (idempotente) 200
{
"ok": true,
"session_id": 42,
"name": "TPV Externo/E00001",
"already_closed": true
}
POST /pos/ext/orders
Envía un pedido ya cobrado a Odoo.
Se crea la venta con sus líneas y pagos y queda en estado paid
(listo para contabilidad).
Si el ext_order_uid ya existe, se devuelve el registro
existente sin duplicar.
Parámetros del body
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
config_code |
string | Recomendado | Código estable del PdV (preferible a config_id) |
config_id |
integer | Opcional* | ID numérico del PdV en Odoo |
ext_order_uid |
string (UUID) | ✔ Sí | ID único del pedido en el TPV externo. Se usa para evitar duplicados. |
ext_session_uid |
string (UUID) | ✔ Sí | UID de la sesión abierta donde se genera este pedido |
date_order |
string (ISO 8601) | Opcional | Fecha/hora del pedido en el TPV |
table_name |
string | Opcional | Nombre o número de mesa (p. ej. "Mesa 5", "T3", "12") |
waiter_name |
string | Opcional | Nombre del camarero que tomó el pedido |
lines |
array | ✔ Sí | Líneas del pedido (ver tabla siguiente). Mínimo 1 línea válida. |
payments |
array | ✔ Sí | Formas de pago. Total debe cubrir el importe del pedido. |
Estructura de una línea (lines[])
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
product_ref |
string | ✔ Sí* | Código interno del producto en Odoo (default_code). Ver sección Productos. |
product_name |
string | Opcional | Nombre del producto. Se usa como fallback si product_ref no coincide. |
qty |
number | ✔ Sí | Cantidad vendida. Puede ser decimal (p. ej. peso). |
price_unit |
number | ✔ Sí | Precio unitario con IVA incluido (precio de venta al público). |
discount_pct |
number | Opcional | Porcentaje de descuento aplicado (0–100). Por defecto 0. |
Estructura de un pago (payments[])
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
method_code |
string | Recomendado | Código corto del método de pago acordado con AdemWeb (p. ej. "CASH", "CARD", "BIZUM"). Se configura una sola vez en Odoo. Más fiable que el nombre. Ver sección Métodos de pago. |
method_name |
string | Opcional | Nombre legible del método. Se usa como fallback si method_code no está configurado o no coincide. |
amount |
number | ✔ Sí | Importe cobrado con este método. Los métodos con importe 0 se ignoran. |
Ejemplo de petición
{
"config_code": "SALA_1",
"ext_order_uid": "f7e3a1b0-1234-5678-9abc-def012345678",
"ext_session_uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"date_order": "2026-05-14T21:32:00",
"table_name": "Mesa 5",
"waiter_name": "Ana García",
"lines": [
{
"product_ref": "CARNE-001",
"product_name": "Carne a la brasa",
"qty": 2,
"price_unit": 14.50,
"discount_pct": 0
},
{
"product_ref": "BEBIDA-COLA",
"product_name": "Coca-Cola 33cl",
"qty": 2,
"price_unit": 2.50,
"discount_pct": 0
}
],
"payments": [
{ "method_code": "CASH", "method_name": "Efectivo", "amount": 20.00 },
{ "method_code": "CARD", "method_name": "Tarjeta", "amount": 14.00 }
]
}
Respuesta exitosa 200
{
"order_id": 1523,
"name": "POS/2026/05/0127",
"state": "paid"
}
Pedido ya existente (idempotente) 200
{
"order_id": 1523,
"name": "POS/2026/05/0127",
"state": "paid",
"already_existed": true
}
Matching de productos
Odoo busca el producto en este orden de prioridad:
product_ref→ busca pordefault_code(código interno) en productos disponibles en POS. Método recomendado.product_ref→ busca porbarcode.product_name→ busca por nombre (case-insensitive).- Auto-creación → si ningún criterio coincide, Odoo crea automáticamente un producto stub con el nombre y precio de la línea del TPV externo.
Ningún pedido se pierde. Si un producto del TPV externo no existe en Odoo, se crea automáticamente en la categoría "TPV Externo - Pendiente" con el nombre, precio e impuesto por defecto de la empresa. El nombre original del TPV externo siempre se conserva en la línea del pedido.
Los productos auto-creados aparecen en Odoo bajo la categoría POS "TPV Externo - Pendiente". Tras la puesta en marcha hay que revisar esa categoría y vincular cada producto al artículo correcto del catálogo (o asignarle la cuenta contable y el impuesto adecuados). Esta revisión se recomienda hacer en los primeros días de operación.
Acción recomendada antes de la puesta en marcha:
acordar con AdemWeb que cada producto del TPV externo lleve un
product_ref que coincida con el default_code en Odoo.
Esto evita la auto-creación y garantiza que los impuestos y cuentas
contables son correctos desde el primer día.
AdemWeb puede exportar el catálogo completo con sus códigos internos.
Matching de métodos de pago
Cada pago puede enviarse con method_code y/o method_name.
Odoo aplica la siguiente prioridad:
| Prioridad | Condición | Ejemplo |
|---|---|---|
| 1ª — Código externo | method_code coincide exactamente con el campo
Código TPV Externo configurado en el método de pago de Odoo |
"BIZUM" → método con código BIZUM |
| 2ª — Tipo de diario | method_name es "efectivo"/"cash"
y el método tiene diario de tipo Cash |
"Efectivo" → diario caja |
| 3ª — Tipo de diario | method_name es "tarjeta"/"card"/"datáfono"
y el método tiene diario de tipo Bank |
"Tarjeta" → diario banco |
| 4ª — Nombre normalizado | Coincidencia parcial sin espacios/guiones, case-insensitive | "Uber Eats" coincide con "UberEats" |
| Fallback | Sin coincidencia → primer método del PdV (con aviso en el log) | El pago se registra igualmente |
Método recomendado: usar method_code.
Antes de la puesta en marcha, AdemWeb configura en cada método de pago del PdV
el código corto acordado con el proveedor del TPV (p. ej. CASH,
CARD, BIZUM, AMEX).
Este código es estable y no depende de cómo se llame el método en Odoo.
Acción previa a la puesta en marcha:
El proveedor del TPV debe proporcionar a AdemWeb la lista de códigos
(method_code) que usará su software para cada forma de pago.
AdemWeb los introduce en el campo Código TPV Externo de cada método
antes de la primera sincronización.
Mesas
El campo table_name es opcional pero recomendado para
restaurantes con distribución de sala. Odoo extrae los dígitos del nombre
y busca la mesa con ese número en el suelo vinculado al PdV.
Ejemplos de valores aceptados: "Mesa 5", "T5",
"5", "mesa5". Si la mesa no se encuentra,
el pedido se crea igualmente sin asignación de mesa.
Flujo completo de un turno
1. [TPV externo] Inicio de turno
POST /pos/ext/sessions/open → session_id: 42
2. [TPV externo] Pedidos durante el turno (uno a uno, en tiempo real o en batch)
POST /pos/ext/orders → order_id: 1520 (pedido 1)
POST /pos/ext/orders → order_id: 1521 (pedido 2)
POST /pos/ext/orders → order_id: 1522 (pedido 3)
...
3. [TPV externo] Fin de turno
POST /pos/ext/sessions/close → ok: true
4. [Odoo] Genera automáticamente:
- Asientos contables de cada venta
- Movimiento de cierre de caja
- Informe de sesión (Z)
Idempotencia y reintentos
Todos los endpoints son idempotentes: enviar la misma petición varias veces produce el mismo resultado sin duplicar datos en Odoo.
| Endpoint | Clave de idempotencia | Si ya existe |
|---|---|---|
/sessions/open |
ext_session_uid |
Devuelve la sesión existente |
/sessions/close |
ext_session_uid |
Devuelve "already_closed": true |
/orders |
ext_order_uid |
Devuelve el pedido existente con "already_existed": true |
El software del TPV puede reintentar cualquier llamada fallida (por timeout de red, error 500, etc.) sin riesgo de duplicar datos, siempre que use el mismo UID en el reintento.
Los errores 400 (datos inválidos) y 403 (auth) no deben reintentarse automáticamente — requieren corrección manual.