Hai il tuo negozio su Shopify e la tua gestione interna su Odoo. Due sistemi che lavorano in parallelo, aggiornamenti manuali dello stock, ordini da copiare a mano, clienti duplicati in entrambi i database. Non è sostenibile. L’integrazione tra Odoo e Shopify è uno dei progetti di automazione più richiesti nel commercio elettronico B2C e B2B, ed è anche uno di quelli che richiede più accortezze tecniche per essere fatto bene.
Questa guida copre tutti i metodi disponibili, con esempi di codice reali, gli errori più comuni e l’architettura consigliata per i negozi con volumi elevati.
Perché integrare Odoo con Shopify (invece di gestirli separatamente)
La gestione manuale tra due sistemi sembra sostenibile finché non scala. A partire da un certo volume, i costi nascosti sono enormi:
- Errori di stock: Un prodotto venduto su Shopify che non viene scalato in Odoo genera overselling. Un ordine reso che non viene registrato in Odoo genera stock fantasma.
- Tempo operativo: Aggiornare i prezzi, creare ordini di vendita, registrare i clienti… Ogni operazione duplicata in due sistemi è tempo perso.
- Incoerenza dei dati: I clienti hanno indirizzi diversi in Shopify e in Odoo. La contabilità non quadra con le vendite reali.
- Scalabilità bloccata: Con 50 ordini al giorno puoi gestirlo manualmente. Con 500, è impossibile.
L’integrazione risolve tutto questo: un unico flusso di dati che mantiene entrambi i sistemi sincronizzati in tempo reale (o quasi reale) senza intervento manuale. Questo tipo di progetto è un caso tipico di integrazione con Odoo che realizziamo in Soamee.
Quali dati devi sincronizzare
Prima di scegliere il metodo di integrazione, definisci esattamente quali flussi ti servono. I più comuni sono:
Inventario (Odoo → Shopify)
Odoo è la fonte di verità dello stock. Ogni volta che lo stock cambia in Odoo (per acquisto, vendita, rettifica o reso), Shopify deve aggiornarsi. La sincronizzazione può essere in tempo reale (webhook) o periodica (ogni 5-15 minuti).
Ordini (Shopify → Odoo)
Ogni ordine confermato su Shopify deve creare un ordine di vendita in Odoo. Questo include righe di prodotto, sconti, imposte, indirizzo di spedizione e dati del cliente.
Clienti (bidirezionale)
Un cliente che compra per la prima volta su Shopify deve essere creato come partner in Odoo. Se quel cliente ha uno storico in Odoo (per esempio, in un canale B2B precedente), bisogna evitare i duplicati usando email o partita IVA come chiave di deduplicazione.
Prezzi e catalogo (Odoo → Shopify)
Se gestisci i prezzi in Odoo (con listini prezzi, tariffe B2B, sconti per volume), devi sincronizzarli verso Shopify. Questa sincronizzazione è di solito meno frequente (giornaliera o su modifica esplicita).
Contabilità (Shopify → Odoo)
I pagamenti, i rimborsi e le commissioni di Shopify Payments devono riflettersi in Odoo perché la contabilità sia completa. Questo di solito avviene tramite registrazioni contabili automatiche basate sui payout di Shopify.
Metodo 1: API nativa di Odoo + Webhook di Shopify
Questo è il metodo più robusto e flessibile. Richiede sviluppo ma ti dà il controllo totale sulla logica di sincronizzazione.
Architettura del flusso
[Shopify] --webhook--> [Middleware API] --XML-RPC/REST--> [Odoo]
[Odoo] --webhook--> [Middleware API] --Admin API-----> [Shopify]
Il middleware (un servizio Node.js o Python) agisce da orchestratore: riceve i webhook di entrambi i sistemi, trasforma i dati e scrive nell’altro sistema.
Ricevere webhook di Shopify in Python
from flask import Flask, request, jsonify
import hmac
import hashlib
import base64
import xmlrpc.client
import os
app = Flask(__name__)
SHOPIFY_SECRET = os.environ['SHOPIFY_WEBHOOK_SECRET']
ODOO_URL = os.environ['ODOO_URL']
ODOO_DB = os.environ['ODOO_DB']
ODOO_USER = os.environ['ODOO_USER']
ODOO_PASSWORD = os.environ['ODOO_PASSWORD']
def verify_shopify_webhook(data, hmac_header):
"""Verifica che il webhook provenga davvero da Shopify."""
digest = hmac.new(
SHOPIFY_SECRET.encode('utf-8'),
data,
hashlib.sha256
).digest()
computed = base64.b64encode(digest).decode('utf-8')
return hmac.compare_digest(computed, hmac_header)
def get_odoo_connection():
"""Ottiene una connessione autenticata a Odoo."""
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {})
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
return uid, models
@app.route('/webhooks/shopify/orders/create', methods=['POST'])
def shopify_order_created():
# Verificare la firma
hmac_header = request.headers.get('X-Shopify-Hmac-Sha256')
if not verify_shopify_webhook(request.data, hmac_header):
return jsonify({'error': 'Unauthorized'}), 401
order = request.get_json()
uid, models = get_odoo_connection()
# Cercare o creare il cliente in Odoo
email = order['email']
partner_ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'search',
[[['email', '=', email]]]
)
if not partner_ids:
partner_id = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'create',
[{
'name': f"{order['billing_address']['first_name']} {order['billing_address']['last_name']}",
'email': email,
'phone': order['billing_address'].get('phone', ''),
'street': order['billing_address'].get('address1', ''),
'city': order['billing_address'].get('city', ''),
'zip': order['billing_address'].get('zip', ''),
'country_id': get_country_id(models, uid, order['billing_address'].get('country_code')),
}]
)
else:
partner_id = partner_ids[0]
# Creare l'ordine di vendita in Odoo
order_lines = []
for item in order['line_items']:
product_id = find_odoo_product(models, uid, item['sku'])
if product_id:
order_lines.append((0, 0, {
'product_id': product_id,
'product_uom_qty': item['quantity'],
'price_unit': float(item['price']),
'name': item['name'],
}))
sale_order_id = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'sale.order', 'create',
[{
'partner_id': partner_id,
'order_line': order_lines,
'client_order_ref': order['name'], # Numero ordine Shopify
'note': f"Ordine Shopify #{order['order_number']}",
}]
)
# Confermare automaticamente l'ordine
models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'sale.order', 'action_confirm',
[[sale_order_id]]
)
return jsonify({'odoo_order_id': sale_order_id}), 200
def find_odoo_product(models, uid, sku):
"""Cerca il prodotto in Odoo per riferimento interno (SKU)."""
product_ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'product.product', 'search',
[[['default_code', '=', sku]]]
)
return product_ids[0] if product_ids else None
Sincronizzare lo stock da Odoo a Shopify in Node.js
import axios from 'axios';
const SHOPIFY_STORE = process.env.SHOPIFY_STORE; // iltuonegozio.myshopify.com
const SHOPIFY_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const shopifyClient = axios.create({
baseURL: `https://${SHOPIFY_STORE}/admin/api/2024-01`,
headers: {
'X-Shopify-Access-Token': SHOPIFY_TOKEN,
'Content-Type': 'application/json',
},
});
async function syncStockToShopify(odooProduct) {
const { sku, qty_available, shopify_variant_id, shopify_location_id } = odooProduct;
if (!shopify_variant_id) {
console.warn(`Il prodotto ${sku} non ha shopify_variant_id mappato`);
return;
}
// Ottenere inventory_item_id da Shopify
const variantRes = await shopifyClient.get(
`/variants/${shopify_variant_id}.json`
);
const inventoryItemId = variantRes.data.variant.inventory_item_id;
// Aggiornare lo stock
const response = await shopifyClient.post('/inventory_levels/set.json', {
inventory_item_id: inventoryItemId,
location_id: shopify_location_id,
available: Math.max(0, Math.floor(qty_available)),
});
console.log(`Stock aggiornato: ${sku} → ${qty_available} unità`);
return response.data;
}
// Webhook di Odoo quando cambia lo stock (tramite azione automatizzata)
app.post('/webhooks/odoo/stock-update', async (req, res) => {
const { products } = req.body;
const results = await Promise.allSettled(
products.map(product => syncStockToShopify(product))
);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.error(`${failed.length} prodotti non sono stati sincronizzati`);
}
res.json({ synced: results.length - failed.length, failed: failed.length });
});
La chiave di questo metodo è usare gli **SKU di prodotto** come identificatore comune tra Odoo e Shopify. Lo SKU (riferimento interno in Odoo, `default_code`) deve essere lo stesso in entrambi i sistemi. Prima di implementare qualsiasi integrazione, fai un audit del tuo catalogo e assicurati che tutti i prodotti abbiano uno SKU unico e coerente nei due sistemi. Questo passaggio preliminare evita l'80% dei problemi di sincronizzazione.
Metodo 2: Connettori ufficiali e del marketplace di Odoo
Esistono moduli nel marketplace di Odoo Apps (apps.odoo.com) che offrono connettori preconfigurati per Shopify. I più popolari sono:
- Shopify Odoo Connector (vari fornitori come Emipro, Vraja Technologies): moduli che aggiungono un’interfaccia in Odoo per configurare la sincronizzazione senza codice.
- Shopify Integration by OdooTec: soluzione popolare con supporto multi-negozio.
Pro dei connettori del marketplace
- Tempo di implementazione ridotto: Installazione e configurazione in ore, non settimane.
- Manutenzione inclusa: Il fornitore aggiorna il connettore a ogni versione di Odoo e a ogni cambiamento dell’API di Shopify.
- Interfaccia visuale: Non richiede che il team tecnico gestisca codice di integrazione.
- Scenari coperti: La maggior parte dei flussi standard (ordini, stock, clienti, prezzi) è contemplata.
Contro dei connettori del marketplace
- Costo ricorrente: Tra 150€ e 600€/anno per licenza, più il supporto.
- Poca flessibilità: Se la tua logica di business è particolare (per esempio, regole di prezzo complesse, magazzini multipli, logica di spedizione custom), i connettori standard non sempre bastano.
- Dipendenza da terzi: Se il fornitore abbandona la manutenzione del modulo, hai un problema.
- Conflitti con altre personalizzazioni: I moduli di terze parti a volte entrano in conflitto tra loro o con moduli custom propri.
I connettori del marketplace sono ideali per negozi con flussi standard e volume moderato (fino a ~500 ordini/giorno). Per casi più complessi, l’integrazione su misura è di solito un investimento migliore nel lungo periodo.
Metodo 3: Middleware di automazione vs. codice su misura
Make (ex Integromat) e Zapier
Strumenti come Make o Zapier permettono di collegare Shopify e Odoo senza codice tramite flussi visuali. Hanno connettori nativi per entrambi i sistemi.
Quando usare Make/Zapier:
- Volume da basso a medio (meno di 200 ordini/giorno).
- Il team non ha risorse di sviluppo.
- I flussi sono semplici e standard (nuovo ordine → creazione in Odoo, senza logica complessa).
- Fase di validazione prima di investire in un’integrazione su misura.
Quando NON usare Make/Zapier:
- Più di 500 ordini/giorno (i costi di esecuzione scalano rapidamente).
- Ti serve logica complessa: deduplicazione dei clienti, trasformazioni dei dati, gestione degli errori sofisticata.
- Hai requisiti di bassa latenza (sincronizzazione dello stock in tempo reale).
- Ti serve tracciabilità completa e audit di ogni operazione.
Codice su misura
Un servizio di integrazione proprio (Python + FastAPI, Node.js + Express) ti dà il controllo totale. È più costoso da sviluppare inizialmente ma più economico nel lungo periodo per volumi elevati, e molto più flessibile.
# Esempio: logica di deduplicazione dei clienti più sofisticata
# di quella che un connettore standard di solito include
def find_or_create_partner(models, uid, shopify_customer):
"""
Strategia di deduplicazione per priorità:
1. Cercare per email (più affidabile)
2. Cercare per telefono se non c'è email
3. Cercare per nome + indirizzo se non ci sono email né telefono
4. Creare se non esiste nessuna corrispondenza
"""
email = shopify_customer.get('email', '').lower().strip()
phone = shopify_customer.get('phone', '').strip()
# Passo 1: cercare per email
if email:
ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'search',
[[['email', '=ilike', email], ['active', 'in', [True, False]]]]
)
if ids:
return ids[0], 'found_by_email'
# Passo 2: cercare per telefono
if phone:
normalized_phone = ''.join(filter(str.isdigit, phone))[-9:]
ids = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'search',
[[['phone', 'like', normalized_phone]]]
)
if ids:
return ids[0], 'found_by_phone'
# Passo 3: creare un nuovo partner
name = f"{shopify_customer.get('first_name', '')} {shopify_customer.get('last_name', '')}".strip()
new_id = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'res.partner', 'create',
[{
'name': name or 'Cliente senza nome',
'email': email,
'phone': phone,
'customer_rank': 1,
}]
)
return new_id, 'created'
Errori comuni e come evitarli
Stock duplicato o negativo
Problema: Se Shopify e Odoo rettificano lo stock in modo indipendente senza coordinamento, puoi ritrovarti con stock che non esiste o con vendite di prodotti senza stock.
Soluzione: Stabilisci un’unica fonte di verità. Nella maggior parte dei casi, Odoo è il sistema master dell’inventario e Shopify riflette solo ciò che dice Odoo. Non rettificare mai lo stock direttamente in Shopify se hai l’integrazione attiva.
Ordini duplicati
Problema: Un webhook può arrivare due volte (Shopify garantisce una consegna at-least-once). Se non gestisci l’idempotenza, crei l’ordine due volte in Odoo.
Soluzione: Salva l’order_id di Shopify in Odoo (nel campo client_order_ref o in un campo custom) e verifica prima di creare:
def is_order_already_imported(models, uid, shopify_order_id):
"""Verifica se l'ordine di Shopify è già stato importato in Odoo."""
existing = models.execute_kw(
ODOO_DB, uid, ODOO_PASSWORD,
'sale.order', 'search_count',
[[['client_order_ref', '=', f'shopify_{shopify_order_id}']]]
)
return existing > 0
ID di prodotto desincronizzati
Problema: Shopify usa variant_id e product_id. Odoo usa product.product (variante) e product.template (prodotto base). La mappatura non è banale, soprattutto con prodotti con più varianti (taglia, colore).
Soluzione: Mantieni una tabella di mappatura esplicita. Può essere semplice come un campo custom in product.product di Odoo che memorizzi lo shopify_variant_id:
# Aggiungere il campo shopify_variant_id al modello product.product in Odoo
# In un modulo custom (models/product.py):
from odoo import models, fields
class ProductProduct(models.Model):
_inherit = 'product.product'
shopify_variant_id = fields.Char(
string='Shopify Variant ID',
index=True,
copy=False,
)
shopify_product_id = fields.Char(
string='Shopify Product ID',
index=True,
copy=False,
)
Fallimenti silenziosi nei webhook
Problema: Un webhook di Shopify fallisce (timeout, errore 500) e l’ordine non arriva mai a Odoo. Shopify riprova per 48 ore ma se il problema persiste, viene scartato.
Soluzione: Implementa un sistema di compensazione: un processo cron che ogni ora interroga gli ordini di Shopify delle ultime 2 ore e verifica che esistano tutti in Odoo. Se ne manca qualcuno, lo importa.
// Processo di riconciliazione: cron ogni ora
async function reconcileRecentOrders() {
const oneHourAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
// Ottenere gli ordini recenti da Shopify
const { data } = await shopifyClient.get('/orders.json', {
params: {
created_at_min: oneHourAgo,
status: 'any',
limit: 250,
},
});
for (const order of data.orders) {
const exists = await checkOrderInOdoo(order.id);
if (!exists) {
console.warn(`Ordine ${order.name} non trovato in Odoo. Nuova importazione...`);
await importOrderToOdoo(order);
}
}
}
Architettura consigliata per negozi con più di 1000 ordini/giorno
Con volumi elevati, l’architettura deve essere resiliente a picchi, guasti parziali e ritardi di rete:
[Webhook Shopify]
|
v
[Coda di messaggi: Redis / RabbitMQ]
|
v
[Worker di elaborazione (istanze multiple)]
|
+---> [Odoo XML-RPC / REST API]
|
+---> [Database di audit]
|
v
[Cron di riconciliazione (ogni 1h)]
|
v
[Avvisi: Slack / PagerDuty in caso di discrepanze]
Principi chiave:
- Coda di messaggi: I webhook di Shopify scrivono in una coda (Redis Streams, RabbitMQ). I worker elaborano in modo asincrono. Questo evita che un picco di ordini saturi Odoo.
- Worker scalabili: Diversi processi worker che consumano la coda in parallelo. Scalano orizzontalmente in base al carico.
- Idempotenza garantita: Ogni operazione ha una
idempotency_keybasata sull’ID dell’evento di Shopify. Se il worker elabora lo stesso evento due volte, il risultato è lo stesso. - Audit completo: Ogni operazione (webhook ricevuto, ordine creato, stock aggiornato) viene registrata in un database con timestamp, payload e risultato. Fondamentale per il debugging.
- Circuit breaker: Se Odoo non risponde per più di N secondi, il worker smette di riprovare e mette i messaggi in una coda di retry. Questo evita cascate di guasti.
Checklist di implementazione
Prima di mettere in produzione l’integrazione, verifica ogni punto:
Preparazione del catalogo
- Tutti i prodotti hanno uno SKU unico in Odoo (
default_code) - Gli SKU di Odoo coincidono con gli SKU/riferimenti di Shopify
- I prodotti con varianti (taglia, colore) sono mappati correttamente variante per variante
- La tabella di mappatura
shopify_variant_id ↔ odoo_product_idè completa
Configurazione tecnica
- Webhook di Shopify configurati per:
orders/create,orders/updated,orders/cancelled,refunds/create - Verifica della firma HMAC attiva su tutti gli endpoint dei webhook
- Variabili d’ambiente configurate (mai credenziali nel codice)
- Log strutturati attivati con livello INFO o DEBUG in fase di test
Flussi di dati
- Nuovo ordine Shopify → ordine di vendita in Odoo (testato con ordini reali)
- Annullamento in Shopify → annullamento in Odoo
- Reso in Shopify → reso/nota di credito in Odoo
- Modifica dello stock in Odoo → aggiornamento in Shopify (testato con rettifica manuale dell’inventario)
- Nuovo cliente in Shopify → partner in Odoo (senza duplicati)
Resilienza
- Idempotenza verificata (elaborare lo stesso webhook due volte non crea duplicati)
- Processo di riconciliazione periodica attivo
- Avvisi configurati per i fallimenti di sincronizzazione
- Piano di ripristino in caso di caduta di Odoo (la coda di messaggi regge senza perdere dati)
Test di carico
- Simulato un picco di ordini (usare uno Shopify development store + dati di prova)
- Verificato che il tempo di elaborazione di un ordine sia < 30 secondi in condizioni normali
Come può aiutarti Soamee
In Soamee siamo specialisti in integrazioni ERP ed e-commerce. Abbiamo implementato integrazioni Odoo-Shopify per clienti con volumi molto diversi, da negozi in crescita a operazioni con migliaia di ordini al giorno.
Il nostro approccio parte sempre dalla comprensione del tuo caso concreto: quali dati devi sincronizzare, con quale frequenza, quale logica di business particolare hai (magazzini multipli, tariffe diverse per canale, gestione di bundle, dropshipping parziale). Solo allora raccomandiamo il metodo adeguato, che sia un connettore standard, un middleware su misura o un’architettura basata su eventi.
Se hai bisogno di un ERP su misura o stai valutando di integrare Odoo con Shopify, prenota una consulenza gratuita. In 45 minuti possiamo darti una roadmap chiara con opzioni, stima dello sforzo e raccomandazione tecnica.
Conclusione
L’integrazione tra Odoo e Shopify non è una questione di “se” ma di “come”. Il metodo giusto dipende dal tuo volume, dalla complessità della tua logica di business e dalle tue risorse tecniche:
- Connettori del marketplace: Ideali per partire velocemente con flussi standard e volume moderato.
- Make/Zapier: Per validare l’esigenza senza investimenti in sviluppo, con volumi bassi.
- Integrazione su misura con API: L’opzione più robusta e flessibile per operazioni che scalano, con logica propria e resilienza reale.
Qualunque sia il metodo che scegli, i pilastri sono gli stessi: SKU come chiave di mappatura, idempotenza in tutti i flussi, fonte di verità unica per lo stock, e un processo di riconciliazione che rilevi e corregga le discrepanze prima che diventino problemi per il cliente.