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