Integración con Odoo
Conecta módulos ERP de Odoo (Inventario, Fabricación, Ventas, Calidad) a Certyo usando XML-RPC, JSON-RPC o la API REST más reciente. Ancla datos de productos y lotes en la blockchain cada vez que se reciben, fabrican o envían mercancías.
Arquitectura
Odoo Server Action / Automated Action
│
▼
Odoo Webhook (or cron job polling)
│
▼
Your middleware (Python / Node.js)
│ ┌─ Transform Odoo record to Certyo payload
│ └─ POST /api/v1/records (X-API-Key header)
▼
Certyo Pipeline
│ Kafka → Accumulate → Merkle tree → IPFS → Polygon
▼
Webhook callback → Update Odoo custom field
(x_certyo_anchor_status, x_certyo_tx_hash)Requisitos previos
- Odoo 16+ (Community o Enterprise) con acceso al API habilitado
- Un usuario de Odoo con clave API o contraseña para XML-RPC / JSON-RPC
- Una clave API de Certyo (
X-API-Key) - Python 3.10+ con xmlrpc.client (stdlib) y requests
Paso 1: Agregar campos personalizados en Odoo
Crea campos personalizados en el modelo stock.picking (orden de entrega) o mrp.production (orden de fabricación) para almacenar los resultados de anclaje de Certyo. Ve a Ajustes → Técnico → Campos o usa un módulo personalizado:
from odoo import fields, models
class StockPicking(models.Model):
_inherit = "stock.picking"
x_certyo_record_hash = fields.Char(
string="Certyo Record Hash",
readonly=True,
copy=False,
)
x_certyo_anchor_status = fields.Selection(
[
("pending", "Pending"),
("anchored", "Anchored"),
("failed", "Failed"),
],
string="Anchor Status",
readonly=True,
copy=False,
)
x_certyo_tx_hash = fields.Char(
string="Polygon Tx Hash",
readonly=True,
copy=False,
)Paso 2: Enviar registros a Certyo
Cuando una orden de entrega es validada (el estado cambia a done), lee los datos del picking vía el API de Odoo y reenvíalos a Certyo. Puedes activar esto con una Acción automatizada de Odoo o un cron job externo.
"""
Odoo → Certyo integration script.
Polls Odoo for validated delivery orders and ingests them into Certyo.
"""
import xmlrpc.client
import requests
from datetime import datetime, timedelta
# --- Odoo connection ---
ODOO_URL = "https://mycompany.odoo.com"
ODOO_DB = "mycompany-prod"
ODOO_USER = "api-user@mycompany.com"
ODOO_API_KEY = "odoo-api-key-here" # Settings → Users → API Keys
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_API_KEY, {})
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
# --- Certyo connection ---
CERTYO_URL = "https://www.certyos.com/api/v1"
CERTYO_API_KEY = "your-certyo-api-key"
CERTYO_TENANT = "mycompany"
def get_recent_pickings():
"""Fetch delivery orders validated in the last hour."""
since = (datetime.utcnow() - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
return models.execute_kw(
ODOO_DB, uid, ODOO_API_KEY,
"stock.picking", "search_read",
[[
("state", "=", "done"),
("picking_type_code", "=", "outgoing"),
("date_done", ">=", since),
("x_certyo_record_hash", "=", False), # not yet ingested
]],
{"fields": [
"name", "partner_id", "origin", "date_done",
"move_ids_without_package", "carrier_tracking_ref",
]},
)
def get_move_lines(picking_id):
"""Fetch product details for a delivery order."""
return models.execute_kw(
ODOO_DB, uid, ODOO_API_KEY,
"stock.move", "search_read",
[[("picking_id", "=", picking_id)]],
{"fields": [
"product_id", "product_uom_qty", "quantity",
"lot_ids", "reference",
]},
)
def ingest_picking(picking):
"""Send a single delivery order to Certyo."""
moves = get_move_lines(picking["id"])
products = []
for move in moves:
products.append({
"productId": move["product_id"][0],
"productName": move["product_id"][1],
"orderedQty": move["product_uom_qty"],
"deliveredQty": move["quantity"],
"lotIds": move.get("lot_ids", []),
})
payload = {
"tenantId": CERTYO_TENANT,
"database": "odoo",
"collection": "stock.picking",
"recordId": picking["name"], # e.g. "WH/OUT/00042"
"recordVersion": "1",
"operationType": "insert",
"recordPayload": {
"pickingName": picking["name"],
"partner": picking["partner_id"][1] if picking["partner_id"] else None,
"sourceDocument": picking.get("origin"),
"dateDone": picking["date_done"],
"trackingRef": picking.get("carrier_tracking_ref"),
"products": products,
},
"sourceTimestamp": picking["date_done"],
"idempotencyKey": f'{picking["name"]}-v1',
}
resp = requests.post(
f"{CERTYO_URL}/records",
json=payload,
headers={
"X-API-Key": CERTYO_API_KEY,
"Content-Type": "application/json",
},
timeout=10,
)
resp.raise_for_status()
return resp.json()
def update_odoo_status(picking_id, certyo_response):
"""Write Certyo hash back to the Odoo record."""
models.execute_kw(
ODOO_DB, uid, ODOO_API_KEY,
"stock.picking", "write",
[[picking_id], {
"x_certyo_record_hash": certyo_response["recordHash"],
"x_certyo_anchor_status": "pending",
}],
)
if __name__ == "__main__":
pickings = get_recent_pickings()
print(f"Found {len(pickings)} new delivery orders")
for picking in pickings:
result = ingest_picking(picking)
update_odoo_status(picking["id"], result)
print(f" ✓ {picking['name']} → {result['recordHash'][:16]}...")"""
Alternative: Use Odoo JSON-RPC instead of XML-RPC.
JSON-RPC is faster for large payloads and avoids XML encoding overhead.
"""
import requests
ODOO_URL = "https://mycompany.odoo.com"
ODOO_DB = "mycompany-prod"
ODOO_USER = "api-user@mycompany.com"
ODOO_API_KEY = "odoo-api-key-here"
def jsonrpc(url, method, params):
"""Make a JSON-RPC 2.0 call to Odoo."""
resp = requests.post(
url,
json={"jsonrpc": "2.0", "method": method, "params": params, "id": 1},
timeout=30,
)
result = resp.json()
if "error" in result:
raise Exception(result["error"]["data"]["message"])
return result["result"]
# Authenticate
uid = jsonrpc(
f"{ODOO_URL}/jsonrpc",
"call",
{
"service": "common",
"method": "authenticate",
"args": [ODOO_DB, ODOO_USER, ODOO_API_KEY, {}],
},
)
# Search for validated pickings
pickings = jsonrpc(
f"{ODOO_URL}/jsonrpc",
"call",
{
"service": "object",
"method": "execute_kw",
"args": [
ODOO_DB, uid, ODOO_API_KEY,
"stock.picking", "search_read",
[[
("state", "=", "done"),
("picking_type_code", "=", "outgoing"),
("x_certyo_record_hash", "=", False),
]],
{"fields": ["name", "partner_id", "origin", "date_done"], "limit": 100},
],
},
)
print(f"Found {len(pickings)} pickings to sync")/**
* Node.js integration: Odoo JSON-RPC → Certyo.
* Uses the odoo-await library for cleaner async/await syntax.
*
* npm install odoo-await node-fetch
*/
const Odoo = require("odoo-await");
const fetch = require("node-fetch");
const CERTYO_URL = "https://www.certyos.com/api/v1";
const CERTYO_API_KEY = "your-certyo-api-key";
const CERTYO_TENANT = "mycompany";
async function main() {
// Connect to Odoo
const odoo = new Odoo({
baseUrl: "https://mycompany.odoo.com",
db: "mycompany-prod",
username: "api-user@mycompany.com",
password: "odoo-api-key-here",
});
await odoo.connect();
// Fetch validated outgoing pickings not yet synced
const pickings = await odoo.searchRead("stock.picking", {
domain: [
["state", "=", "done"],
["picking_type_code", "=", "outgoing"],
["x_certyo_record_hash", "=", false],
],
fields: ["name", "partner_id", "origin", "date_done"],
limit: 100,
});
console.log(`Found ${pickings.length} delivery orders`);
for (const picking of pickings) {
// Ingest into Certyo
const resp = await fetch(`${CERTYO_URL}/records`, {
method: "POST",
headers: {
"X-API-Key": CERTYO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
tenantId: CERTYO_TENANT,
database: "odoo",
collection: "stock.picking",
recordId: picking.name,
recordVersion: "1",
operationType: "insert",
recordPayload: {
pickingName: picking.name,
partner: picking.partner_id?.[1] ?? null,
sourceDocument: picking.origin,
dateDone: picking.date_done,
},
sourceTimestamp: picking.date_done,
idempotencyKey: `${picking.name}-v1`,
}),
});
const result = await resp.json();
console.log(` ✓ ${picking.name} → ${result.recordHash.slice(0, 16)}...`);
// Write hash back to Odoo
await odoo.update("stock.picking", picking.id, {
x_certyo_record_hash: result.recordHash,
x_certyo_anchor_status: "pending",
});
}
}
main().catch(console.error);Paso 3: Acción automatizada (sin middleware)
Para configuraciones más simples, usa una Acción automatizada de Odoo (Ajustes → Técnico → Acciones automatizadas) para llamar a Certyo directamente cuando se valida un picking. Esto evita la necesidad de un script de sincronización externo:
# Triggered: When stock.picking is updated and state == 'done'
# Action type: Execute Python Code
import json
import requests
CERTYO_URL = "https://www.certyos.com/api/v1/records"
CERTYO_API_KEY = env["ir.config_parameter"].sudo().get_param("certyo.api_key")
CERTYO_TENANT = env["ir.config_parameter"].sudo().get_param("certyo.tenant_id")
for picking in records.filtered(
lambda p: p.picking_type_code == "outgoing" and not p.x_certyo_record_hash
):
payload = {
"tenantId": CERTYO_TENANT,
"database": "odoo",
"collection": "stock.picking",
"recordId": picking.name,
"recordVersion": "1",
"operationType": "insert",
"recordPayload": {
"pickingName": picking.name,
"partner": picking.partner_id.name if picking.partner_id else None,
"sourceDocument": picking.origin or "",
"dateDone": str(picking.date_done),
"products": [
{
"productId": move.product_id.id,
"productName": move.product_id.name,
"quantity": move.quantity,
}
for move in picking.move_ids
],
},
"sourceTimestamp": str(picking.date_done),
"idempotencyKey": f"{picking.name}-v1",
}
try:
resp = requests.post(
CERTYO_URL,
json=payload,
headers={"X-API-Key": CERTYO_API_KEY, "Content-Type": "application/json"},
timeout=10,
)
if resp.status_code == 202:
data = resp.json()
picking.sudo().write({
"x_certyo_record_hash": data["recordHash"],
"x_certyo_anchor_status": "pending",
})
log(f"Certyo: {picking.name} ingested → {data['recordHash'][:16]}...")
else:
log(f"Certyo error for {picking.name}: {resp.status_code}", level="warning")
except Exception as e:
log(f"Certyo request failed for {picking.name}: {e}", level="error")Paso 4: Consultar estado de anclaje
Crea una acción programada (ir.cron) que periódicamente consulte los resultados de anclaje en Certyo y actualice los registros de Odoo:
# ir.cron: Run every 5 minutes
# Model: stock.picking
# Python code:
import requests
CERTYO_URL = "https://www.certyos.com/api/v1"
CERTYO_API_KEY = env["ir.config_parameter"].sudo().get_param("certyo.api_key")
CERTYO_TENANT = env["ir.config_parameter"].sudo().get_param("certyo.tenant_id")
pending = env["stock.picking"].search([
("x_certyo_anchor_status", "=", "pending"),
("x_certyo_record_hash", "!=", False),
], limit=50)
for picking in pending:
try:
resp = requests.post(
f"{CERTYO_URL}/verify/record",
json={
"tenantId": CERTYO_TENANT,
"recordId": picking.name,
},
headers={"X-API-Key": CERTYO_API_KEY, "Content-Type": "application/json"},
timeout=10,
)
if resp.status_code == 200:
data = resp.json()
if data.get("verified"):
picking.sudo().write({
"x_certyo_anchor_status": "anchored",
"x_certyo_tx_hash": data.get("transactionHash", ""),
})
log(f"Certyo: {picking.name} anchored ✓")
except Exception as e:
log(f"Certyo verify failed for {picking.name}: {e}", level="warning")Mapeo de campos
| Campo de Odoo | Campo de Certyo | Notas |
|---|---|---|
stock.picking.name | recordId | por ejemplo "WH/OUT/00042" |
stock.picking.date_done | sourceTimestamp | Formato ISO 8601 |
(config param) | tenantId | Configurado en Parámetros del sistema de Odoo |
"odoo" | database | Identificador codificado |
"stock.picking" | collection | Nombre del modelo de Odoo |
(computed) | recordPayload | Objeto JSON con detalles del picking y producto |
picking.name + version | idempotencyKey | Previene ingesta duplicada en reintentos |
Autenticación
Lado de Odoo
Odoo soporta tres métodos de autenticación para acceso al API:
- API Keys (Odoo 14+) — Ve a Ajustes → Usuarios → API Keys. Preferido para integraciones servidor a servidor.
- Contraseña — La contraseña de inicio de sesión del usuario. Funciona pero es menos segura para scripts automatizados.
- OAuth 2.0 (Odoo Enterprise) — Disponible vía el módulo auth_oauth para integraciones SSO.
Lado de Certyo
Pasa tu clave API de Certyo en el encabezado X-API-Key. Almacénala como un Parámetro del sistema de Odoo (ir.config_parameter) para que no esté codificada directamente:
# Via Odoo shell or Settings → Technical → Parameters → System Parameters
env['ir.config_parameter'].set_param('certyo.api_key', 'your-certyo-api-key')
env['ir.config_parameter'].set_param('certyo.tenant_id', 'mycompany')Órdenes de fabricación
El mismo patrón funciona para órdenes de fabricación (mrp.production). Ancla la lista de materiales y el lote de producción cuando una orden de fabricación se marca como completada:
# Same pattern, different model and fields
production = env["mrp.production"].browse(production_id)
payload = {
"tenantId": CERTYO_TENANT,
"database": "odoo",
"collection": "mrp.production",
"recordId": production.name, # e.g. "MO/00015"
"recordVersion": "1",
"operationType": "insert",
"recordPayload": {
"productionName": production.name,
"product": production.product_id.name,
"quantity": production.qty_produced,
"lotNumber": production.lot_producing_id.name if production.lot_producing_id else None,
"billOfMaterials": production.bom_id.display_name,
"components": [
{
"product": move.product_id.name,
"quantity": move.quantity,
"lot": move.lot_ids[0].name if move.lot_ids else None,
}
for move in production.move_raw_ids
],
"dateFinished": str(production.date_finished),
},
"sourceTimestamp": str(production.date_finished),
"idempotencyKey": f"{production.name}-v1",
}Próximos pasos
- Para migración inicial de datos históricos, usa POST /api/v1/records/bulk con lotes de hasta 1000 registros
- Cuando Certyo agregue soporte de webhooks, registra una URL de callback para recibir eventos batch.anchored en lugar de hacer polling
- Integra con Odoo Quality (quality.check) para crear automáticamente controles de calidad con resultados de verificación de Certyo
- Agrega una sección de certificado Certyo a la plantilla de reporte QWeb del albarán de entrega
AI Integration Skill
Download a skill file that enables AI agents to generate working Odoo + Certyo integration code for any language or framework.
What's inside
- Authentication — API Keys de Odoo e ir.config_parameter para almacenamiento de credenciales
- Architecture — stock.picking state=done → cron externo o Acción automatizada → Certyo
- Field mapping — Campos de stock.picking y mrp.production al esquema de registro Certyo
- Code examples — Python XML-RPC, JSON-RPC, Node.js odoo-await, código de servidor de Acción automatizada
- Verification — Acción programada ir.cron para consultar estado de anclaje
- Version handling — Diferencias de nombres de campo en Odoo 14/15/16/17+ y compatibilidad
How to use
Claude Code
Place the file in your project's .claude/commands/ directory, then use it as a slash command:
# Download the skill file
mkdir -p .claude/commands
curl -o .claude/commands/certyo-odoo.md \
https://www.certyos.com/developers/skills/certyo-odoo-skill.md
# Use it in Claude Code
/certyo-odoo "Genera un script Python que sincronice órdenes de entrega de Odoo con Certyo"Cursor / Copilot / Any AI Agent
Add the file to your project root or attach it to a conversation. The AI agent will use the Odoo-specific patterns, field mappings, and code examples to generate correct integration code.
# Add to your project
curl -o CERTYO_ODOO.md \
https://www.certyos.com/developers/skills/certyo-odoo-skill.md
# Then in your AI agent:
"Using the Certyo Odoo spec in CERTYO_ODOO.md,
genera un script python que sincronice órdenes de entrega de odoo con certyo"CLAUDE.md Context File
Append the skill file to your project's CLAUDE.md so every Claude conversation has Odoo + Certyo context automatically.
# Append to your project's CLAUDE.md
echo "" >> CLAUDE.md
echo "## Certyo Odoo Integration" >> CLAUDE.md
cat CERTYO_ODOO.md >> CLAUDE.md