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: onlyauthenticate, returns theuid./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:
| Field | Max | How it is filled |
|---|---|---|
image_1920 | 1920x1920 | The editable field: you write the base64 here |
image_1024 | 1024x1024 | Stored related field, derived from image_1920 |
image_512 | 512x512 | Stored related field, derived from image_1920 |
image_256 | 256x256 | Stored related field, derived from image_1920 |
image_128 | 128x128 | Stored 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:
| Destination | Dimensions |
|---|---|
| instagram post | 1080x1080 |
| instagram story | 1080x1920 |
| facebook post | 1200x630 |
| linkedin post | 1200x627 |
| twitter post | 1200x675 |
| youtube thumbnail | 1280x720 |
| tiktok cover | 1080x1920 |
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 = "user@example.com"
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
| Situation | Usual cause |
|---|---|
xmlrpc.client.Fault on authenticate | Wrong database, user or API key |
Empty image_1920 | The product has no image: the smaller sizes derive from image_1920, so they are empty too |
SocialCutter 401 | The X-API-Key header is missing or the key is revoked |
SocialCutter 413 | The original image is over 5 MB |
write has no effect | The 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
- API guide with curl: Process images with the API from the terminal
- Python guide: Automate SocialCutter with Python
- ERPNext guide: Normalize catalogue images in ERPNext
- API reference: https://docs.socialcutter.theboomer.dev
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.