Web Controllers y APIs Externas en Odoo
En Odoo 20 distinguimos controladores de nuestra aplicación y la API externa de modelos JSON-2. Todos deben respetar los permisos del usuario autenticado.
1. Controladores internos HTTP y JSON-RPC #
Crea la carpeta controllers, importa controllers en el __init__.py raíz e importa biblioteca en controllers/__init__.py.
Fichero controllers/biblioteca.py:
1from odoo import http 2from odoo.http import request 3 4 5class LibraryController(http.Controller): 6 @http.route('/biblioteca/books', type='http', auth='user', methods=['GET']) 7 def books_http(self): 8 books = request.env['library.book'].search_read( 9 [('is_available', '=', True)], ['name', 'author', 'price'], limit=50, 10 ) 11 return request.make_json_response({'books': books}) 12 13 @http.route('/biblioteca/books/rpc', type='jsonrpc', auth='user', methods=['POST']) 14 def books_rpc(self): 15 return request.env['library.book'].search_read( 16 [('is_available', '=', True)], ['name', 'author', 'price'], limit=50, 17 )
Las rutas utilizan la sesión de un usuario con rol de biblioteca. No emplean sudo(): los accesos y dominios multiempresa siguen aplicándose. type='http' también puede devolver JSON; type='jsonrpc' utiliza el sobre JSON-RPC (params, result, error). No son el mismo protocolo.
Un webhook externo normalmente recibe HTTP con JSON ordinario y utiliza type='http'. Si no puede enviar token CSRF, desactiva CSRF solo en esa ruta, verifica la firma del proveedor sobre los bytes originales con comparación en tiempo constante, comprueba timestamp y evita procesar el mismo evento dos veces. No confirmes pagos únicamente porque llegue un POST.
2. API externa JSON-2 #
Genera una clave API para un usuario de integración con los permisos mínimos. Usa HTTPS fuera del entorno local. El siguiente script se ejecuta en la máquina cliente, no dentro del addon:
1import os 2import requests 3 4url = os.environ['ODOO_URL'].rstrip('/') 5headers = { 6 'Authorization': f"bearer {os.environ['ODOO_API_KEY']}", 7 'X-Odoo-Database': os.environ['ODOO_DATABASE'], 8 'User-Agent': 'curso-odoo20/2.0', 9} 10response = requests.post( 11 f'{url}/json/2/library.book/search_read', 12 headers=headers, 13 json={ 14 'domain': [('is_available', '=', True)], 15 'fields': ['name', 'author', 'price'], 16 'limit': 10, 17 }, 18 timeout=10, 19) 20response.raise_for_status() 21books = response.json() 22print(books)
Configura ODOO_URL, ODOO_API_KEY y ODOO_DATABASE en tu entorno local; no guardes claves en Git. Los argumentos son nombrados, el modelo y método están en la URL y la respuesta es JSON sin el sobre JSON-RPC. Cada llamada tiene su propia transacción: agrupa una operación de negocio atómica en un método de servidor.
En el servicio comercial Odoo Online, la disponibilidad de la API depende del plan; este curso trabaja sobre Community autohospedado.
3. Consumir un servicio externo #
Añade exchange_service a models/__init__.py. Define EXCHANGERATE_ACCESS_KEY como variable de entorno del servicio web de Compose (sin incluir su valor en el manifiesto del módulo).
Fichero models/exchange_service.py:
1import os 2import requests 3from odoo import models, _ 4from odoo.exceptions import UserError 5 6 7class ExchangeService(models.AbstractModel): 8 _name = 'library.exchange.service' 9 _description = 'Exchange rate client' 10 11 def fetch_latest_rates(self): 12 key = os.environ.get('EXCHANGERATE_ACCESS_KEY') 13 if not key: 14 raise UserError(_('Configure the exchange rate API key.')) 15 try: 16 response = requests.get( 17 'https://api.exchangerate.host/live', 18 params={'access_key': key}, timeout=10, 19 ) 20 response.raise_for_status() 21 payload = response.json() 22 except (requests.RequestException, ValueError): 23 raise UserError(_('The exchange rate service is unavailable.')) from None 24 if not isinstance(payload, dict): 25 raise UserError(_('Unexpected exchange rate response.')) 26 if not payload.get('success'): 27 raise UserError(_('The exchange rate service rejected the request.')) 28 quotes = payload.get('quotes') 29 if not isinstance(payload.get('source'), str) or not isinstance(quotes, dict): 30 raise UserError(_('Unexpected exchange rate response.')) 31 return {'source': payload.get('source'), 'quotes': quotes}
El endpoint live devuelve cotizaciones con una moneda de origen; no presupongas EUR ni confundas las cotizaciones con un importe convertido. Revisa las capacidades de tu plan. No muestres excepciones de red completas: podrían incluir la URL con la clave.
4. Compatibilidad histórica #
Las APIs externas XML-RPC y JSON-RPC están deprecadas desde Odoo 19; los servicios common y object tienen retirada prevista en Odoo 22 y Online 21.1. El servicio db ya se retiró en Odoo 20. Esto no depreca nuestros controladores type='jsonrpc'.