ERP y gestión
Normaliza imágenes de catálogo en Odoo
Lee productos de Odoo por XML-RPC, procesa su imagen con SocialCutter a las medidas de ecommerce y redes, y vuelve a escribirla. Snippet en Python.
- Odoo
- XML-RPC
- product.template
- catálogo
- ecommerce
- Python
- imágenes
Por qué normalizar el catálogo
En un catálogo, las fotos llegan con proporciones distintas: unas cuadradas, otras verticales, otras apaisadas. Publicar el mismo producto en la ficha de la tienda, en Instagram y en LinkedIn exige tres encuadres distintos. Hacerlo producto a producto no escala. SocialCutter recorta de forma centrada a las medidas exactas de cada destino y devuelve una URL por salida; Odoo guarda la imagen resultante en el propio producto para que el catálogo quede homogéneo.
Cómo funciona la API externa de Odoo
Odoo expone una API externa por XML-RPC en dos endpoints:
/xmlrpc/2/common: soloauthenticate, devuelve eluid./xmlrpc/2/object:execute_kw, ejecuta cualquier método de modelo.
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(DB, USER, API_KEY, {})
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
Los productos viven en product.template (la plantilla) y product.product (la variante). Para una normalización de catálogo, trabaja sobre product.template.
Aviso de versión
Este flujo asume Odoo 14 o posterior. El campo de imagen grande es image_1920; en versiones anteriores el campo se llama image. Los nombres de modelo y método son estables, pero conviene confirmar los campos disponibles en tu instancia con fields_get antes de escribir en producción:
fields = models.execute_kw(DB, uid, API_KEY, "product.template", "fields_get",
[], {"attributes": ["string", "type"]})
print([k for k in fields if k.startswith("image")])
Los campos de imagen del producto
product.template hereda image.mixin, así que no expone un único campo de imagen sino una familia. Los tamaños máximos y el origen de cada uno:
| Campo | Máximo | Cómo se llena |
|---|---|---|
image_1920 | 1920x1920 | Campo editable: aquí se escribe el base64 |
image_1024 | 1024x1024 | Relacionado almacenado, derivado de image_1920 |
image_512 | 512x512 | Relacionado almacenado, derivado de image_1920 |
image_256 | 256x256 | Relacionado almacenado, derivado de image_1920 |
image_128 | 128x128 | Relacionado almacenado, derivado de image_1920 |
El detalle que importa al automatizar: escribir el base64 grande en image_1920 es lo que dispara la generación de los tamaños menores. image_1024…image_128 son campos related con store=True y de solo lectura: no se escriben a mano, Odoo los recalcula a partir de image_1920. Escribir un valor pequeño directamente en image_128 no construye la cadena.
Estos tamaños se escalan manteniendo la proporción, sin recortar: si la imagen supera el máximo, se reduce hasta el límite sin deformarla. Es lo contrario de un encuadre a medida, así que el recorte conviene resolverlo antes de escribir. SocialCutter recorta de forma centrada a las medidas del destino y ese resultado es el que se guarda en image_1920.
XML-RPC y JSON-RPC
Todo esto funciona igual por los dos protocolos. Son las mismas llamadas al servicio object, con el mismo método execute_kw y los mismos argumentos [db, uid, password, modelo, método, args, kwargs]; solo cambia el endpoint y el empaquetado:
- XML-RPC:
POST /xmlrpc/2/object - JSON-RPC:
POST /jsonrpc, con{"service": "object", "method": "execute_kw", "args": [...]}
import requests
payload = {
"jsonrpc": "2.0",
"method": "call",
"params": {
"service": "object",
"method": "execute_kw",
"args": [ODOO_DB, uid, ODOO_KEY, "product.template", "write",
[[product_id], {"image_1920": b64_resultado}]],
},
"id": 1,
}
requests.post(f"{ODOO_URL}/jsonrpc", json=payload, timeout=60).raise_for_status()
Odoo documenta /xmlrpc, /xmlrpc/2 y /jsonrpc como obsoletos, con retirada prevista en Odoo 22, y apunta a la nueva API JSON-2 con autenticación por API key. Si empiezas una integración nueva, tenlo en cuenta al elegir.
Leer los productos y su imagen
products = models.execute_kw(
DB, uid, API_KEY, "product.template", "search_read",
[[("image_1920", "!=", False)]],
{"fields": ["name", "image_1920"], "limit": 50},
)
El campo image_1920 llega como cadena base64. Se decodifica a bytes antes de subirla.
Procesar la imagen con SocialCutter
Odoo ya tiene los bytes, así que se usa el endpoint multipart. Los destinos se eligen según dónde se vaya a publicar el producto:
DESTINATIONS = [
{"platform": "instagram", "format": "post"}, # 1080x1080
{"platform": "linkedin", "format": "post"}, # 1200x627
]
Cada destino tiene unas medidas fijas conocidas, que sirven para registrar el resultado:
| Destino | Medidas |
|---|---|
| instagram post | 1080x1080 |
| instagram story | 1080x1920 |
| facebook post | 1200x630 |
| linkedin post | 1200x627 |
| twitter post | 1200x675 |
| youtube thumbnail | 1280x720 |
| tiktok cover | 1080x1920 |
Escribir de vuelta en el producto
La respuesta de SocialCutter trae image_id y un array outputs, con la URL, la plataforma y el formato de cada salida. Se descarga la salida que se quiera como imagen de catálogo y se vuelve a codificar en base64 para el campo binario de Odoo:
models.execute_kw(DB, uid, API_KEY, "product.template", "write",
[[product_id], {"image_1920": b64_resultado}])
Odoo espera base64 en los campos binarios. Odoo no guarda las medidas de la imagen como campo estándar del producto: si necesitas conservarlas, escríbelas en un campo personalizado (x_image_width, x_image_height) o en la descripción.
Snippet completo en Python
import base64
import json
import xmlrpc.client
import requests
ODOO_URL = "https://mi-odoo.example.com"
ODOO_DB = "mi_base"
ODOO_USER = "[email protected]"
ODOO_KEY = "api_key_de_odoo" # usa una API key, no la contraseña real
SC_URL = "https://api.socialcutter.theboomer.dev"
SC_KEY = "sc_tu_clave"
DESTINATIONS = [
{"platform": "instagram", "format": "post"},
{"platform": "linkedin", "format": "post"},
]
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_KEY, {})
if not uid:
raise SystemExit("Autenticacion fallida en Odoo")
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
def sc_process(image_bytes, filename):
resp = requests.post(
f"{SC_URL}/api/v1/images/process/upload",
headers={"X-API-Key": SC_KEY},
files={"file": (filename, image_bytes, "image/jpeg")},
data={"destinations": json.dumps(DESTINATIONS)},
timeout=60,
)
resp.raise_for_status()
return resp.json()
products = models.execute_kw(
ODOO_DB, uid, ODOO_KEY, "product.template", "search_read",
[[("image_1920", "!=", False)]],
{"fields": ["name", "image_1920"], "limit": 50},
)
for product in products:
raw = base64.b64decode(product["image_1920"])
result = sc_process(raw, f"producto-{product['id']}.jpg")
# Inspecciona la forma real de cada salida
for output in result["outputs"]:
print(product["id"], output.get("platform"), output.get("format"), output.get("url"))
# Salida 1:1 como imagen principal de catalogo
square = next(o for o in result["outputs"] if o["platform"] == "instagram")
img = requests.get(square["url"], timeout=60)
img.raise_for_status()
models.execute_kw(
ODOO_DB, uid, ODOO_KEY, "product.template", "write",
[[product["id"]], {"image_1920": base64.b64encode(img.content).decode("ascii")}],
)
print("Actualizado", product["id"], product["name"])
Errores típicos
| Situación | Causa habitual |
|---|---|
xmlrpc.client.Fault al autenticar | Base de datos, usuario o API key incorrectos |
image_1920 vacío | El producto no tiene imagen: los tamaños menores se derivan de image_1920, así que también están vacíos |
401 de SocialCutter | La cabecera X-API-Key falta o la clave está revocada |
413 de SocialCutter | La imagen original supera 5 MB |
write sin efecto | El uid no tiene permisos de escritura sobre el modelo |
Coste
- 1 uso por destino (plataforma y formato) por petición.
- Los destinos repetidos en la misma petición no se cobran dos veces.
- Los procesamientos fallidos se devuelven.
Procesar 100 productos para dos destinos son 200 usos. Si vas a recorrer un catálogo grande, usa el endpoint POST /api/v1/images/batch o reparte el trabajo por lotes.
Siguientes pasos
- Guía de la API con curl: Procesa imágenes con la API desde la terminal
- Guía de Python: Automatiza SocialCutter con Python
- Guía de ERPNext: Normaliza imágenes de catálogo en ERPNext
- Documentación de la API: https://docs.socialcutter.theboomer.dev
Preguntas frecuentes
¿Qué versión de Odoo necesito?
El flujo usa la API externa XML-RPC, disponible en Odoo 14 y posteriores. El campo de imagen grande es image_1920 desde Odoo 13; en versiones más antiguas el campo se llama image. Confirma los nombres de campo en tu instancia antes de escribir.
¿Cómo me autentico contra Odoo?
Con xmlrpc.client contra /xmlrpc/2/common para obtener el uid y /xmlrpc/2/object para ejecutar métodos. Usa una API key de Odoo como contraseña en lugar de la contraseña real de la cuenta.
¿Cómo paso la imagen de Odoo a SocialCutter?
El campo de imagen de Odoo es binario y llega en base64. Se decodifica a bytes y se sube por multipart a POST /api/v1/images/process/upload.
¿Qué diferencia hay entre image_1920 y image_128?
image_1920 es el campo editable: ahí se escribe el base64. image_1024, image_512, image_256 y image_128 son campos relacionados almacenados y de solo lectura que se derivan de image_1920. Escribir el base64 en image_1920 es lo que hace que Odoo genere los tamaños menores; no se escriben a mano.
¿Dónde guardo las medidas de la imagen procesada?
Odoo no tiene un campo estándar de medidas en el producto. Las medidas salen de la ficha de cada destino; si quieres conservarlas, escríbelas en un campo personalizado o en la descripción del producto.
¿Cuánto cuesta procesar un producto?
1 uso por destino. Procesar un producto para Instagram post y LinkedIn post son 2 usos.