""" 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"""