Skip to main contentCertyo Developer Portal

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

Integration flowbash
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:

Odoo custom module — models/stock_picking.pypython
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.

certyo_odoo_sync.pypython
"""
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]}...")

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:

Odoo Automated Action — Server Action (Python code)python
# 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")
Acciones automatizadas de Odoo y llamadas de red
Las Acciones automatizadas se ejecutan dentro de una transacción de base de datos. Si la llamada al API de Certyo es lenta o falla, puede bloquear al usuario. Para producción, prefiere el enfoque de middleware externo o usa una acción programada (ir.cron) que procese una cola.

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:

Scheduled action — poll Certyo verificationpython
# 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 OdooCampo de CertyoNotas
stock.picking.namerecordIdpor ejemplo "WH/OUT/00042"
stock.picking.date_donesourceTimestampFormato ISO 8601
(config param)tenantIdConfigurado en Parámetros del sistema de Odoo
"odoo"databaseIdentificador codificado
"stock.picking"collectionNombre del modelo de Odoo
(computed)recordPayloadObjeto JSON con detalles del picking y producto
picking.name + versionidempotencyKeyPreviene 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:

Set Certyo credentials in Odoo System Parametersbash
# 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:

Manufacturing order → Certyopython
# 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",
}
Compatibilidad de versiones de Odoo
Esta guía está dirigida a Odoo 16+ (Community y Enterprise). Para Odoo 15, reemplaza move.quantity con move.quantity_done. Para Odoo 14, los patrones XML-RPC y JSON-RPC son idénticos pero las API Keys requieren habilitar el módulo auth_api_key.

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 · v1.0.0

AI Integration Skill

Download a skill file that enables AI agents to generate working Odoo + Certyo integration code for any language or framework.

v1.0.0
What is this?
A markdown file containing Odoo-specific field mappings, authentication setup, code examples, and integration patterns for Certyo. Drop it into your AI agent's context and ask it to generate integration code.

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