# 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.
- URL: https://socialcutter.theboomer.dev/guias/odoo/
- Idioma: es
- Familia: erp
- Actualizado: 2026-09-24
- Palabras clave: 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`: solo `authenticate`, devuelve el `uid`.
- `/xmlrpc/2/object`: `execute_kw`, ejecuta cualquier método de modelo.

```python
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:

```python
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": [...]}`

```python
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

```python
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:

```python
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:

```python
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

```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ó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](/guias/curl/)
- Guía de Python: [Automatiza SocialCutter con Python](/guias/python/)
- Guía de ERPNext: [Normaliza imágenes de catálogo en ERPNext](/guias/erpnext/)
- Documentación de la API: https://docs.socialcutter.theboomer.dev