Integrar IA a Odoo: lead scoring i models predictius amb Python

Guia tècnica completa per connectar models de machine learning a Odoo CRM: des del disseny del pipeline de dades fins al scoring en temps real d'oportunitats comercials

Per què la IA a Odoo és més que un mòdul de chatbot

La majoria d'articles sobre «IA a Odoo» parlen dels assistents conversacionals que Odoo S.A. està incorporant a les versions recents. Això és només la capa superficial. El que realment transforma el rendiment d'un equip comercial o d'operacions és integrar models predictius entrenats sobre les pròpies dades del negoci: l'historial d'oportunitats, els patrons de compra, els temps de tancament, les característiques dels clients que sí van convertir enfront dels que no.

Al projecte Aplantida vaig entrenar i desplegar un model propi de visió per computador (arquitectura ViT/MobileNet amb knowledge distillation) sobre 171.000 imatges per a classificació etnobotànica, desplegat directament al navegador amb TensorFlow.js. Aquesta experiència amb pipelines de dades, entrenament, optimització de models per a inferència lleugera i desplegament en producció és directament transferible al context d'Odoo: els principis són els mateixos, el domini és diferent.

Aquesta guia cobreix els casos d'ús més rendibles de IA sobre Odoo, l'arquitectura tècnica d'integració i un exemple complet de lead scoring sobre crm.lead.

Casos d'ús reals de IA a Odoo

Abans de parlar d'arquitectura, convé ser concret sobre quins problemes resol la IA en un context Odoo real. Aquests són els quatre casos de major retorn que implemento en projectes:

1. Lead scoring al CRM

El model prediu la probabilitat que una oportunitat al pipeline de vendes es tanqui amb èxit, en quin termini i per quin import aproximat. Amb aquesta puntuació, l'equip comercial prioritza els leads més calents i el manager detecta colls d'ampolla abans que el pipeline es desequilibri. En instal·lacions amb centenars de leads actius simultàniament, la diferència entre treballar amb i sense scoring és mesurable en taxa de conversió en poques setmanes.

2. Forecast de vendes

Els models de sèries temporals (Prophet, LightGBM amb features de calendari, o fins i tot LSTM per a patrons complexos) sobre l'historial de comandes d'Odoo permeten generar previsions de venda amb intervals de confiança. Això alimenta directament la planificació de compres i de tresoreria: en lloc d'un forecast manual que l'equip de vendes actualitza amb optimisme variable, tens una estimació estadística basada en patrons reals.

3. OCR i interpretació de factures i albarans

L'entrada manual de factures de proveïdor és un dels grans embornals de temps en equips d'administració. Un model d'OCR + extracció d'entitats (número de factura, dates, línies de producte, imports) integrat al flux d'Odoo pot automatitzar el 70–80% de les entrades de documents estàndard, deixant només els casos ambigus per a revisió humana. La integració d'APIs de visió (Google Document AI, Azure Form Recognizer) o models open source com PaddleOCR amb el flux de account.move a Odoo és tècnicament directa.

4. Classificació automàtica d'incidències i tickets

En instal·lacions amb el mòdul de Helpdesk o amb molts missatges entrants al CRM, un classificador de text (fine-tuned sobre l'historial de tickets etiquetats) pot assignar automàticament categoria, prioritat i equip responsable als nous tickets. L'estalvi en triatge manual en equips de suport d'alt volum és significatiu.

Arquitectura d'integració: mòdul Odoo vs microservei

La primera decisió de disseny és on viu el model. Hi ha dos patrons principals i l'elecció correcta depèn del cas d'ús:

Opció A: model incrustat en un mòdul Odoo custom

El model es carrega en el context del worker d'Odoo i s'invoca directament des del codi Python del mòdul. És l'opció més senzilla per a models lleugers (scikit-learn, models ONNX exportats) que no requereixen GPU i el temps d'inferència dels quals és inferior a un segon. L'avantatge és la simplicitat operativa: no hi ha servei addicional que mantenir. L'inconvenient és que cada worker d'Odoo carrega el model a memòria, la qual cosa pot augmentar l'ús de RAM de forma significativa si el model és gran.

Opció B: microservei extern amb API REST

El model viu en un servei independent (FastAPI + Uvicorn, típicament a Docker) que exposa un endpoint de predicció. El mòdul Odoo fa crides HTTP a aquest endpoint quan necessita una puntuació. És l'opció correcta quan el model requereix GPU, té temps d'inferència alt, necessita reentrenar-se freqüentment sense afectar Odoo, o quan es vol servir el mateix model a múltiples aplicacions.

En la meva experiència, el patró microservei és més robust per a producció a escala: desacobla el cicle de vida del model del cicle de vida de l'ERP, permet actualitzar el model sense reiniciar Odoo i facilita el monitoratge independent de la latència del model.

Pipeline de dades: d'Odoo al model i de tornada

El pipeline de dades per a un model de lead scoring té quatre etapes:

  1. Extracció: obtenir l'historial d'oportunitats amb les seves features i el seu resultat (guanyada/perduda).
  2. Feature engineering: construir les variables predictores a partir de les dades en brut.
  3. Entrenament: entrenar i avaluar el model offline sobre l'historial.
  4. Inferència en temps real: calcular la puntuació de cada lead actiu i escriure-la de tornada a Odoo.

Extracció de l'historial de leads des d'Odoo

import xmlrpc.client
import pandas as pd


def extract_crm_leads(url: str, db: str, uid: int, password: str) -> pd.DataFrame:
    """Extrae el historial de oportunidades cerradas para entrenamiento."""
    models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")

    leads = models.execute_kw(
        db, uid, password,
        "crm.lead", "search_read",
        [[  # dominio: solo oportunidades cerradas
            ["type", "=", "opportunity"],
            ["active", "in", [True, False]],
            ["stage_id.probability", "in", [0, 100]],  # perdidas (0) o ganadas (100)
        ]],
        {
            "fields": [
                "id", "name", "partner_id", "user_id", "team_id",
                "expected_revenue", "priority", "probability",
                "stage_id", "create_date", "date_closed",
                "date_deadline", "country_id", "industry_id",
                "activity_ids", "message_ids",
            ],
            "limit": 10000,
        }
    )

    df = pd.DataFrame(leads)
    df["won"] = (df["probability"] == 100).astype(int)  # variable objetivo
    return df

Feature engineering sobre crm.lead

Les features en brut d'Odoo necessiten transformar-se en variables que el model pugui consumir. Aquestes són les que aporten més poder predictiu en projectes de scoring sobre Odoo:

from datetime import datetime
import numpy as np


def build_features(df: pd.DataFrame) -> pd.DataFrame:
    """Construye features predictoras a partir de datos crudos de crm.lead."""

    # 1. Tiempo en pipeline (días desde creación hasta cierre)
    df["create_date"] = pd.to_datetime(df["create_date"])
    df["date_closed"] = pd.to_datetime(df["date_closed"])
    df["days_in_pipeline"] = (
        (df["date_closed"] - df["create_date"]).dt.days.fillna(0)
    )

    # 2. Tiene fecha límite definida (señal de madurez del lead)
    df["has_deadline"] = df["date_deadline"].notna().astype(int)

    # 3. Importe esperado (log para reducir skewness)
    df["log_revenue"] = np.log1p(df["expected_revenue"].fillna(0))

    # 4. Prioridad (0=normal, 1=alta, 2=muy alta) → ya es numérica
    df["priority"] = df["priority"].astype(int)

    # 5. Número de actividades realizadas (interacción con el lead)
    df["activity_count"] = df["activity_ids"].apply(
        lambda x: len(x) if isinstance(x, list) else 0
    )

    # 6. Número de mensajes (engagement)
    df["message_count"] = df["message_ids"].apply(
        lambda x: len(x) if isinstance(x, list) else 0
    )

    # 7. Comercial asignado (one-hot encoding)
    df["salesperson_id"] = df["user_id"].apply(
        lambda x: x[0] if isinstance(x, list) else 0
    )

    # 8. Etapa actual del pipeline (ordinal encoding)
    stage_order = {
        "Nuevo": 0, "Cualificado": 1, "Propuesta": 2,
        "Negociación": 3, "Ganado": 4
    }
    df["stage_ordinal"] = df["stage_id"].apply(
        lambda x: stage_order.get(x[1] if isinstance(x, list) else "", 0)
    )

    feature_cols = [
        "days_in_pipeline", "has_deadline", "log_revenue",
        "priority", "activity_count", "message_count",
        "salesperson_id", "stage_ordinal",
    ]
    return df[feature_cols + ["won"]]

Entrenament del model de lead scoring

Per a lead scoring recomano començar amb GradientBoostingClassifier (scikit-learn) o LightGBM. Tots dos són robustos a features d'escala heterogènia, interpretatius via importància de features i no requereixen GPU. La sortida és una probabilitat entre 0 i 1, que es converteix directament en la puntuació del lead.

from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score, classification_report
import joblib


def train_lead_scoring_model(df: pd.DataFrame) -> Pipeline:
    """Entrena y evalúa el modelo de scoring. Devuelve el pipeline serializable."""
    feature_cols = [
        "days_in_pipeline", "has_deadline", "log_revenue",
        "priority", "activity_count", "message_count",
        "salesperson_id", "stage_ordinal",
    ]
    X = df[feature_cols].values
    y = df["won"].values

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    pipeline = Pipeline([
        ("scaler", StandardScaler()),
        ("clf", GradientBoostingClassifier(
            n_estimators=200,
            learning_rate=0.05,
            max_depth=4,
            subsample=0.8,
            random_state=42,
        )),
    ])

    pipeline.fit(X_train, y_train)

    # Evaluación
    y_proba = pipeline.predict_proba(X_test)[:, 1]
    auc = roc_auc_score(y_test, y_proba)
    print(f"[Model] ROC-AUC en test: {auc:.4f}")
    print(classification_report(y_test, pipeline.predict(X_test)))

    # Validación cruzada
    cv_scores = cross_val_score(pipeline, X, y, cv=5, scoring="roc_auc")
    print(f"[Model] CV ROC-AUC: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")

    # Guardar el modelo
    joblib.dump(pipeline, "models/lead_scoring_v1.pkl")
    print("[Model] Guardado en models/lead_scoring_v1.pkl")

    return pipeline

Microservei d'inferència amb FastAPI

El model entrenat s'exposa com un servei REST. Odoo invoca aquest endpoint quan necessita la puntuació d'un lead. FastAPI és l'opció estàndard en Python per a aquest tipus de serveis: async natiu, validació d'input amb Pydantic i documentació automàtica.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib
import numpy as np

app = FastAPI(title="Odoo Lead Scoring API", version="1.0.0")

# Cargar el modelo al arrancar (una sola vez)
model = joblib.load("models/lead_scoring_v1.pkl")


class LeadFeatures(BaseModel):
    days_in_pipeline: float
    has_deadline: int
    log_revenue: float
    priority: int
    activity_count: int
    message_count: int
    salesperson_id: int
    stage_ordinal: int


class ScoreResponse(BaseModel):
    lead_id: int
    score: float          # probabilidad de ganar (0.0 - 1.0)
    score_label: str      # "hot" / "warm" / "cold"


def score_to_label(score: float) -> str:
    if score >= 0.70:
        return "hot"
    elif score >= 0.40:
        return "warm"
    return "cold"


@app.post("/score", response_model=ScoreResponse)
def predict_score(lead_id: int, features: LeadFeatures):
    try:
        X = np.array([[
            features.days_in_pipeline,
            features.has_deadline,
            features.log_revenue,
            features.priority,
            features.activity_count,
            features.message_count,
            features.salesperson_id,
            features.stage_ordinal,
        ]])
        proba = model.predict_proba(X)[0, 1]
        return ScoreResponse(
            lead_id=lead_id,
            score=round(float(proba), 4),
            score_label=score_to_label(proba),
        )
    except Exception as exc:
        raise HTTPException(status_code=500, detail=str(exc))


@app.get("/health")
def health():
    return {"status": "ok", "model_loaded": model is not None}

Mòdul Odoo: integració del scoring a crm.lead

El mòdul custom d'Odoo estén el model crm.lead amb un camp de puntuació i un mètode que invoca el microservei. La puntuació es recalcula automàticament en desar el registre i es pot llançar també des d'un botó manual o des d'un cron job d'Odoo per a actualització massiva.

from odoo import models, fields, api
import requests
import logging
import math

_logger = logging.getLogger(__name__)

ML_SERVICE_URL = "http://lead-scoring-api:8001"  # servicio interno en Docker network


class CrmLead(models.Model):
    _inherit = "crm.lead"

    ml_score = fields.Float(
        string="ML Score",
        default=0.0,
        readonly=True,
        help="Probabilidad de cierre predicha por el modelo (0.0 - 1.0)",
    )
    ml_score_label = fields.Selection(
        selection=[("hot", "Caliente"), ("warm", "Templado"), ("cold", "Frío")],
        string="Clasificación ML",
        readonly=True,
    )
    ml_score_date = fields.Datetime(
        string="Última actualización ML",
        readonly=True,
    )

    def _build_ml_features(self) -> dict:
        """Construye el payload de features para el microservicio."""
        create_dt = self.create_date or fields.Datetime.now()
        days = (
            (fields.Datetime.now() - create_dt).days
            if create_dt else 0
        )
        stage_order = {
            "Nuevo": 0, "Cualificado": 1, "Propuesta": 2,
            "Negociación": 3,
        }
        return {
            "days_in_pipeline": days,
            "has_deadline": 1 if self.date_deadline else 0,
            "log_revenue": math.log1p(self.expected_revenue or 0),
            "priority": int(self.priority or 0),
            "activity_count": len(self.activity_ids),
            "message_count": len(self.message_ids),
            "salesperson_id": self.user_id.id or 0,
            "stage_ordinal": stage_order.get(self.stage_id.name, 0),
        }

    def action_compute_ml_score(self):
        """Calcula el score ML para los leads seleccionados."""
        for lead in self:
            if lead.type != "opportunity":
                continue
            try:
                payload = lead._build_ml_features()
                response = requests.post(
                    f"{ML_SERVICE_URL}/score",
                    params={"lead_id": lead.id},
                    json=payload,
                    timeout=5,
                )
                response.raise_for_status()
                result = response.json()
                lead.write({
                    "ml_score": result["score"],
                    "ml_score_label": result["score_label"],
                    "ml_score_date": fields.Datetime.now(),
                })
            except requests.RequestException as exc:
                _logger.warning(
                    "Lead scoring ML falló para lead %s: %s", lead.id, exc
                )

    @api.model
    def cron_update_ml_scores(self):
        """Cron job: actualiza scores de todas las oportunidades activas."""
        leads = self.search([
            ["type", "=", "opportunity"],
            ["active", "=", True],
            ["probability", "not in", [0, 100]],  # excluir cerradas
        ])
        _logger.info("[ML Cron] Actualizando scores de %d oportunidades", len(leads))
        leads.action_compute_ml_score()
        _logger.info("[ML Cron] Scores actualizados.")

Pipeline de reentrenament i drift del model

Un model entrenat avui comença a degradar-se des del moment en què el comportament del negoci canvia. Els canvis d'equip comercial, de producte, de cicle econòmic o d'estratègia de vendes són suficients perquè les distribucions de features canviïn i el model perdi precisió. Això s'anomena data drift i és el principal enemic dels models en producció.

El pipeline de reentrenament que implemento en projectes té tres components:

  • Monitor de drift: cada setmana, es compara la distribució de features dels nous leads amb la distribució del conjunt d'entrenament usant tests estadístics (KS-test, Population Stability Index). Si el PSI supera 0,2 en alguna feature, es dispara una alerta.
  • Reentrenament programat: mensualment, s'extreu l'historial actualitzat d'Odoo, es reentrena el model amb les dades més recents i s'avalua en un hold-out set. Si l'AUC millora o es manté, el nou model reemplaça l'anterior al microservei.
  • A/B testing de models: abans de promocionar un model nou a producció completa, es dirigeix una fracció del trànsit al nou model i es comparen les mètriques de negoci reals (no només l'AUC offline).
from scipy.stats import ks_2samp
import numpy as np


def check_feature_drift(
    train_data: np.ndarray,
    current_data: np.ndarray,
    feature_names: list[str],
    threshold: float = 0.1,
) -> dict:
    """
    Comprueba drift en cada feature con KS test.
    Retorna dict con p-value y alerta por feature.
    """
    results = {}
    for i, fname in enumerate(feature_names):
        stat, pvalue = ks_2samp(train_data[:, i], current_data[:, i])
        drift_detected = pvalue < threshold
        results[fname] = {
            "ks_statistic": round(stat, 4),
            "p_value": round(pvalue, 4),
            "drift_detected": drift_detected,
        }
        if drift_detected:
            print(
                f"[DRIFT ALERT] Feature '{fname}': "
                f"KS={stat:.4f}, p={pvalue:.4f} — reentrenamiento recomendado"
            )
    return results

Lliçons d'Aplantida: ViT, knowledge distillation i desplegament lleuger

Al projecte Aplantida, el repte de desplegar un model de visió per computador (ViT) al navegador amb TensorFlow.js em va obligar a resoldre problemes de producció que són directament aplicables al context d'Odoo:

  • Knowledge distillation: el model ViT complet era massa pesat per a inferència al navegador. Es va entrenar un model «alumne» més compacte (MobileNet amb arquitectura reduïda) que imitava les distribucions de sortida del «professor» (ViT), aconseguint un 94% de la precisió amb un 15% de la mida. Aquesta mateixa tècnica s'aplica a models de scoring a Odoo: si el temps d'inferència del model complet és inacceptable, una versió destil·lada pot donar resultats equivalents amb latència deu vegades menor.
  • ONNX com a format d'intercanvi: exportar el model a ONNX permet executar-lo amb el runtime d'ONNX (més ràpid que el runtime natiu de scikit-learn o PyTorch per a inferència individual) i independitza el model del framework d'entrenament. Un model entrenat amb PyTorch en ONNX pot executar-se en producció amb onnxruntime sense necessitat d'instal·lar PyTorch al servidor d'inferència.
  • Monitoratge de confiança en producció: a Aplantida, les prediccions amb confiança inferior a 0,6 es marquen com a «incertes» i es sol·licita revisió humana. En lead scoring, el mateix patró s'aplica: un lead amb puntuació entre 0,40 i 0,60 es troba a la zona grisa i s'ha de tractar de forma diferent a un amb puntuació 0,85.

Consideracions d'infraestructura i seguretat

Alguns punts pràctics que es passen per alt a les guies teòriques:

  • El microservei ha d'estar a la mateixa xarxa Docker que Odoo, mai exposat públicament. La comunicació interna evita latència de xarxa externa i elimina la necessitat d'autenticació API entre serveis a la mateixa infraestructura.
  • Timeout a les crides al microservei: si el servei de ML no respon en 5 segons, Odoo ha de continuar sense la puntuació, no bloquejar-se. El try/except amb timeout és obligatori, com es veu al codi del mòdul anterior.
  • Les dades d'entrenament no han de sortir del servidor: si les dades de CRM són confidencials (sempre ho són), l'entrenament ha d'ocórrer on-premise o en un entorn cloud amb les mateixes garanties de protecció de dades que l'ERP. Mai enviïs historials de clients a APIs de tercers per a entrenament.
  • Versionat de models: guarda sempre el model anterior abans de promocionar-ne un de nou a producció. El rollback a la versió anterior ha de ser tan senzill com canviar el fitxer .pkl que carrega el microservei i reiniciar-lo.

Conclusió

Integrar IA a Odoo no és instal·lar un mòdul: és construir un pipeline de dades que connecti l'historial de l'ERP amb models entrenats i que retorni prediccions accionables als usuaris que prenen decisions. L'arquitectura correcta —extracció de dades estructurada, feature engineering específic del domini, model entrenat offline i microservei d'inferència desacoblat— és el que separa els projectes de IA que creen valor real dels experiments que mai arriben a producció.

L'experiència amb 171.000 imatges a Aplantida i amb pipelines de dades massius a Cymit demostra que els principis són universals: el domini canvia, els reptes d'escalat, de drift i de desplegament no. Si el teu equip comercial treballa amb més leads dels que pot gestionar amb atenció uniforme, o si el teu forecast de vendes es basa en intuïció de l'equip més que en patrons estadístics, hi ha un model de ML esperant per a canviar-ho.

Vols implementar IA predictiva al teu Odoo?

Sol·licitar auditoria tècnica gratuïta

Business Intelligence sobre Odoo amb Metabase: de la dada bruta al dashboard executiu
Com connectar Metabase al PostgreSQL d'Odoo, modelar KPIs amb dbt i construir dashboards executius de vendes, marge, cashflow i cohorts que la direcció realment utilitza.