22 mayo 2026
Lo que aprendí integrando Odoo 18 con Next.js — 3 errores que cometí
Primera parada: el blog de rimamo.dev. Si has llegado hasta aquí, bienvenido. Este es el primer post de muchos donde voy a mostrar el trabajo real, sin filtros, de un CTO Fractional construyendo para PYMEs.
Contexto: de dónde viene esto
Hace unos meses empecé a trabajar con un cliente del sector construcción en Barcelona. Su problema: gestionaban todo con Excel — presupuestos, órdenes de trabajo, inventario, operarios. Cero visibilidad en tiempo real, decisiones basadas en corazonadas, y un infierno de conciliación cada semana.
Mi trabajo: construir un sistema de gestión desde cero en 3 meses. Stack elegido:
- Odoo 18 como backend ERP (gestión de operarios, clientes, inventario)
- Next.js como frontend custom (app mobile-first para operarios en campo)
- JSON-RPC para la comunicación entre ambos
Suena bien sobre el papel. En la práctica, me llevé varias sorpresas.
Aquí van los 3 errores que más me costaron tiempo, y cómo los solucioné para que tú no pierdas las mismas horas.
Error #1: La API de Odoo no siempre hace lo que promete (groups_id edition)
El escenario:
Necesitaba crear usuarios operarios desde la app Next.js. Al registrarse, el sistema debía asignarles automáticamente un grupo de permisos específico (“Operario de Campo”) en Odoo.
Mirando la documentación de la API JSON-RPC de Odoo 18, create soporta campos estándar y relacionales. groups_id es un campo Many2many. Debería funcionar, ¿no?
El código que escribí primero:
# Llamada JSON-RPC desde Next.js
models.execute_kw(
db, uid, password,
'res.users', 'create',
[{
'name': 'Operario ejemplo',
'login': '[email protected]',
'groups_id': [(4, group_id)]
}]
)Lo que pasó:
El usuario se creaba, pero sin grupo asignado. Silenciosamente. Sin error, sin warning. Odoo ignoraba el campo groups_id en la creación vía API.
Por qué ocurre:
Por seguridad, Odoo 18 restringe la modificación de groups_id vía API externa. Solo el superadmin o scripts internos pueden tocarlo. La documentación no lo dice claramente — tienes que descubrirlo en los foros o en el código fuente.
La solución:
Escribir un endpoint específico en un módulo custom de Odoo que sí tiene permisos internos:
# Módulo custom en Odoo
class UserManagement(models.Model):
_name = 'myapp.user.management'
def action_create_operator(self, vals):
user = self.env['res.users'].create({
'name': vals['name'],
'login': vals['login'],
'groups_id': [(4, self.env.ref('myapp.group_operator').id)]
})
return user.idY desde Next.js llamar a este método en lugar de al create directo.
Error #2: Push sin pull = conflicto asegurado (y datos perdidos)
El escenario:
Trabajando desde dos máquinas (portátil en casa + PC en Hetzner). Hago un cambio en el módulo de Odoo desde casa, hago push. Al rato, desde el PC hago cambios, y sin pull previo, hago push.
Lo que pasó:
Git me rechazó el push porque la rama remota tenía commits que yo no tenía. Solución rápida: git pull --rebase. Pero en el proceso, perdí uno de los cambios que había hecho en el PC porque el rebase no se resolvió limpiamente y acepté la versión equivocada del conflicto.
Por qué ocurre:
Prisa. Cuando estás en modo “arreglar esto YA”, te saltas los pasos. Y en un proyecto en solitario, crees que no hace falta ser tan estricto con Git porque “solo trabajo yo”.
La solución (el workflow que uso ahora):
# Antes de empezar a trabajar SIEMPRE:
git pull --rebase
# Después de cada cambio atómico:
git add -p
git commit -m "tipo: descripción clara"
git push
# Si hay conflicto:
git pull --rebase
# Resolver conflictos con cuidado
git add .
git rebase --continue
git pushY lo más importante: un alias para no olvidarme:
git config --global alias.sync '!git pull --rebase && git push'Error #3: Asumir que todos los campos de Odoo son editables vía API
El escenario:
Quería actualizar el estado de una orden de trabajo desde la app móvil. El campo state del modelo sale.order tiene un workflow definido. En la interfaz de Odoo, cambiar de estado implica pasar por métodos específicos (action_confirm(), action_done()).
Pero yo intenté hacerlo directamente:
// Next.js → Odoo JSON-RPC
const result = await odooClient.write('sale.order', orderId, {
state: 'done'
});Lo que pasó:
El campo se actualizaba en base de datos pero Odoo no ejecutaba ninguna de las acciones asociadas al cambio de estado. Sin validaciones, sin workflows, sin logs. La orden aparecía como “Completada” pero sin los efectos secundarios (factura generada, stock actualizado, notificación enviada).
La solución:
// Llamar al método de Odoo
const result = await odooClient.call(
'sale.order', 'action_done', [orderId]
);O, si necesitas lógica específica:
# Módulo custom en Odoo
class WorkOrder(models.Model):
_inherit = 'sale.order'
def mobile_complete(self):
if self.state not in ['sale', 'done']:
raise UserError("La orden no se puede completar")
self._create_mobile_completion_log()
return self.action_done()action_*).El workflow que uso ahora
Después de estos errores, mi proceso de integración Odoo ↔ Next.js es:
- Siempre crear un módulo custom intermediario en Odoo para cualquier operación compleja. No expongo el modelo directamente.
- Documentar cada método expuesto con sus parámetros, efectos secundarios y permisos necesarios.
- Testear cada llamada en staging antes de tocar producción. Tengo un script Postman con todas las llamadas JSON-RPC.
- Validar que el estado final es el esperado — no solo que el campo se escribió, sino que los workflows se ejecutaron.
- Logging en ambos lados: Next.js logga la petición, Odoo logga la respuesta y la acción ejecutada.
// Cliente JSON-RPC tipado en Next.js
interface OdooResponse<T> {
jsonrpc: '2.0'
id: number
result?: T
error?: {
code: number
message: string
data?: any
}
}
async function callOdoo<T>(
method: string,
params: any[]
): Promise<T> {
const response = await fetch(`${ODOO_URL}/jsonrpc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {
service: 'object',
method: 'execute_kw',
args: [ODOO_DB, UID, PASSWORD,
'sale.order', method, params]
},
id: Date.now()
})
})
const data: OdooResponse<T> = await response.json()
if (data.error) {
console.error('Odoo error:', data.error)
throw new Error(data.error.message)
}
return data.result
}Por qué comparto esto públicamente
Porque el CTO Fractional no es el que nunca se equivoca — es el que ya se equivocó, aprendió, y puede ahorrarte cometer los mismos errores.
Este proyecto es mi primer caso real como CTO Fractional, y estos errores son reales. Cada uno me costó entre 2 y 6 horas de debugging. Si este post te ahorra aunque sea una de esas horas, ya ha cumplido su propósito.
Los leo en comentarios o en mi DM de LinkedIn.
Si este tipo de contenido te resulta útil, estoy empezando una newsletter quincenal donde comparto lecciones aprendidas construyendo para PYMEs. Sin hype, sin cursos, solo trabajo real.
— Ricardo Martínez, CTO Fractional para PYMEs