Odoo · Integraciones · APIs
Odoo expone su ORM completo por XML-RPC y JSON-RPC desde el primer día: no hace falta escribir un controlador para leer o escribir datos desde fuera. El problema no es la conexión —eso funciona a la primera—, es la autenticación floja, la falta de un usuario técnico y los write sin validar que abren la puerta a cualquier sistema externo mal configurado.
Conexión
Autenticar y llamar al ORM desde fuera
import xmlrpc.client
url = 'https://mi-odoo.com'
db, username, password = 'produccion', 'api_tecnico', 'xxx'
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
partners = models.execute_kw(
db, uid, password,
'res.partner', 'search_read',
[[['customer_rank', '>', 0]]],
{'fields': ['name', 'email'], 'limit': 20})
El patrón es idéntico en JSON-RPC, solo cambia el transporte a peticiones HTTP con cuerpo JSON contra /jsonrpc. Para depuración con curl, JSON-RPC es más cómodo; para clientes legados que ya traen librería XML-RPC, no merece la pena reescribir nada.
Autenticación
Un usuario técnico, nunca el admin
- Crea un usuario dedicado a la integración, con grupos mínimos (no
Administración/Ajustes). - Usa una clave de API (
res.users.apikeys) en lugar de la contraseña del usuario cuando la versión de Odoo lo soporte. - Restringe qué modelos puede tocar ese usuario vía grupos y record rules, igual que harías con cualquier otro perfil.
Exponer
Cuando Odoo es el que recibe: controlador propio
class ApiController(http.Controller):
@http.route('/api/v1/partner', type='json',
auth='user', methods=['POST'], csrf=False)
def partner_create(self, **data):
if not data.get('name'):
raise UserError("El campo 'name' es obligatorio")
partner = request.env['res.partner'].sudo().create({
'name': data['name'],
'email': data.get('email'),
})
return {'id': partner.id}
La ruta valida el payload antes de tocar el ORM y solo expone los campos que decide aceptar; nunca hagas create(**data) a pelo, porque cualquier clave adicional del JSON entrante acabaría escribiéndose sin control. El sudo() aquí es deliberado: el usuario técnico autenticado no necesita permisos de creación directos sobre res.partner, el controlador decide qué se permite crear.
Resiliencia
Errores, idempotencia y reintentos
@http.route('/api/v1/order', type='json', auth='user', methods=['POST'])
def order_create(self, **data):
external_ref = data.get('external_ref')
existing = request.env['sale.order'].sudo().search(
[('client_order_ref', '=', external_ref)], limit=1)
if existing:
return {'id': existing.id, 'status': 'already_exists'}
order = request.env['sale.order'].sudo().create({...})
return {'id': order.id, 'status': 'created'}
Cualquier integración con reintentos automáticos —y casi todas los tienen— acabará enviando la misma petición dos veces. Buscar por una referencia externa única antes de crear evita duplicados sin necesidad de coordinación adicional entre los dos sistemas.
type='json', una excepción no controlada se traduce en un JSON-RPC error genérico. Captura las excepciones de negocio y devuelve {'error': {'code': ..., 'message': ...}} para que el consumidor pueda distinguir un fallo de validación de un fallo de servidor.
Checklist
Antes de dar acceso RPC a un tercero
| ✅ Comprobación | Por qué |
|---|---|
| Usuario técnico con grupos mínimos | Limita el radio de un compromiso |
| Sin acceso admin/ajustes | Evita escalado de privilegios |
Payload validado antes de create/write | No aceptar campos arbitrarios |
| Búsqueda por referencia externa antes de crear | Idempotencia ante reintentos |
| HTTPS obligatorio | Credenciales no viajan en claro |
Resumen
Usa un usuario técnico con permisos mínimos, nunca el admin, para conectar Odoo con sistemas externos por XML-RPC o JSON-RPC. Cuando Odoo recibe peticiones, valida el payload explícitamente en lugar de pasar el diccionario entero a create, y diseña cada endpoint para que sea idempotente ante reintentos usando una referencia externa única.