⚠️ Aviso de vigencia: esta página se ha construido a partir de la última nota técnica disponible (
modulo-x-sga-receptor.md, 30/07/2026), que en su momento sustituyó a un montaje anterior. Los documentos fuente referencian un02-tecnico.md"vigente" que no estaba disponible al escribir esta página — si existe una versión más reciente, esta página necesita una revisión de contraste antes de darla por definitiva.
Receptor en Odoo de los movimientos de stock que genera el SGA (sistema de gestión de almacén) de Dinamic Area. Sustituye al conector .NET anterior, que aplicaba stock movimiento a movimiento y bloqueaba artículos de forma permanente cuando el producto no tenía fila de stock previa.
El conector antiguo tenía un fallo estructural: cuando Odoo rechazaba un movimiento (típicamente IllegalStateException: Producto X no encontrado ni en Stock ni en Playa), el artículo quedaba bloqueado para siempre y el conector no lo reportaba. Además marcó como enviados movimientos que Odoo nunca llegó a aplicar (49 de 50 en una muestra de diagnóstico), generando stock perdido en silencio.
El diagnóstico (29/07/2026) encontró dos problemas distintos coexistiendo:
stock.quant) en su ubicación.El nuevo diseño invierte el modelo: el SGA solo comunica, Odoo aplica. Ver Contrato con Dinamic Area más abajo.
Módulo Odoo declarativo (sin Python de addon), instalado vía base_import_module, con el patrón estándar de esta instalación (modelos como datos XML, lógica en ir.actions.server).
SGA (Dinamic Area)
│ POST JSON (uno o varios movimientos)
▼
/web/hook/<uuid> ← la URL es una credencial
│
base.automation (trigger on_webhook, log_webhook_calls=True)
│
├── record_getter: busca por x_sga_id o crea el registro
│ en x_sga_movimiento (estado=recibido)
▼
ir.actions.server "SGA: regularizar movimiento de stock"
│
▼
producto → marca → SOCIEDAD → almacén central → ubicación
crea la fila de stock si no existe → REGULARIZA → verifica por SQL → estado
El endpoint es auth='public' sin CSRF: quien tenga la URL puede escribir movimientos. La URL no debe ir a ningún repositorio ni canal no cifrado.
x_sga_receptor__manifest__.py
data/models.xml -- los dos modelos y sus campos
data/marcas.xml -- el mapa marca → sociedad, 14 registros
data/automations.xml -- el endpoint, la lógica y los dos crons
data/views.xml -- listas, formulario, buscador, acciones y menús
security/ir.model.access.csv
Gestionado desde mcp.module (exportable a git desde ahí).
Un POST con Content-Type: application/json. Acepta un movimiento o varios.
Uno:
{
"id_sga": "1190173",
"barcode": "8445692789000",
"cantidad": -1,
"tipo": "1",
"usuario": "P2L 29/07/26",
"fecha": "2026-07-29 11:38:44"
}
Varios (recomendado, dobla el rendimiento):
{
"movimientos": [
{"id_sga": "1199868", "barcode": "8445710091009", "cantidad": -1, "tipo": "1", "usuario": "P2L 30/07/26", "fecha": "2026-07-30 09:34:05"},
{"id_sga": "1199861", "barcode": "8445710096509", "cantidad": -2, "tipo": "1", "usuario": "P2L 30/07/26", "fecha": "2026-07-30 09:34:04"}
]
}
Tamaño de lote recomendado: 100. Medido con datos reales: uno por llamada = 11.167 mov/hora; lotes de 100 = 21.428 mov/hora (×1,9 de mejora por ahorro de latencia HTTP, el procesado sigue siendo uno a uno con su propio savepoint).
| Campo | Obligatorio | Notas |
|---|---|---|
id_sga |
Sí | mvi_Id del SGA. Clave de idempotencia (índice único en Odoo) |
barcode |
Sí (o product_id) |
El EAN tal cual, sin quitar ceros ni normalizar. Odoo resuelve el producto |
cantidad |
Sí | Delta con signo. 0 se registra como omitido, no como error |
tipo |
No | mvi_Tipo (1 = consumo/picking, 8 = entrada) |
usuario |
No | mvi_Usuario — es un identificador de ola del pick-to-light (P2L DD/MM/AA), no una persona en el 88,47 % de los casos |
fecha |
No | 'YYYY-MM-DD HH:MM:SS' en UTC. La conversión Madrid→UTC se pide en origen porque en el cambio de hora de octubre las 02:30 locales ocurren dos veces y no es reconstruible desde Odoo |
product_id |
No | Si el SGA ya conoce el id de Odoo, tiene preferencia sobre el barcode |
Respuesta: {"status": "ok"} con 200 si el movimiento quedó registrado (no necesariamente aplicado). 500 solo si ni siquiera pudo registrarse — ahí sí debe reintentar el emisor.
Un
200no implica que el stock se aplicó. Si el procesado falla, el movimiento queda enerrorcon el motivo, y se resuelve del lado de Odoo (crons de reintento + botón manual). El emisor no debe reintentar tras un 200.
x_sga_movimientox_sga_id char, índice ÚNICO -- clave de idempotencia (char, no integer:
mvi_Id es bigint y XML-RPC no admite
enteros de 64 bits)
x_ean char, índice -- EAN recibido, sin normalizar
x_cantidad float -- delta con signo
x_tipo integer -- mvi_Tipo
x_ola char -- mvi_Usuario (ola o operario)
x_fecha_movimiento datetime, índice -- en UTC
x_fecha_recepcion datetime
x_payload text -- el JSON crudo recibido, para depurar
x_estado selection, índice -- recibido | procesado | error | omitido
x_intentos integer
x_ultimo_intento datetime
x_fecha_procesado datetime
x_error_codigo char, índice -- código corto, agrupable
x_error_mensaje text -- texto completo del fallo
x_product_id many2one product.product
x_producto_url char, calculado -- enlace directo a la ficha de la variante
x_marca char, índice -- marca resuelta
x_company_id many2one res.company -- sociedad resuelta
x_warehouse_id many2one stock.warehouse
x_location_id many2one stock.location
x_quant_id many2one stock.quant -- fila de stock afectada
x_quant_creado boolean -- si la fila no existía y se creó (la métrica del proyecto)
x_unidades_antes integer
x_unidades_despues integer
| Código | Significado |
|---|---|
SIN_PRODUCTO |
Ningún producto activo y almacenable con ese EAN |
EAN_AMBIGUO |
El EAN devuelve más de un producto activo y almacenable |
PRODUCTO_ARCHIVADO |
El producto está archivado |
PRODUCTO_NO_ALMACENABLE |
is_storable = false |
SIN_MARCA |
La plantilla no tiene x_studio_marca |
MARCA_NO_MAPEADA |
Marca sin sociedad asignada en el mapa |
SIN_ALMACEN / SIN_UBICACION |
La sociedad no tiene almacén, o el almacén no tiene ubicación interna |
VERIFICACION_FALLIDA |
Se aplicó la regularización pero el stock releído no cuadra (se deshace) |
ERROR_ODOO |
Excepción del ORM. Mensaje completo en x_error_mensaje |
DELTA_CERO |
Cantidad 0 (estado omitido, no es un error) |
El orden importa: no se busca el almacén a ciegas, se asocia el movimiento a la sociedad y desde ahí se localiza el almacén central.
1. Delta = 0 → estado "omitido", nada que regularizar
2. Producto: barcode exacto, con active=true e is_storable=true
→ si no hay: error SIN_PRODUCTO
→ si hay varios: error EAN_AMBIGUO
3. Marca: product_tmpl_id.x_studio_marca
→ si vacía: error SIN_MARCA
4. SOCIEDAD: mapa marca → company_id (modelo x_sga_marca_sociedad)
→ si no está: error MARCA_NO_MAPEADA
5. ALMACÉN: el almacén "CENTRAL" de esa sociedad (derivado, NO hardcodeado)
6. UBICACIÓN: warehouse.lot_stock_id
7. Fila de stock: si el producto no tiene quant en esa ubicación, SE CREA
8. Regularización: inventory_quantity = actual + delta → action_apply_inventory()
9. VERIFICACIÓN por SQL directo: releer stock_quant y comprobar la cantidad
10. Solo entonces → estado "procesado"
El paso 9 es la diferencia estructural con el conector antiguo: nunca se marca procesado sin releer el stock real y comprobar que cuadra. Esta regla nació de un bug propio durante el desarrollo (un Savepoint.close() sin rollback=False marcaba procesado mientras el stock quedaba intacto — la misma avería del conector que se está sustituyendo). La verificación por SQL la habría cazado automáticamente.
Único dato de negocio a mantener. Editable en Inventario → Configuración → SGA: marcas y sociedades, sin desplegar nada:
| Marca | Sociedad | Almacén central | Ubicación | Contrapartida |
|---|---|---|---|---|
| Pampling | 4 Discam Textiles | 2 CDT | 96 CDT/Stock |
34 |
| Wituka, Nerry | 5 Wituka | 10 CWAFE | 160 CWAFE/Stock |
42 |
| PajaroFlama | 15 Coloretee | 23 CC | 264 CC/Stock |
30 |
| Iwik | 10 Amura Green | 80 AMURA | 736 AMURA/Stock |
29 |
| Versed, Crowdence, Nostalgic, Thundernoise, Catharsis, FEM, Pit I Collons, Agobio, BMC | 16 Crowdence | 79 CROW | 728 CROW/Stock |
32 |
El almacén, la ubicación y la contrapartida no están escritos en el módulo: se derivan dinámicamente de la sociedad (warehouse.lot_stock_id). Si mañana cambia una ubicación de stock, el receptor sigue funcionando sin tocar código.
⚠️ En producción la sociedad 16 aparece como "Crowdence Sl POR ARCHIVAR". Nueve de las catorce marcas apuntan ahí — pendiente de aclarar antes del corte definitivo con Dinamic Area.
⚠️ En producción existe la marca
Holgers(4 plantillas activas) que no está mapeada — sus movimientos caerían enMARCA_NO_MAPEADAhasta que se añada al mapa.
El registro y el procesado ocurren en la misma llamada, aislado con un savepoint: si la regularización lanza una excepción, se deshace solo esa parte y el registro sobrevive con su mensaje de error (en vez de perderse en un rollback de toda la transacción).
Detalles de esta instalación que condicionaron la solución:
with está prohibido en safe_eval — el savepoint se maneja a mano con try/except.Savepoint.close() hace ROLLBACK por defecto — hay que llamar close(rollback=False) explícitamente para confirmar.| Elemento | Frecuencia | Qué hace |
|---|---|---|
| Cron de inicialización | Una vez, autodesactivado | Crea por SQL el índice UNIQUE sobre x_sga_id (x_sga_movimiento_sga_id_uniq) y un índice sobre x_estado. Ya ejecutado |
| Cron de reintentos | Cada 30 min | Reprocesa lo que esté en error o recibido con menos de 5 intentos. Salta a propósito los errores que no se arreglan reintentando: SIN_MARCA, MARCA_NO_MAPEADA, EAN_AMBIGUO, PRODUCTO_NO_ALMACENABLE, DELTA_CERO |
| Botón "Reintentar" manual | Bajo demanda | Disponible en la ficha o para varios a la vez desde el menú Acciones. No respeta el límite de 5 intentos ni la lista de exclusiones — se asume que la causa ya se resolvió. Los movimientos ya procesado se saltan siempre (evita duplicar el stock aplicado) |
Interfaz en Inventario → Informes → Movimientos SGA: lista con badges de estado, filtros (pendientes/procesados/con error/omitidos/fila creada por el receptor/reintentados/de hoy), agrupaciones por estado/marca/sociedad/ubicación/producto/día, y formulario con lo recibido, lo resuelto, el resultado y el diagnóstico (mensaje de error + payload crudo).
Configuración del mapa: Inventario → Configuración → SGA: marcas y sociedades.
Cambio de modelo respecto al conector anterior:
ANTES SGA → [aplica stock en Odoo, espera resultado] → marca mvi_EnviaERP
AHORA SGA → [entrega el movimiento a Odoo] → marca mvi_EnviaERP
│
└─ Odoo lo procesa por su cuenta, con su propio
registro de errores y reintentos
Entrega y procesado son asíncronos e independientes. Si Odoo acepta la entrega (devuelve 200), el movimiento está entregado — punto. Si el procesado posterior falla, es responsabilidad de Pampling: se ve en el registro propio y se reintenta desde ahí. Dinamic Area no tiene que hacer nada al respecto.
| Responsabilidad | Quién |
|---|---|
Registrar el movimiento físico en clca_MovItemERP |
Dinamic Area |
| Entregar el movimiento a Odoo y gestionar reintentos de entrega | Dinamic Area |
Marcar mvi_EnviaERP cuando la entrega se confirma |
Dinamic Area |
| Resolver producto y ubicación en Odoo | Pampling |
| Aplicar el stock, incluido crear la fila cuando el producto no la tiene | Pampling |
| Registrar errores de procesado y reintentarlos | Pampling |
| Vigilar la cola y la deriva entre sistemas | Pampling (sync-sga, https://sync-sga.pampl.ing) |
El fallo que originó el proyecto (IllegalStateException: Producto X no encontrado ni en Stock ni en Playa) pasa a la columna de Pampling, y queda resuelto por diseño: el receptor crea la fila de stock si no existe.
x_sga_id tiene índice único en Odoo: reenviar el mismo movimiento no duplica el stock.| Fase | Qué |
|---|---|
| 1 | Pampling termina el receptor en staging y lo valida |
| 2 | Se dan a Dinamic Area credenciales de staging y el nombre del modelo/endpoint. Prueban el envío en lotes contra staging |
| 3 | Doble escritura en staging: su conector actual sigue aplicando en producción y a la vez entrega a staging. Se compara el stock resultante con sync-sga durante varios días |
| 4 | Si el stock cuadra, se despliega en producción y su conector pasa a solo entregar |
| 5 | Se decide qué hacer con los movimientos históricos atascados |
El módulo está instalado en producción, con la tabla de movimientos vacía — no se ha enviado ningún movimiento de prueba contra el endpoint de producción, precisamente para no mover stock real sin validación previa. Toda la validación funcional se hizo en staging con los mismos ficheros, byte a byte.
| Elemento | Estado |
|---|---|
| Endpoint | Activo, con registro de llamadas |
| Movimientos | 0 |
| Marcas cargadas | 14 |
| Crons | Inicialización ejecutada y autodesactivada; reintentos cada 30 min activo |
| Índice único | x_sga_movimiento_sga_id_uniq creado |
| Conector antiguo del SGA | Sigue funcionando igual, sin tocar |
Automatización del webhook a test01.pampling.com |
Sigue activa en producción (en staging se desactivó; en producción es una decisión aparte, pendiente) |
Holgers (4 plantillas activas en producción).Auditoría en producción: 113 plantillas activas y almacenables sin x_studio_marca. Sus movimientos caerían en SIN_MARCA.
La cifra real preocupante es mucho menor: la mayoría no son artículos de almacén.
| Total sin marca | 113 |
| Con código de barras (pueden llegar del SGA) | 14 |
| Con stock en almacén central | 5 |
| Artículos de venta reales a corregir | 1 (ya corregido: Malaga Costa del Sol Sweatshirt) |
Las 99 restantes son servicios/gastos, cuentas de venta, envíos de Shopify, productos demo de Odoo, materias primas de serigrafía y productos de punto de venta — nunca llegará un movimiento suyo desde el SGA.
Pendiente de decisión: si los consumibles de embalaje (rollos de TPV/impresora, pegatinas, bolsas — todos Wituka) se gestionan por el SGA. Dos de ellos (rollo TPV y rollo impresora) ya arrastran stock negativo acumulado de −1.071 unidades entre ambos, señal de que se consumen sin darse de alta nunca — problema previo al receptor, a mirar aparte.
Consulta para reproducir la lista en Odoo:
[('active','=',True), ('is_storable','=',True), ('x_studio_marca','in',[False,''])]
Resumen de las decisiones más relevantes tomadas durante el desarrollo (29–30/07/2026). Para el detalle completo y el razonamiento línea a línea, ver la bitácora de decisiones del proyecto.
stock.move con is_inventory=True (no stock.quant.inventory_quantity) porque aplica un delta puro sin leer el estado previo — inmune a condiciones de carrera con reservas o picking concurrentes, y crea el quant automáticamente si no existe.test01.pampling.com por cada escritura de stock era sospechosa de ser el cuello de botella real del conector antiguo (17.000 llamadas/día sobre transacciones síncronas). Se desactivó en staging; en producción sigue activa a la espera de decisión.base_import_module, sin Python de addon): esta instalación de Odoo ya tenía 5 módulos así, no hacía falta pedir nada a Process Control como planteaba el briefing original.Estos documentos, con fecha 29–30/07/2026, contienen el detalle de diagnóstico y decisión que esta página resume. Se conservan como archivo histórico del proyecto, no como referencia técnica vigente:
x_movimientos_sga, eliminado por completo el 30/07/2026. Se conserva por el detalle de la lógica de savepoint.x_sga_receptor — nota técnica del montaje final, base de esta página.Si existe un
02-tecnico.mdmás reciente que consolide todo esto, esta página de wiki debe revisarse contra él.