Por qué la IA en Odoo es más que un módulo de chatbot
La mayoría de los artículos sobre «IA en Odoo» hablan de los asistentes conversacionales que Odoo S.A. está incorporando en las versiones recientes. Eso es solo la capa superficial. Lo que realmente transforma el rendimiento de un equipo comercial o de operaciones es integrar modelos predictivos entrenados sobre los propios datos del negocio: el historial de oportunidades, los patrones de compra, los tiempos de cierre, las características de los clientes que sí convirtieron frente a los que no.
En el proyecto Aplantida entrené y desplegué un modelo propio de visión por computador (arquitectura ViT/MobileNet con knowledge distillation) sobre 171.000 imágenes para clasificación etnobotánica, desplegado directamente en navegador con TensorFlow.js. Esa experiencia con pipelines de datos, entrenamiento, optimización de modelos para inferencia ligera y despliegue en producción es directamente transferible al contexto de Odoo: los principios son los mismos, el dominio es distinto.
Esta guía cubre los casos de uso más rentables de IA sobre Odoo, la arquitectura técnica de integración y un ejemplo completo de lead scoring sobre crm.lead.
Casos de uso reales de IA en Odoo
Antes de hablar de arquitectura, conviene ser concreto sobre qué problemas resuelve la IA en un contexto Odoo real. Estos son los cuatro casos de mayor retorno que implemento en proyectos:
1. Lead scoring en CRM
El modelo predice la probabilidad de que una oportunidad en el pipeline de ventas se cierre con éxito, en qué plazo y por qué importe aproximado. Con ese score, el equipo comercial prioriza los leads más calientes y el manager detecta cuellos de botella antes de que el pipeline se desequilibre. En instalaciones con cientos de leads activos simultáneos, la diferencia entre trabajar con y sin scoring es medible en tasa de conversión en pocas semanas.
2. Forecast de ventas
Los modelos de series temporales (Prophet, LightGBM con features de calendario, o incluso LSTM para patrones complejos) sobre el histórico de pedidos de Odoo permiten generar previsiones de venta con intervalos de confianza. Esto alimenta directamente la planificación de compras y de tesorería: en lugar de un forecast manual que el equipo de ventas actualiza con optimismo variable, tienes una estimación estadística basada en patrones reales.
3. OCR e interpretación de facturas y albaranes
La entrada manual de facturas de proveedor es uno de los grandes sumideros de tiempo en equipos de administración. Un modelo de OCR + extracción de entidades (número de factura, fechas, líneas de producto, importes) integrado en el flujo de Odoo puede automatizar el 70-80 % de las entradas de documentos estándar, dejando solo los casos ambiguos para revisión humana. La integración de APIs de visión (Google Document AI, Azure Form Recognizer) o modelos open source como PaddleOCR con el flujo de account.move en Odoo es técnicamente directa.
4. Clasificación automática de incidencias y tickets
En instalaciones con el módulo de Helpdesk o con muchos mensajes entrantes al CRM, un clasificador de texto (fine-tuned sobre el histórico de tickets etiquetados) puede asignar automáticamente categoría, prioridad y equipo responsable a los nuevos tickets. El ahorro en triaje manual en equipos de soporte con alto volumen es significativo.
Arquitectura de integración: módulo Odoo vs microservicio
La primera decisión de diseño es dónde vive el modelo. Hay dos patrones principales y la elección correcta depende del caso de uso:
Opción A: modelo embebido en un módulo Odoo custom
El modelo se carga en el contexto del worker de Odoo y se invoca directamente desde el código Python del módulo. Es la opción más sencilla para modelos ligeros (scikit-learn, modelos ONNX exportados) que no requieren GPU y cuyo tiempo de inferencia es inferior a un segundo. La ventaja es la simplicidad operativa: no hay servicio adicional que mantener. La desventaja es que cada worker de Odoo carga el modelo en memoria, lo que puede aumentar el uso de RAM de forma significativa si el modelo es grande.
Opción B: microservicio externo con API REST
El modelo vive en un servicio independiente (FastAPI + Uvicorn, típicamente en Docker) que expone un endpoint de predicción. El módulo Odoo hace llamadas HTTP a ese endpoint cuando necesita un score. Es la opción correcta cuando el modelo requiere GPU, tiene tiempo de inferencia alto, necesita reentrenarse frecuentemente sin afectar a Odoo, o cuando se quiere servir el mismo modelo a múltiples aplicaciones.
En mi experiencia, el patrón microservicio es más robusto para producción a escala: desacopla el ciclo de vida del modelo del ciclo de vida del ERP, permite actualizar el modelo sin reiniciar Odoo y facilita el monitoreo independiente de la latencia del modelo.
Pipeline de datos: de Odoo al modelo y de vuelta
El pipeline de datos para un modelo de lead scoring tiene cuatro etapas:
- Extracción: obtener el histórico de oportunidades con sus features y su resultado (ganada/perdida).
- Feature engineering: construir las variables predictoras a partir de los datos crudos.
- Entrenamiento: entrenar y evaluar el modelo offline sobre el histórico.
- Inferencia en tiempo real: calcular el score de cada lead activo y escribirlo de vuelta en Odoo.
Extracción del histórico de leads desde 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
Las features brutas de Odoo necesitan transformarse en variables que el modelo pueda consumir. Estas son las que más poder predictivo aportan en proyectos 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"]]
Entrenamiento del modelo de lead scoring
Para lead scoring recomiendo comenzar con GradientBoostingClassifier (scikit-learn) o LightGBM. Ambos son robustos a features de escala heterogénea, interpretativos via importancia de features y no requieren GPU. El output es una probabilidad entre 0 y 1, que se convierte directamente en el score 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
Microservicio de inferencia con FastAPI
El modelo entrenado se expone como un servicio REST. Odoo invoca este endpoint cuando necesita el score de un lead. FastAPI es la opción estándar en Python para este tipo de servicios: async nativo, validación de input con Pydantic y documentación 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ódulo Odoo: integración del scoring en crm.lead
El módulo custom de Odoo extiende el modelo crm.lead con un campo de score y un método que invoca el microservicio. El score se recalcula automáticamente al guardar el registro y puede lanzarse también desde un botón manual o desde un cron job de Odoo para actualización masiva.
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 reentrenamiento y drift del modelo
Un modelo entrenado hoy empieza a degradarse desde el momento en que el comportamiento del negocio cambia. Los cambios de equipo comercial, de producto, de ciclo económico o de estrategia de ventas son suficientes para que las distribuciones de features cambien y el modelo pierda precisión. Esto se llama data drift y es el principal enemigo de los modelos en producción.
El pipeline de reentrenamiento que implemento en proyectos tiene tres componentes:
- Monitor de drift: cada semana, se compara la distribución de features de los leads nuevos con la distribución del conjunto de entrenamiento usando tests estadísticos (KS-test, Population Stability Index). Si el PSI supera 0.2 en alguna feature, se dispara una alerta.
- Reentrenamiento programado: mensualmente, se extrae el historial actualizado de Odoo, se reentrenan el modelo con los datos más recientes y se evalúa en un hold-out set. Si el AUC mejora o se mantiene, el nuevo modelo reemplaza al anterior en el microservicio.
- A/B testing de modelos: antes de promover un modelo nuevo a producción completa, se dirige una fracción del tráfico al nuevo modelo y se comparan las métricas de negocio reales (no solo el 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
Lecciones de Aplantida: ViT, knowledge distillation y despliegue ligero
En el proyecto Aplantida, el desafío de desplegar un modelo de visión por computador (ViT) en el navegador con TensorFlow.js me obligó a resolver problemas de producción que son directamente aplicables al contexto de Odoo:
- Knowledge distillation: el modelo ViT completo era demasiado pesado para inferencia en el navegador. Se entrenó un modelo «alumno» más compacto (MobileNet con arquitectura reducida) que imitaba las distribuciones de salida del «profesor» (ViT), consiguiendo un 94 % de la precisión con un 15 % del tamaño. Esta misma técnica aplica a modelos de scoring en Odoo: si el tiempo de inferencia del modelo completo es inaceptable, una versión destilada puede dar resultados equivalentes con latencia diez veces menor.
- ONNX como formato de intercambio: exportar el modelo a ONNX permite ejecutarlo con el runtime de ONNX (más rápido que el runtime nativo de scikit-learn o PyTorch para inferencia individual) e independiza el modelo del framework de entrenamiento. Un modelo entrenado con PyTorch en ONNX puede ejecutarse en producción con
onnxruntimesin necesidad de instalar PyTorch en el servidor de inferencia. - Monitoreo de confianza en producción: en Aplantida, las predicciones con confianza inferior a 0.6 se marcan como «inciertas» y se solicita revisión humana. En lead scoring, el mismo patrón aplica: un lead con score entre 0.40 y 0.60 está en la zona gris y debe tratarse de forma diferente a uno con score 0.85.
Consideraciones de infraestructura y seguridad
Algunos puntos prácticos que se pasan por alto en las guías teóricas:
- El microservicio debe estar en la misma red Docker que Odoo, nunca expuesto públicamente. La comunicación interna evita latencia de red externa y elimina la necesidad de autenticación API entre servicios en la misma infraestructura.
- Timeout en las llamadas al microservicio: si el servicio de ML no responde en 5 segundos, Odoo debe continuar sin el score, no bloquearse. El
try/exceptcon timeout es obligatorio, como se ve en el código del módulo anterior. - Los datos de entrenamiento no deben salir del servidor: si los datos de CRM son confidenciales (lo son siempre), el entrenamiento debe ocurrir on-premise o en un entorno cloud con las mismas garantías de protección de datos que el ERP. Nunca envíes historiales de clientes a APIs de terceros para entrenamiento.
- Versionado de modelos: guarda siempre el modelo anterior antes de promover uno nuevo a producción. El rollback a la versión anterior debe ser tan sencillo como cambiar el fichero
.pklque carga el microservicio y reiniciarlo.
Conclusión
Integrar IA en Odoo no es instalar un módulo: es construir un pipeline de datos que conecte el historial del ERP con modelos entrenados y que devuelva predicciones accionables a los usuarios que toman decisiones. La arquitectura correcta —extracción de datos estructurada, feature engineering específico del dominio, modelo entrenado offline y microservicio de inferencia desacoplado— es lo que separa los proyectos de IA que crean valor real de los experimentos que nunca llegan a producción.
La experiencia con 171.000 imágenes en Aplantida y con pipelines de datos masivos en Cymit demuestra que los principios son universales: el dominio cambia, los retos de escalado, de drift y de despliegue no. Si tu equipo comercial trabaja con más leads de los que puede gestionar con atención uniforme, o si tu forecast de ventas se basa en intuición del equipo más que en patrones estadísticos, hay un modelo de ML esperando para cambiar eso.