MCP_server_Odoo / crm_gradio_tools.py
Aktraiser
Finish
390d6bf
"""
Fonctions CRM natives pour Gradio
=================================
Interface simplifiée pour récupérer et afficher les données CRM Odoo.
Les prédictions ML sont gérées par le package modal_tools.
"""
import logging
from typing import Dict, List, Any
from datetime import datetime, timedelta
import json
# Import du client Odoo
import config
logger = logging.getLogger(__name__)
# =============================================================================
# FONCTIONS UTILITAIRES SIMPLES
# =============================================================================
def _format_currency(amount: float) -> str:
"""Formate un montant en euros"""
return f"{amount:,.2f} €"
def _format_percentage(value: float) -> str:
"""Formate un pourcentage"""
return f"{value:.1f}%"
def _safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Division sécurisée évitant la division par zéro"""
return numerator / denominator if denominator != 0 else default
def _get_stage_name(stage_data) -> str:
"""Extrait le nom de l'étape depuis les données Odoo"""
if isinstance(stage_data, (list, tuple)) and len(stage_data) > 1:
return stage_data[1]
return "N/A"
# =============================================================================
# FONCTIONS PRINCIPALES CRM POUR GRADIO
# =============================================================================
def get_crm_statistics() -> str:
"""
Récupère les statistiques de base des leads CRM depuis Odoo.
Returns:
str: Statistiques CRM formatées
"""
client = config.client
if not client or not client.is_connected():
return "❌ **Client Odoo non connecté**\n\nCliquez 'Enregistrer' dans la section connexion pour vous connecter d'abord."
try:
logger.info("📊 Calcul des statistiques CRM...")
# Statistiques de base
stats_data = client.read_group(
'crm.lead', [('id', '>', 0)],
['expected_revenue:sum'], []
)
ca_espere_total = float(stats_data[0]['expected_revenue']) if stats_data and stats_data[0]['expected_revenue'] else 0.0
# Comptages de base
total_leads = client.search_count('crm.lead', [])
leads_chauds = client.search_count('crm.lead', [('expected_revenue', '>', 10000)])
# Leads gagnés
won_stages = client.search('crm.stage', [('name', 'ilike', 'gagné')])
leads_gagnes = 0
ca_realise = 0.0
if won_stages:
leads_gagnes = client.search_count('crm.lead', [('stage_id', 'in', won_stages)])
won_stats = client.read_group(
'crm.lead',
[('stage_id', 'in', won_stages)],
['expected_revenue:sum'], []
)
ca_realise = float(won_stats[0]['expected_revenue']) if won_stats and won_stats[0]['expected_revenue'] else 0.0
# Métriques calculées
taux_conversion = _safe_divide(leads_gagnes, total_leads) * 100
revenue_moyen = _safe_divide(ca_espere_total, total_leads)
# Répartition par étapes
stages_data = client.read_group(
'crm.lead', [],
['stage_id'], ['stage_id']
)
response = f"""📊 **STATISTIQUES CRM ODOO**
💼 **VUE D'ENSEMBLE**:
• **Total leads**: {total_leads:,}
• **Leads haute valeur** (>10K€): {leads_chauds:,}
• **Leads gagnés**: {leads_gagnes:,}
💰 **CHIFFRE D'AFFAIRES**:
• **CA attendu total**: {_format_currency(ca_espere_total)}
• **CA réalisé**: {_format_currency(ca_realise)}
• **Revenue moyen/lead**: {_format_currency(revenue_moyen)}
📈 **PERFORMANCE**:
• **Taux de conversion**: {_format_percentage(taux_conversion)}
📊 **RÉPARTITION PAR ÉTAPES**:"""
# Ajouter les étapes actives
for stage_data in stages_data[:5]:
stage_name = _get_stage_name(stage_data.get('stage_id', [None, 'N/A']))
count = stage_data.get('stage_id_count', 0)
response += f"\n• **{stage_name}**: {count:,} leads"
response += f"\n\n📅 **Dernière mise à jour**: {datetime.now().strftime('%d/%m/%Y %H:%M')}"
logger.info("✅ Statistiques CRM calculées avec succès")
return response
except Exception as e:
logger.error(f"❌ Erreur get_crm_statistics: {e}")
return f"❌ **Erreur lors de la récupération des statistiques**: {str(e)}"
def analyze_leads_advanced(domain_filter: str = "[]", limit: int = 20) -> str:
"""
Liste des leads avec informations de base (sans prédiction ML).
Les prédictions sont disponibles via modal_tools.
Args:
domain_filter: Filtre de domaine Odoo au format JSON
limit: Nombre maximum de leads à analyser
Returns:
str: Liste des leads formatée
"""
client = config.client
if not client or not client.is_connected():
return "❌ **Client Odoo non connecté**\n\nCliquez 'Enregistrer' dans la section connexion pour vous connecter d'abord."
try:
# Validation limite
validated_limit = max(5, min(limit, 50))
# Parser le domaine
try:
domain = json.loads(domain_filter) if domain_filter.strip() != "[]" else []
except json.JSONDecodeError:
return "❌ **Format de domaine invalide**\n\nUtilisez le format JSON: [['field', '=', 'value']]"
logger.info(f"📋 Récupération de {validated_limit} leads...")
# Récupération des données
fields = [
'name', 'email_from', 'phone', 'expected_revenue',
'stage_id', 'create_date', 'description'
]
leads_data = client.search_read('crm.lead', domain, fields, limit=validated_limit)
if not leads_data:
return "⚠️ **Aucun lead trouvé**\n\nVérifiez vos critères de recherche."
# Calculs de base
total_revenue = sum(l.get('expected_revenue', 0) or 0 for l in leads_data)
high_value_leads = [l for l in leads_data if (l.get('expected_revenue', 0) or 0) > 10000]
complete_leads = [l for l in leads_data if l.get('email_from') and l.get('phone')]
response = f"""📋 **ANALYSE LEADS CRM**
📊 **RÉSUMÉ**:
• **Leads analysés**: {len(leads_data):,}
• **Revenue total**: {_format_currency(total_revenue)}
• **Revenue moyen**: {_format_currency(_safe_divide(total_revenue, len(leads_data)))}
• **Leads haute valeur**: {len(high_value_leads):,}
• **Leads avec contact complet**: {len(complete_leads):,}
📋 **DÉTAILS DES LEADS**:"""
# Afficher les leads triés par revenue
sorted_leads = sorted(leads_data, key=lambda x: x.get('expected_revenue', 0) or 0, reverse=True)
for i, lead in enumerate(sorted_leads[:10], 1):
name = lead.get('name', 'N/A')
revenue = lead.get('expected_revenue', 0) or 0
stage_name = _get_stage_name(lead.get('stage_id', [None, 'N/A']))
email = lead.get('email_from', 'N/A')
phone = lead.get('phone', 'N/A')
# Indicateur simple basé sur le revenue
indicator = "🔥" if revenue > 50000 else "🌟" if revenue > 10000 else "📊"
response += f"""
**{i}. {indicator} {name}**
• 💰 **Revenue**: {_format_currency(revenue)}
• 📊 **Étape**: {stage_name}
• 📧 **Email**: {email}
• 📞 **Téléphone**: {phone}"""
if len(sorted_leads) > 10:
response += f"\n\n... et {len(sorted_leads) - 10} autres leads"
response += f"""
💡 **INFORMATIONS**:
• Pour des **prédictions ML avancées**, utilisez le package **modal_tools**
• Cette vue montre les **données brutes Odoo** uniquement
• Les leads sont triés par revenue attendu"""
logger.info(f"✅ Analyse de {len(leads_data)} leads terminée")
return response
except Exception as e:
logger.error(f"❌ Erreur analyze_leads_advanced: {e}")
return f"❌ **Erreur lors de l'analyse**: {str(e)}"
def monitor_crm_performance(time_window_hours: int = 24, alert_threshold: float = 0.7) -> str:
"""
Surveille l'activité CRM basique sur une période donnée.
Args:
time_window_hours: Fenêtre de temps en heures
alert_threshold: Seuil d'alerte (non utilisé dans cette version simple)
Returns:
str: Rapport d'activité
"""
client = config.client
if not client or not client.is_connected():
return "❌ **Client Odoo non connecté**\n\nCliquez 'Enregistrer' dans la section connexion pour vous connecter d'abord."
try:
# Validation
validated_hours = max(1, min(time_window_hours, 168)) # Max 1 semaine
logger.info(f"📊 Monitoring CRM sur les dernières {validated_hours}h...")
# Période de monitoring
start_date = (datetime.now() - timedelta(hours=validated_hours)).strftime('%Y-%m-%d %H:%M:%S')
# Leads récents
recent_leads = client.search_read(
'crm.lead',
[('create_date', '>=', start_date)],
['name', 'expected_revenue', 'email_from', 'phone'],
limit=100
)
# Leads modifiés
updated_leads = client.search_read(
'crm.lead',
[('date_last_stage_update', '>=', start_date)],
['name', 'expected_revenue'],
limit=100
)
# Métriques
total_nouveaux = len(recent_leads)
total_modifies = len(updated_leads)
revenue_nouveaux = sum(l.get('expected_revenue', 0) or 0 for l in recent_leads)
revenue_moyen = _safe_divide(revenue_nouveaux, total_nouveaux)
# Leads avec données complètes
complete_leads = [l for l in recent_leads if l.get('email_from') and l.get('phone')]
taux_completude = _safe_divide(len(complete_leads), total_nouveaux) * 100
response = f"""📊 **MONITORING CRM**
⏰ **PÉRIODE**: Dernières {validated_hours}h
📈 **ACTIVITÉ**:
• **Nouveaux leads**: {total_nouveaux:,}
• **Leads modifiés**: {total_modifies:,}
• **Revenue nouveaux leads**: {_format_currency(revenue_nouveaux)}
• **Revenue moyen**: {_format_currency(revenue_moyen)}
• **Taux complétude données**: {_format_percentage(taux_completude)}
💡 **OBSERVATIONS**:"""
if total_nouveaux == 0:
response += "\n• ⚠️ **Aucun nouveau lead** sur la période"
elif total_nouveaux < 5 and validated_hours >= 24:
response += "\n• ⚠️ **Faible activité** de prospection"
else:
response += "\n• ✅ **Activité normale** de prospection"
if taux_completude < 50:
response += "\n• ⚠️ **Données incomplètes** - Améliorer la qualification"
else:
response += "\n• ✅ **Bonne qualité** des données"
if revenue_moyen < 5000 and total_nouveaux > 0:
response += "\n• 📊 **Revenue moyen faible** - Cibler des prospects premium"
response += f"\n\n📅 **Généré le**: {datetime.now().strftime('%d/%m/%Y %H:%M')}"
logger.info(f"✅ Monitoring effectué sur {validated_hours}h")
return response
except Exception as e:
logger.error(f"❌ Erreur monitor_crm_performance: {e}")
return f"❌ **Erreur lors du monitoring**: {str(e)}"
def search_leads_by_criteria(search_name: str = "", min_revenue: float = 0, stage_filter: str = "", limit: int = 10) -> str:
"""
Recherche des leads selon différents critères.
Args:
search_name: Nom ou partie du nom du lead
min_revenue: Revenue minimum attendu
stage_filter: Filtre sur l'étape
limit: Nombre maximum de résultats
Returns:
str: Liste des leads trouvés
"""
client = config.client
if not client or not client.is_connected():
return "❌ **Client Odoo non connecté**\n\nCliquez 'Enregistrer' dans la section connexion pour vous connecter d'abord."
try:
# Construire le domaine de recherche
domain = []
if search_name.strip():
domain.append(['name', 'ilike', search_name.strip()])
if min_revenue > 0:
domain.append(['expected_revenue', '>=', min_revenue])
if stage_filter.strip():
domain.append(['stage_id', 'ilike', stage_filter.strip()])
# Récupération
fields = [
'name', 'email_from', 'phone', 'expected_revenue',
'stage_id', 'create_date', 'description'
]
leads = client.search_read('crm.lead', domain, fields, limit)
if not leads:
criteres = []
if search_name.strip():
criteres.append(f"nom contenant '{search_name}'")
if min_revenue > 0:
criteres.append(f"revenue >= {_format_currency(min_revenue)}")
if stage_filter.strip():
criteres.append(f"étape contenant '{stage_filter}'")
criteres_text = " ET ".join(criteres) if criteres else "aucun critère"
return f"🔍 **Aucun lead trouvé**\n\nCritères: {criteres_text}"
# Résumé
total_revenue = sum(l.get('expected_revenue', 0) or 0 for l in leads)
complete_leads = [l for l in leads if l.get('email_from') and l.get('phone')]
response = f"""🔍 **{len(leads)} LEAD(S) TROUVÉ(S)**
📊 **RÉSUMÉ**:
• **Revenue total**: {_format_currency(total_revenue)}
• **Revenue moyen**: {_format_currency(_safe_divide(total_revenue, len(leads)))}
• **Leads avec contact**: {len(complete_leads)}/{len(leads)}
📋 **DÉTAILS**:"""
# Détails de chaque lead
for i, lead in enumerate(leads, 1):
name = lead.get('name', 'N/A')
email = lead.get('email_from', 'N/A')
phone = lead.get('phone', 'N/A')
revenue = lead.get('expected_revenue', 0) or 0
stage_name = _get_stage_name(lead.get('stage_id', [None, 'N/A']))
# Indicateur simple
indicator = "🔥" if revenue > 50000 else "🌟" if revenue > 10000 else "📊"
# Age du lead
create_date = lead.get('create_date')
age_text = "N/A"
if create_date:
try:
created = datetime.fromisoformat(create_date.replace('Z', '+00:00'))
age_days = (datetime.now() - created.replace(tzinfo=None)).days
age_text = f"{age_days} jour(s)"
except:
age_text = "N/A"
response += f"""
**{i}. {indicator} {name}**
• 💰 **Revenue**: {_format_currency(revenue)}
• 📊 **Étape**: {stage_name}
• 📧 **Email**: {email}
• 📞 **Téléphone**: {phone}
• 📅 **Âge**: {age_text}"""
# Recommandations simples
response += "\n\n💼 **ACTIONS SUGGÉRÉES**:\n"
high_value = [l for l in leads if (l.get('expected_revenue', 0) or 0) >= 50000]
if high_value:
response += f"• 🔥 **Prioriser** {len(high_value)} lead(s) très haute valeur\n"
incomplete = [l for l in leads if not (l.get('email_from') and l.get('phone'))]
if incomplete:
response += f"• 📝 **Compléter** les informations de {len(incomplete)} lead(s)\n"
if len(leads) >= limit:
response += f"• 🔍 **Affiner** la recherche (limite de {limit} atteinte)\n"
logger.info(f"✅ Recherche de {len(leads)} leads terminée")
return response
except Exception as e:
logger.error(f"❌ Erreur search_leads_by_criteria: {e}")
return f"❌ **Erreur lors de la recherche**: {str(e)}"
def get_crm_tools_info() -> str:
"""
Informations sur les fonctions CRM disponibles.
Returns:
str: Description des fonctions CRM
"""
return """🛠️ **FONCTIONS CRM POUR GRADIO**
🎯 **FONCTIONS DISPONIBLES**:
📊 **get_crm_statistics()**
• Statistiques de base du pipeline CRM
• Métriques de conversion et revenue
• Répartition par étapes
📋 **analyze_leads_advanced(domain_filter, limit)**
• Liste des leads avec informations de base
• Tri par revenue et indicateurs simples
• Pas de prédiction ML (voir modal_tools)
📊 **monitor_crm_performance(time_window_hours, alert_threshold)**
• Surveillance de l'activité CRM
• Métriques sur une période donnée
• Observations sur la qualité des données
🔍 **search_leads_by_criteria(name, min_revenue, stage, limit)**
• Recherche multi-critères
• Affichage détaillé des résultats
• Suggestions d'actions simples
💡 **IMPORTANT**:
• Ces fonctions affichent les **données Odoo brutes**
• Pour les **prédictions ML**, utilisez **modal_tools**
• Interface optimisée pour Gradio et MCP
• Pas de doublons avec les outils d'IA existants
🚀 **COMPLÉMENTARITÉ**:
• **CRM tools** → Données Odoo de base
• **Modal tools** → Prédictions et analyses ML
• **Sales tools** → Gestion des devis et commandes"""