Skip to main content
SocialCutter

ERP and management

Normalize catalogue images in Odoo

Read products from Odoo over XML-RPC, process their image with SocialCutter to ecommerce and social dimensions, and write it back. Python snippet.

  • Odoo
  • XML-RPC
  • product.template
  • catalogue
  • ecommerce
  • Python
  • images

Why normalize the catalogue

In a catalogue, photos arrive in different proportions: some square, some portrait, some landscape. Publishing the same product on the shop page, Instagram and LinkedIn needs three different framings. Doing it product by product does not scale. SocialCutter crops centrally to the exact dimensions of each destination and returns one URL per output; Odoo stores the resulting image on the product itself so the catalogue stays consistent.

How Odoo’s external API works

Odoo exposes an external XML-RPC API on two endpoints:

  • /xmlrpc/2/common: only authenticate, returns the uid.
  • /xmlrpc/2/object: execute_kw, runs any model method.
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")

Products live in product.template (the template) and product.product (the variant). For catalogue normalization, work on product.template.

Version note

This flow assumes Odoo 14 or later. The large image field is image_1920; on older versions the field is called image. Model and method names are stable, but confirm the available fields on your instance with fields_get before writing to production:

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")])

The product image fields

product.template inherits image.mixin, so it does not expose a single image field but a family of them:

FieldMaxHow it is filled
image_19201920x1920The editable field: you write the base64 here
image_10241024x1024Stored related field, derived from image_1920
image_512512x512Stored related field, derived from image_1920
image_256256x256Stored related field, derived from image_1920
image_128128x128Stored related field, derived from image_1920

The detail that matters when automating: writing the large base64 into image_1920 is what triggers the smaller sizes to be generated. image_1024…image_128 are related fields with store=True and they are read-only: you do not write them by hand, Odoo recomputes them from image_1920. Writing a small value straight into image_128 does not build the chain.

Those sizes are scaled keeping the aspect ratio, without cropping: an oversized image is shrunk to the limit without distortion. That is the opposite of a fixed framing, so the crop is best resolved before writing. SocialCutter crops centrally to the destination dimensions, and that result is what gets stored in image_1920.

XML-RPC and JSON-RPC

All of this works the same over both protocols. They are the same calls to the object service, with the same execute_kw method and the same arguments [db, uid, password, model, method, args, kwargs]; only the endpoint and the packaging change:

  • XML-RPC: POST /xmlrpc/2/object
  • JSON-RPC: POST /jsonrpc, with {"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_result}]],
    },
    "id": 1,
}
requests.post(f"{ODOO_URL}/jsonrpc", json=payload, timeout=60).raise_for_status()

Odoo documents /xmlrpc, /xmlrpc/2 and /jsonrpc as deprecated, with removal planned in Odoo 22, and points to the new JSON-2 API with API-key authentication. Keep that in mind if you start a new integration.

Read products and their image

products = models.execute_kw(
    DB, uid, API_KEY, "product.template", "search_read",
    [[("image_1920", "!=", False)]],
    {"fields": ["name", "image_1920"], "limit": 50},
)

The image_1920 field arrives as a base64 string. Decode it to bytes before uploading.

Process the image with SocialCutter

Odoo already holds the bytes, so the multipart endpoint is the right one. Pick destinations based on where the product will be published:

DESTINATIONS = [
    {"platform": "instagram", "format": "post"},   # 1080x1080
    {"platform": "linkedin", "format": "post"},    # 1200x627
]

Each destination has fixed, known dimensions, which serve to record the result:

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

Write back to the product

The SocialCutter response carries image_id and an outputs array with the URL, platform and format of each output. Download the output you want as the catalogue image and re-encode it to base64 for Odoo’s binary field:

models.execute_kw(DB, uid, API_KEY, "product.template", "write",
                  [[product_id], {"image_1920": b64_result}])

Odoo expects base64 in binary fields. Odoo does not store image dimensions as a standard product field: if you need to keep them, write them to a custom field (x_image_width, x_image_height) or the description.

Full Python snippet

import base64
import json
import xmlrpc.client

import requests

ODOO_URL = "https://my-odoo.example.com"
ODOO_DB = "my_db"
ODOO_USER = "[email protected]"
ODOO_KEY = "odoo_api_key"           # use an API key, not the real password

SC_URL = "https://api.socialcutter.theboomer.dev"
SC_KEY = "sc_your_key"

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("Odoo authentication failed")

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"product-{product['id']}.jpg")

    # Inspect the real shape of each output
    for output in result["outputs"]:
        print(product["id"], output.get("platform"), output.get("format"), output.get("url"))

    # 1:1 output as the main catalogue image
    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("Updated", product["id"], product["name"])

Typical errors

SituationUsual cause
xmlrpc.client.Fault on authenticateWrong database, user or API key
Empty image_1920The product has no image: the smaller sizes derive from image_1920, so they are empty too
SocialCutter 401The X-API-Key header is missing or the key is revoked
SocialCutter 413The original image is over 5 MB
write has no effectThe uid lacks write permission on the model

Cost

  • 1 use per destination (platform and format) per request.
  • Repeated destinations in the same request are not charged twice.
  • Failed processing is refunded.

Processing 100 products for two destinations is 200 uses. For a large catalogue, use the POST /api/v1/images/batch endpoint or spread the work into batches.

Next steps

Frequently asked questions

Which Odoo version do I need?

The flow uses the external XML-RPC API, available in Odoo 14 and later. The large image field is image_1920 since Odoo 13; on older versions the field is called image. Confirm field names on your instance before writing.

How do I authenticate against Odoo?

With xmlrpc.client against /xmlrpc/2/common to get the uid and /xmlrpc/2/object to run methods. Use an Odoo API key as the password instead of the account's real password.

How do I pass the Odoo image to SocialCutter?

Odoo's image field is binary and arrives as base64. Decode it to bytes and upload it as multipart to POST /api/v1/images/process/upload.

What is the difference between image_1920 and image_128?

image_1920 is the editable field: you write the base64 there. image_1024, image_512, image_256 and image_128 are stored, read-only related fields derived from image_1920. Writing the base64 into image_1920 is what makes Odoo generate the smaller sizes; they are never written by hand.

Where do I store the dimensions of the processed image?

Odoo has no standard dimension field on a product. Dimensions come from each destination spec; to keep them, write them to a custom field or the product description.

How much does processing one product cost?

1 use per destination. Processing one product for Instagram post and LinkedIn post is 2 uses.