Integrating AI into Odoo: lead scoring and predictive models with Python
A complete technical guide to connecting machine learning models to Odoo CRM: from designing the data pipeline to real-time scoring of sales opportunities
Why AI in Odoo is more than a chatbot module
Most articles about "AI in Odoo" talk about the conversational assistants that Odoo S.A. is incorporating into recent versions. That is only the surface layer. What really transforms the performance of a sales or operations team is integrating predictive models trained on the business's own data: the history of opportunities, the purchasing patterns, the closing times, the characteristics of the customers who did convert versus those who did not.
On the Aplantida project I trained and deployed my own computer vision model (a 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, optimising models for lightweight inference and deploying to production 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 top of Odoo, the technical integration architecture and a complete lead scoring example on crm.lead.
Real AI use cases in Odoo
Before talking about architecture, it is worth being concrete about which problems AI solves in a real Odoo context. These are the four highest-return 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 roughly what amount. With that score, the sales team prioritises the hottest leads and the manager spots bottlenecks before the pipeline gets out of balance. In installations with hundreds of simultaneously 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) over Odoo's order history let you generate sales forecasts with confidence intervals. This feeds directly into purchasing and cash-flow planning: instead of a manual forecast that the sales team updates with variable optimism, you have 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 great time sinks in administration teams. An OCR + entity extraction model (invoice number, dates, product lines, amounts) integrated into the Odoo flow can automate 70-80 % of standard document entries, leaving only the 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 issues 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 on 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 right choice depends on the use case:
Option A: model embedded in a custom Odoo module
The model is loaded into the context of the Odoo worker and invoked directly from the module's Python code. It is the simplest option for lightweight models (scikit-learn, exported ONNX models) that do not require a GPU and whose inference time is under a second. The advantage is operational simplicity: there is no additional service to maintain. The downside is that each Odoo worker loads the model into memory, which can increase RAM usage significantly if the model is large.
Option B: external microservice with a REST API
The model lives in a standalone service (FastAPI + Uvicorn, typically in Docker) that exposes a prediction endpoint. The Odoo module makes HTTP calls to that endpoint when it needs a score. It is the right option when the model requires a GPU, has a high inference time, needs to be retrained frequently without affecting Odoo, or when you want to serve the same model to multiple applications.
In my experience, the microservice pattern is more robust for production at scale: it decouples the model's lifecycle from the ERP's lifecycle, lets you update the model without restarting Odoo and makes it easier to monitor the model's latency independently.
Data pipeline: from Odoo to the model and back
The data pipeline for a lead scoring model has four stages:
- Extraction: obtain the history of opportunities with their features and their 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: compute the score of each active lead and write it back into Odoo.
Extracting the 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 dfFeature engineering on crm.lead
Odoo's raw features need to be transformed into variables the model can consume. These are the ones that provide the most predictive power in scoring projects on 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"]]Training the lead scoring model
For lead scoring I recommend starting with GradientBoostingClassifier (scikit-learn) or LightGBM. Both are robust to features of heterogeneous scale, 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's 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 pipelineInference microservice with FastAPI
The trained model is exposed as a REST service. Odoo invokes this endpoint when it needs a lead's score. FastAPI is the standard option in Python for this kind 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 the scoring into crm.lead
The custom Odoo module extends the crm.lead model with a score field and a method that invokes the microservice. The score is recomputed automatically when the record is saved and can also be launched from a manual button or from an Odoo cron job for bulk 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 the business's behaviour changes. Changes in the sales team, the product, the economic cycle or the sales strategy are enough for the feature distributions to change and the model to lose accuracy. This is called data drift and it 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 the new leads is compared against the distribution of the training set 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 with 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 testing of models: before promoting a new model to full production, a fraction of the traffic is directed to the new model and the real business metrics are compared (not just the offline AUC).
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 resultsLessons from Aplantida: ViT, knowledge distillation and lightweight deployment
On 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 inference in the browser. A more compact "student" model (MobileNet with a 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 give equivalent results with ten times lower latency.
- ONNX as an interchange format: exporting the model to ONNX lets you run it with the ONNX runtime (faster than the native scikit-learn or PyTorch runtime for individual inference) and makes the model independent of the training framework. A model trained with PyTorch and exported to ONNX can run in production with onnxruntime without needing to install PyTorch 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 handled differently from one with a score of 0.85.
Infrastructure and security considerations
Some practical points that get overlooked in theoretical guides:
- The microservice must be on the same Docker network as Odoo, never publicly exposed. Internal communication avoids external network latency and removes the need for API authentication between services on the same infrastructure.
- Timeout on the calls to the microservice: if the ML service does not respond within 5 seconds, Odoo must continue without the score, not block. The try/except with a timeout is mandatory, as seen in the module code above.
- Training data must not leave the server: if the 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 keep the previous model before promoting a new one to production. Rolling back to the previous version should be as simple as changing the .pkl file the microservice loads and restarting it.
Conclusion
Integrating AI into Odoo is not installing a module: it is building a data pipeline that connects the ERP's history with trained models and returns actionable predictions to the users who make decisions. The right 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.
The experience with 171.000 images in Aplantida and with massive data pipelines in Cymit shows that the principles are universal: the domain changes, the challenges of scaling, drift and deployment do not. If your sales team works with more leads than it can handle with uniform attention, or if your sales forecast is based on the team's intuition rather than statistical patterns, there is an ML model waiting to change that.