Spaces:
Running
Running
""" | |
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""" |