Saltar al contenido principal
SocialCutter

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

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: solo authenticate, devuelve el uid.
  • /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:

CampoMáximoCómo se llena
image_19201920x1920Campo editable: aquí se escribe el base64
image_10241024x1024Relacionado almacenado, derivado de image_1920
image_512512x512Relacionado almacenado, derivado de image_1920
image_256256x256Relacionado almacenado, derivado de image_1920
image_128128x128Relacionado 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:

DestinoMedidas
instagram post1080x1080
instagram story1080x1920
facebook post1200x630
linkedin post1200x627
twitter post1200x675
youtube thumbnail1280x720
tiktok cover1080x1920

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 = "usuario@example.com"
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ónCausa habitual
xmlrpc.client.Fault al autenticarBase de datos, usuario o API key incorrectos
image_1920 vacíoEl producto no tiene imagen: los tamaños menores se derivan de image_1920, así que también están vacíos
401 de SocialCutterLa cabecera X-API-Key falta o la clave está revocada
413 de SocialCutterLa imagen original supera 5 MB
write sin efectoEl 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

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.