Why AI in Odoo is more than a chatbot module
Most articles about 'AI in Odoo' discuss the conversational assistants that Odoo S.A. is incorporating into recent versions. That is only the surface layer. What genuinely transforms the performance of a sales or operations team is integrating predictive models trained on the business's own data: the history of opportunities, purchasing patterns, closing timelines, the characteristics of customers who converted versus those who did not.
In the Aplantida project I trained and deployed a custom computer-vision model (ViT/MobileNet architecture with knowledge distillation) on 171,000 images for ethnobotanical classification, deployed directly in the browser with TensorFlow.js. That experience with data pipelines, training, model optimisation for lightweight inference and production deployment is directly transferable to the Odoo context: the principles are the same, the domain is different.
This guide covers the most profitable AI use cases on Odoo, the technical integration architecture and a complete example of lead scoring on crm.lead.
Real AI use cases in Odoo
Before discussing architecture, it is worth being concrete about which problems AI solves in a real Odoo context. These are the four highest-return use cases I implement in projects:
1. Lead scoring in CRM
The model predicts the probability that an opportunity in the sales pipeline will close successfully, within what timeframe and for approximately what amount. With that score, the sales team prioritises the hottest leads and the manager detects bottlenecks before the pipeline becomes unbalanced. In installations with hundreds of simultaneous active leads, the difference between working with and without scoring is measurable in conversion rate within a few weeks.
2. Sales forecast
Time-series models (Prophet, LightGBM with calendar features, or even LSTM for complex patterns) applied to Odoo's order history allow sales forecasts to be generated with confidence intervals. This feeds directly into purchasing and cash planning: instead of a manual forecast that the sales team updates with variable optimism, you get a statistical estimate based on real patterns.
3. OCR and interpretation of invoices and delivery notes
Manual entry of supplier invoices is one of the biggest time sinks for administration teams. An OCR + entity extraction model (invoice number, dates, product lines, amounts) integrated into the Odoo workflow can automate 70–80% of standard document entries, leaving only ambiguous cases for human review. Integrating vision APIs (Google Document AI, Azure Form Recognizer) or open-source models such as PaddleOCR with the account.move flow in Odoo is technically straightforward.
4. Automatic classification of incidents and tickets
In installations with the Helpdesk module or with many incoming messages to the CRM, a text classifier (fine-tuned on the history of labelled tickets) can automatically assign category, priority and responsible team to new tickets. The saving in manual triage in high-volume support teams is significant.
Integration architecture: Odoo module vs microservice
The first design decision is where the model lives. There are two main patterns and the correct choice depends on the use case:
Option A: model embedded in a custom Odoo module
The model is loaded in the context of the Odoo worker and invoked directly from the module's Python code. This is the simplest option for lightweight models (scikit-learn, exported ONNX models) that do not require a GPU and whose inference time is under one second. The advantage is operational simplicity: no additional service to maintain. The disadvantage is that each Odoo worker loads the model into memory, which can significantly increase RAM usage if the model is large.
Option B: external microservice with REST API
The model lives in an independent service (FastAPI + Uvicorn, typically in Docker) that exposes a prediction endpoint. The Odoo module makes HTTP calls to that endpoint whenever it needs a score. This is the correct option when the model requires a GPU, has high inference time, needs to be retrained frequently without affecting Odoo, or when the same model is to be served to multiple applications.
In my experience, the microservice pattern is more robust for production at scale: it decouples the model lifecycle from the ERP lifecycle, allows the model to be updated without restarting Odoo and facilitates independent monitoring of model latency.
Data pipeline: from Odoo to the model and back
The data pipeline for a lead scoring model has four stages:
- Extraction: obtain the opportunity history with its features and outcome (won/lost).
- Feature engineering: build the predictor variables from the raw data.
- Training: train and evaluate the model offline on the history.
- Real-time inference: calculate the score for each active lead and write it back to Odoo.
Extraction of lead history from 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 on crm.lead
Odoo's raw features need to be transformed into variables the model can consume. These are the ones that contribute the most predictive power in Odoo scoring projects:
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"]]
Lead scoring model training
For lead scoring I recommend starting with GradientBoostingClassifier (scikit-learn) or LightGBM. Both are robust to heterogeneous-scale features, interpretable via feature importance and do not require a GPU. The output is a probability between 0 and 1, which converts directly into the lead score.
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
Inference microservice with FastAPI
The trained model is exposed as a REST service. Odoo invokes this endpoint whenever it needs the score of a lead. FastAPI is the standard Python option for this type of service: native async, input validation with Pydantic and automatic documentation.
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}
Odoo module: integrating scoring into crm.lead
The custom Odoo module extends the crm.lead model with a score field and a method that calls the microservice. The score is automatically recalculated when the record is saved and can also be triggered from a manual button or from an Odoo cron job for mass updates.
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.")
Retraining pipeline and model drift
A model trained today starts to degrade from the moment business behaviour changes. Changes in the sales team, product, economic cycle or sales strategy are sufficient for feature distributions to shift and the model to lose accuracy. This is called data drift and is the main enemy of models in production.
The retraining pipeline I implement in projects has three components:
- Drift monitor: every week, the feature distribution of new leads is compared with the training-set distribution using statistical tests (KS-test, Population Stability Index). If the PSI exceeds 0.2 on any feature, an alert is triggered.
- Scheduled retraining: monthly, the updated history is extracted from Odoo, the model is retrained on the most recent data and evaluated on a hold-out set. If the AUC improves or holds, the new model replaces the previous one in the microservice.
- A/B model testing: before promoting a new model to full production, a fraction of traffic is directed to the new model and real business metrics (not just offline AUC) are compared.
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
Lessons from Aplantida: ViT, knowledge distillation and lightweight deployment
In the Aplantida project, the challenge of deploying a computer-vision model (ViT) in the browser with TensorFlow.js forced me to solve production problems that are directly applicable to the Odoo context:
- Knowledge distillation: the full ViT model was too heavy for browser inference. A more compact 'student' model (MobileNet with reduced architecture) was trained to imitate the output distributions of the 'teacher' (ViT), achieving 94% of the accuracy at 15% of the size. This same technique applies to scoring models in Odoo: if the inference time of the full model is unacceptable, a distilled version can deliver equivalent results with ten times lower latency.
- ONNX as an interchange format: exporting the model to ONNX allows it to be run with the ONNX runtime (faster than scikit-learn's or PyTorch's native runtime for individual inference) and decouples the model from the training framework. A model trained with PyTorch and exported to ONNX can run in production with
onnxruntimewithout needing PyTorch installed on the inference server. - Confidence monitoring in production: in Aplantida, predictions with confidence below 0.6 are flagged as 'uncertain' and human review is requested. In lead scoring the same pattern applies: a lead with a score between 0.40 and 0.60 is in the grey zone and should be treated differently from one with a score of 0.85.
Infrastructure and security considerations
Some practical points that theoretical guides overlook:
- The microservice must be on the same Docker network as Odoo, never publicly exposed. Internal communication avoids external network latency and eliminates the need for API authentication between services on the same infrastructure.
- Timeout on calls to the microservice: if the ML service does not respond within 5 seconds, Odoo must continue without the score, not block. The
try/exceptwith timeout is mandatory, as shown in the previous module code. - Training data must not leave the server: if CRM data is confidential (it always is), training must happen on-premise or in a cloud environment with the same data-protection guarantees as the ERP. Never send customer histories to third-party APIs for training.
- Model versioning: always save the previous model before promoting a new one to production. Rolling back to the previous version must be as simple as swapping the
.pklfile the microservice loads and restarting it.
Conclusió
Integrating AI into Odoo is not about installing a module: it is about building a data pipeline that connects the ERP's history with trained models and returns actionable predictions to the users making decisions. The correct architecture — structured data extraction, domain-specific feature engineering, an offline-trained model and a decoupled inference microservice — is what separates AI projects that create real value from experiments that never reach production.
Experience with 171,000 images in Aplantida and with massive data pipelines in Cymit demonstrates that the principles are universal: the domain changes, the challenges of scaling, drift and deployment do not. If your sales team is working with more leads than it can manage with uniform attention, or if your sales forecast is based on team intuition rather than statistical patterns, there is an ML model waiting to change that.