first commit
This commit is contained in:
commit
e6c52820cd
227 changed files with 16156 additions and 0 deletions
5
modules/crm/__init__.py/init.py
Normal file
5
modules/crm/__init__.py/init.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .client import Client
|
||||
from .handler import ClientHandler
|
||||
from .prospect import Prospect
|
||||
from .prospect_handler import ProspectHandler
|
||||
from .cli import main
|
||||
849
modules/crm/cli.py
Normal file
849
modules/crm/cli.py
Normal file
|
|
@ -0,0 +1,849 @@
|
|||
import os
|
||||
from .client import Client
|
||||
from .handler import ClientHandler
|
||||
from .prospect import Prospect
|
||||
from .prospect_handler import ProspectHandler
|
||||
from datetime import date
|
||||
|
||||
def clear_screen():
|
||||
"""Efface l'écran du terminal"""
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def main():
|
||||
"""Point d'entrée pour l'application CRM"""
|
||||
client_handler = ClientHandler()
|
||||
prospect_handler = ProspectHandler()
|
||||
|
||||
while True:
|
||||
clear_screen()
|
||||
print("=== CRM - Gestion des Relations Clients ===\n")
|
||||
|
||||
print("1. Gestion des Clients")
|
||||
print("2. Gestion des Prospects")
|
||||
print("3. Tableau de bord")
|
||||
print("4. Retour au menu principal")
|
||||
|
||||
choice = input("\nSélectionnez une option (1-4): ")
|
||||
|
||||
if choice == "1":
|
||||
# Gestion des clients
|
||||
manage_clients(client_handler, prospect_handler)
|
||||
elif choice == "2":
|
||||
# Gestion des prospects
|
||||
manage_prospects(client_handler, prospect_handler)
|
||||
elif choice == "3":
|
||||
# Tableau de bord
|
||||
show_dashboard(client_handler, prospect_handler)
|
||||
elif choice == "4":
|
||||
# Retour au menu principal
|
||||
print("\nRetour au menu principal...")
|
||||
break
|
||||
else:
|
||||
print("\nOption invalide. Veuillez réessayer.")
|
||||
|
||||
input("\nAppuyez sur Entrée pour continuer...")
|
||||
|
||||
def display_all_clients(client_handler):
|
||||
"""Affiche tous les clients"""
|
||||
clear_screen()
|
||||
print("=== Liste des Clients ===\n")
|
||||
|
||||
clients = client_handler.get_all_clients()
|
||||
|
||||
if not clients:
|
||||
print("Aucun client trouvé.")
|
||||
return
|
||||
|
||||
for i, client in enumerate(clients, 1):
|
||||
print(f"{i}. {client.name} ({client.company})")
|
||||
print(f" Email: {client.email} | Téléphone: {client.phone}")
|
||||
print(f" Dernière interaction: {client.last_contact}")
|
||||
print(f" Documents: Devis ({len(client.linked_docs['devis'])}) | "
|
||||
f"Propositions ({len(client.linked_docs['propositions'])}) | "
|
||||
f"Factures ({len(client.linked_docs['factures'])})")
|
||||
print("")
|
||||
|
||||
def search_clients(client_handler):
|
||||
"""Recherche des clients par nom"""
|
||||
clear_screen()
|
||||
print("=== Recherche de Clients ===\n")
|
||||
|
||||
search_term = input("Entrez un terme de recherche (nom, email, entreprise): ")
|
||||
|
||||
if not search_term:
|
||||
print("Recherche annulée.")
|
||||
return
|
||||
|
||||
results = []
|
||||
clients = client_handler.get_all_clients()
|
||||
|
||||
for client in clients:
|
||||
if (search_term.lower() in client.name.lower() or
|
||||
search_term.lower() in client.email.lower() or
|
||||
search_term.lower() in client.company.lower()):
|
||||
results.append(client)
|
||||
|
||||
if not results:
|
||||
print(f"Aucun client trouvé pour '{search_term}'.")
|
||||
return
|
||||
|
||||
print(f"\n{len(results)} client(s) trouvé(s):\n")
|
||||
|
||||
for i, client in enumerate(results, 1):
|
||||
print(f"{i}. {client.name} ({client.company})")
|
||||
print(f" Email: {client.email} | Téléphone: {client.phone}")
|
||||
print("")
|
||||
|
||||
# Offrir la possibilité de voir les détails d'un client
|
||||
if results:
|
||||
choice = input("\nEntrez le numéro du client pour voir les détails (ou Entrée pour annuler): ")
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(results):
|
||||
client = results[int(choice) - 1]
|
||||
display_client_details(client)
|
||||
|
||||
def add_new_client(client_handler):
|
||||
"""Ajoute un nouveau client"""
|
||||
clear_screen()
|
||||
print("=== Ajout d'un Nouveau Client ===\n")
|
||||
|
||||
# Recueillir les informations du client
|
||||
name = input("Nom du client: ")
|
||||
if not name:
|
||||
print("Le nom est obligatoire. Opération annulée.")
|
||||
return
|
||||
|
||||
company = input("Entreprise: ")
|
||||
email = input("Email: ")
|
||||
phone = input("Téléphone: ")
|
||||
notes = input("Notes: ")
|
||||
|
||||
# Créer le nouveau client
|
||||
new_client = Client(
|
||||
name=name,
|
||||
company=company,
|
||||
email=email,
|
||||
phone=phone,
|
||||
notes=notes
|
||||
)
|
||||
|
||||
# Ajouter le client
|
||||
client_handler.add_client(new_client)
|
||||
print(f"\nClient '{name}' ajouté avec succès!")
|
||||
|
||||
def edit_client(client_handler):
|
||||
"""Modifie un client existant"""
|
||||
clear_screen()
|
||||
print("=== Modification d'un Client ===\n")
|
||||
|
||||
# Afficher tous les clients pour sélection
|
||||
clients = client_handler.get_all_clients()
|
||||
|
||||
if not clients:
|
||||
print("Aucun client trouvé.")
|
||||
return
|
||||
|
||||
for i, client in enumerate(clients, 1):
|
||||
print(f"{i}. {client.name} ({client.company})")
|
||||
|
||||
# Sélectionner un client à modifier
|
||||
choice = input("\nEntrez le numéro du client à modifier (ou Entrée pour annuler): ")
|
||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(clients):
|
||||
print("Opération annulée ou sélection invalide.")
|
||||
return
|
||||
|
||||
client = clients[int(choice) - 1]
|
||||
|
||||
# Afficher les détails actuels
|
||||
print(f"\nModification de '{client.name}':")
|
||||
print(f"1. Nom: {client.name}")
|
||||
print(f"2. Entreprise: {client.company}")
|
||||
print(f"3. Email: {client.email}")
|
||||
print(f"4. Téléphone: {client.phone}")
|
||||
print(f"5. Notes: {client.notes}")
|
||||
print("6. Tout modifier")
|
||||
print("7. Annuler")
|
||||
|
||||
# Choisir ce qu'il faut modifier
|
||||
edit_choice = input("\nQue souhaitez-vous modifier (1-7): ")
|
||||
|
||||
if edit_choice == "7":
|
||||
print("Modification annulée.")
|
||||
return
|
||||
|
||||
if edit_choice == "1" or edit_choice == "6":
|
||||
client.name = input(f"Nouveau nom [{client.name}]: ") or client.name
|
||||
|
||||
if edit_choice == "2" or edit_choice == "6":
|
||||
client.company = input(f"Nouvelle entreprise [{client.company}]: ") or client.company
|
||||
|
||||
if edit_choice == "3" or edit_choice == "6":
|
||||
client.email = input(f"Nouvel email [{client.email}]: ") or client.email
|
||||
|
||||
if edit_choice == "4" or edit_choice == "6":
|
||||
client.phone = input(f"Nouveau téléphone [{client.phone}]: ") or client.phone
|
||||
|
||||
if edit_choice == "5" or edit_choice == "6":
|
||||
client.notes = input(f"Nouvelles notes [{client.notes}]: ") or client.notes
|
||||
|
||||
# Mettre à jour le client
|
||||
client_handler.update_client(client)
|
||||
print(f"\nClient '{client.name}' mis à jour avec succès!")
|
||||
|
||||
def delete_client(client_handler):
|
||||
"""Supprime un client"""
|
||||
clear_screen()
|
||||
print("=== Suppression d'un Client ===\n")
|
||||
|
||||
# Afficher tous les clients pour sélection
|
||||
clients = client_handler.get_all_clients()
|
||||
|
||||
if not clients:
|
||||
print("Aucun client trouvé.")
|
||||
return
|
||||
|
||||
for i, client in enumerate(clients, 1):
|
||||
print(f"{i}. {client.name} ({client.company})")
|
||||
|
||||
# Sélectionner un client à supprimer
|
||||
choice = input("\nEntrez le numéro du client à supprimer (ou Entrée pour annuler): ")
|
||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(clients):
|
||||
print("Opération annulée ou sélection invalide.")
|
||||
return
|
||||
|
||||
client = clients[int(choice) - 1]
|
||||
|
||||
# Confirmer la suppression
|
||||
confirm = input(f"Êtes-vous sûr de vouloir supprimer '{client.name}'? (o/n): ")
|
||||
|
||||
if confirm.lower() != 'o':
|
||||
print("Suppression annulée.")
|
||||
return
|
||||
|
||||
# Supprimer le client
|
||||
client_handler.delete_client(client.id)
|
||||
print(f"\nClient '{client.name}' supprimé avec succès!")
|
||||
|
||||
def view_client_documents(client_handler):
|
||||
"""Affiche les documents associés à un client"""
|
||||
clear_screen()
|
||||
print("=== Documents d'un Client ===\n")
|
||||
|
||||
# Afficher tous les clients pour sélection
|
||||
clients = client_handler.get_all_clients()
|
||||
|
||||
if not clients:
|
||||
print("Aucun client trouvé.")
|
||||
return
|
||||
|
||||
for i, client in enumerate(clients, 1):
|
||||
print(f"{i}. {client.name} ({client.company})")
|
||||
|
||||
# Sélectionner un client
|
||||
choice = input("\nEntrez le numéro du client pour voir ses documents (ou Entrée pour annuler): ")
|
||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(clients):
|
||||
print("Opération annulée ou sélection invalide.")
|
||||
return
|
||||
|
||||
client = clients[int(choice) - 1]
|
||||
display_client_documents(client)
|
||||
|
||||
def display_client_details(client):
|
||||
"""Affiche les détails d'un client"""
|
||||
clear_screen()
|
||||
print(f"=== Détails du Client: {client.name} ===\n")
|
||||
|
||||
print(f"ID: {client.id}")
|
||||
print(f"Nom: {client.name}")
|
||||
print(f"Entreprise: {client.company}")
|
||||
print(f"Email: {client.email}")
|
||||
print(f"Téléphone: {client.phone}")
|
||||
print(f"Notes: {client.notes}")
|
||||
print(f"Dernière interaction: {client.last_contact}")
|
||||
print(f"Prochaine action: {client.next_action}")
|
||||
|
||||
if client.tags:
|
||||
print(f"Tags: {', '.join(client.tags)}")
|
||||
|
||||
# Afficher les documents
|
||||
display_client_documents(client)
|
||||
|
||||
def display_client_documents(client):
|
||||
"""Affiche les documents associés à un client"""
|
||||
print(f"\n=== Documents de {client.name} ===\n")
|
||||
|
||||
devis = client.linked_docs.get('devis', [])
|
||||
propositions = client.linked_docs.get('propositions', [])
|
||||
factures = client.linked_docs.get('factures', [])
|
||||
|
||||
if not devis and not propositions and not factures:
|
||||
print("Aucun document trouvé pour ce client.")
|
||||
return
|
||||
|
||||
if devis:
|
||||
print("Devis:")
|
||||
for i, doc in enumerate(devis, 1):
|
||||
print(f" {i}. {os.path.basename(doc)}")
|
||||
|
||||
if propositions:
|
||||
print("\nPropositions commerciales:")
|
||||
for i, doc in enumerate(propositions, 1):
|
||||
print(f" {i}. {os.path.basename(doc)}")
|
||||
|
||||
if factures:
|
||||
print("\nFactures:")
|
||||
for i, doc in enumerate(factures, 1):
|
||||
print(f" {i}. {os.path.basename(doc)}")
|
||||
|
||||
# Option pour ouvrir un document
|
||||
print("\n1. Ouvrir un document")
|
||||
print("2. Retour")
|
||||
|
||||
choice = input("\nQue souhaitez-vous faire (1-2): ")
|
||||
|
||||
if choice == "1":
|
||||
# Choisir le type de document
|
||||
print("\nType de document:")
|
||||
print("1. Devis")
|
||||
print("2. Proposition commerciale")
|
||||
print("3. Facture")
|
||||
|
||||
doc_type_choice = input("\nSélectionnez le type de document (1-3): ")
|
||||
|
||||
doc_list = []
|
||||
if doc_type_choice == "1" and devis:
|
||||
doc_list = devis
|
||||
doc_type_name = "devis"
|
||||
elif doc_type_choice == "2" and propositions:
|
||||
doc_list = propositions
|
||||
doc_type_name = "proposition commerciale"
|
||||
elif doc_type_choice == "3" and factures:
|
||||
doc_list = factures
|
||||
doc_type_name = "facture"
|
||||
else:
|
||||
print("Choix invalide ou aucun document de ce type.")
|
||||
return
|
||||
|
||||
# Afficher les documents du type sélectionné
|
||||
print(f"\n{doc_type_name.capitalize()}s disponibles:")
|
||||
for i, doc in enumerate(doc_list, 1):
|
||||
print(f" {i}. {os.path.basename(doc)}")
|
||||
|
||||
# Sélectionner un document à ouvrir
|
||||
doc_choice = input(f"\nSélectionnez un {doc_type_name} à ouvrir (1-{len(doc_list)}): ")
|
||||
|
||||
if doc_choice.isdigit() and 1 <= int(doc_choice) <= len(doc_list):
|
||||
selected_doc = doc_list[int(doc_choice) - 1]
|
||||
open_document(selected_doc)
|
||||
else:
|
||||
print("Sélection invalide.")
|
||||
|
||||
def open_document(document_path):
|
||||
"""Ouvre un document PDF"""
|
||||
try:
|
||||
if os.path.exists(document_path):
|
||||
if os.name == 'nt': # Windows
|
||||
os.startfile(document_path)
|
||||
elif os.name == 'posix': # macOS et Linux
|
||||
if 'darwin' in os.uname().sysname.lower(): # macOS
|
||||
os.system(f'open "{document_path}"')
|
||||
else: # Linux
|
||||
os.system(f'xdg-open "{document_path}"')
|
||||
print(f"Document ouvert: {os.path.basename(document_path)}")
|
||||
else:
|
||||
print(f"Erreur: Le document {document_path} n'existe pas.")
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'ouverture du document: {e}")
|
||||
|
||||
def manage_clients(client_handler, prospect_handler):
|
||||
"""Gestion des clients"""
|
||||
while True:
|
||||
clear_screen()
|
||||
print("=== Gestion des Clients ===\n")
|
||||
|
||||
print("1. Afficher tous les clients")
|
||||
print("2. Rechercher un client")
|
||||
print("3. Ajouter un nouveau client")
|
||||
print("4. Modifier un client")
|
||||
print("5. Supprimer un client")
|
||||
print("6. Voir les documents d'un client")
|
||||
print("7. Retour")
|
||||
|
||||
choice = input("\nSélectionnez une option (1-7): ")
|
||||
|
||||
if choice == "1":
|
||||
# Afficher tous les clients
|
||||
display_all_clients(client_handler)
|
||||
elif choice == "2":
|
||||
# Rechercher un client
|
||||
search_clients(client_handler)
|
||||
elif choice == "3":
|
||||
# Ajouter un nouveau client
|
||||
add_new_client(client_handler)
|
||||
elif choice == "4":
|
||||
# Modifier un client
|
||||
edit_client(client_handler)
|
||||
elif choice == "5":
|
||||
# Supprimer un client
|
||||
delete_client(client_handler)
|
||||
elif choice == "6":
|
||||
# Voir les documents d'un client
|
||||
view_client_documents(client_handler)
|
||||
elif choice == "7":
|
||||
# Retour
|
||||
break
|
||||
else:
|
||||
print("\nOption invalide. Veuillez réessayer.")
|
||||
|
||||
input("\nAppuyez sur Entrée pour continuer...")
|
||||
|
||||
def manage_prospects(client_handler, prospect_handler):
|
||||
"""Gestion des prospects"""
|
||||
while True:
|
||||
clear_screen()
|
||||
print("=== Gestion des Prospects ===\n")
|
||||
|
||||
print("1. Afficher tous les prospects")
|
||||
print("2. Rechercher un prospect")
|
||||
print("3. Ajouter un nouveau prospect")
|
||||
print("4. Modifier un prospect")
|
||||
print("5. Supprimer un prospect")
|
||||
print("6. Convertir un prospect en client")
|
||||
print("7. Afficher les prospects par statut")
|
||||
print("8. Retour")
|
||||
|
||||
choice = input("\nSélectionnez une option (1-8): ")
|
||||
|
||||
if choice == "1":
|
||||
# Afficher tous les prospects
|
||||
display_all_prospects(prospect_handler)
|
||||
elif choice == "2":
|
||||
# Rechercher un prospect
|
||||
search_prospects(prospect_handler)
|
||||
elif choice == "3":
|
||||
# Ajouter un nouveau prospect
|
||||
add_new_prospect(prospect_handler)
|
||||
elif choice == "4":
|
||||
# Modifier un prospect
|
||||
edit_prospect(prospect_handler)
|
||||
elif choice == "5":
|
||||
# Supprimer un prospect
|
||||
delete_prospect(prospect_handler)
|
||||
elif choice == "6":
|
||||
# Convertir un prospect en client
|
||||
convert_prospect_to_client(prospect_handler, client_handler)
|
||||
elif choice == "7":
|
||||
# Afficher les prospects par statut
|
||||
display_prospects_by_status(prospect_handler)
|
||||
elif choice == "8":
|
||||
# Retour
|
||||
break
|
||||
else:
|
||||
print("\nOption invalide. Veuillez réessayer.")
|
||||
|
||||
input("\nAppuyez sur Entrée pour continuer...")
|
||||
|
||||
def show_dashboard(client_handler, prospect_handler):
|
||||
"""Affiche un tableau de bord avec des statistiques"""
|
||||
clear_screen()
|
||||
print("=== Tableau de Bord CRM ===\n")
|
||||
|
||||
# Récupérer les données
|
||||
clients = client_handler.get_all_clients()
|
||||
prospects = prospect_handler.get_all_prospects()
|
||||
|
||||
# Statistiques de base
|
||||
print(f"Nombre total de clients: {len(clients)}")
|
||||
print(f"Nombre total de prospects: {len(prospects)}")
|
||||
|
||||
# Statistiques des prospects par statut
|
||||
statuses = {}
|
||||
for prospect in prospects:
|
||||
status = prospect.status
|
||||
if status in statuses:
|
||||
statuses[status] += 1
|
||||
else:
|
||||
statuses[status] = 1
|
||||
|
||||
if statuses:
|
||||
print("\nProspects par statut:")
|
||||
for status, count in statuses.items():
|
||||
print(f" {status}: {count}")
|
||||
|
||||
# Derniers prospects ajoutés
|
||||
recent_prospects = prospect_handler.get_recent_prospects(days=30)
|
||||
if recent_prospects:
|
||||
print(f"\nNouveaux prospects (30 derniers jours): {len(recent_prospects)}")
|
||||
|
||||
# Activité récente
|
||||
print("\nRécapitulatif des activités récentes:")
|
||||
print(" Propositions commerciales en attente...")
|
||||
print(" Devis en cours...")
|
||||
|
||||
# Suggestions d'actions
|
||||
print("\nSuggestions d'actions:")
|
||||
if len(prospects) > 0:
|
||||
print(" • Suivez vos prospects pour augmenter votre taux de conversion")
|
||||
if len(clients) > 0:
|
||||
print(" • Contactez vos clients existants pour des opportunités de vente additionnelle")
|
||||
if len(prospects) == 0:
|
||||
print(" • Ajoutez des prospects pour développer votre portefeuille client")
|
||||
|
||||
def display_all_prospects(prospect_handler):
|
||||
"""Affiche tous les prospects"""
|
||||
clear_screen()
|
||||
print("=== Liste des Prospects ===\n")
|
||||
|
||||
prospects = prospect_handler.get_all_prospects()
|
||||
|
||||
if not prospects:
|
||||
print("Aucun prospect trouvé.")
|
||||
return
|
||||
|
||||
for i, prospect in enumerate(prospects, 1):
|
||||
print(f"{i}. {prospect.name} ({prospect.company})")
|
||||
print(f" Email: {prospect.email} | Téléphone: {prospect.phone}")
|
||||
print(f" Statut: {prospect.status} | Source: {prospect.source}")
|
||||
print(f" Dernière interaction: {prospect.last_contact}")
|
||||
print("")
|
||||
|
||||
def search_prospects(prospect_handler):
|
||||
"""Recherche des prospects par nom"""
|
||||
clear_screen()
|
||||
print("=== Recherche de Prospects ===\n")
|
||||
|
||||
search_term = input("Entrez un terme de recherche (nom, email, entreprise): ")
|
||||
|
||||
if not search_term:
|
||||
print("Recherche annulée.")
|
||||
return
|
||||
|
||||
results = []
|
||||
prospects = prospect_handler.get_all_prospects()
|
||||
|
||||
for prospect in prospects:
|
||||
if (search_term.lower() in prospect.name.lower() or
|
||||
search_term.lower() in prospect.email.lower() or
|
||||
search_term.lower() in prospect.company.lower()):
|
||||
results.append(prospect)
|
||||
|
||||
if not results:
|
||||
print(f"Aucun prospect trouvé pour '{search_term}'.")
|
||||
return
|
||||
|
||||
print(f"\n{len(results)} prospect(s) trouvé(s):\n")
|
||||
|
||||
for i, prospect in enumerate(results, 1):
|
||||
print(f"{i}. {prospect.name} ({prospect.company})")
|
||||
print(f" Email: {prospect.email} | Téléphone: {prospect.phone}")
|
||||
print(f" Statut: {prospect.status}")
|
||||
print("")
|
||||
|
||||
# Offrir la possibilité de voir les détails d'un prospect
|
||||
if results:
|
||||
choice = input("\nEntrez le numéro du prospect pour voir les détails (ou Entrée pour annuler): ")
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(results):
|
||||
prospect = results[int(choice) - 1]
|
||||
display_prospect_details(prospect)
|
||||
|
||||
def add_new_prospect(prospect_handler):
|
||||
"""Ajoute un nouveau prospect"""
|
||||
clear_screen()
|
||||
print("=== Ajout d'un Nouveau Prospect ===\n")
|
||||
|
||||
# Recueillir les informations du prospect
|
||||
name = input("Nom du prospect: ")
|
||||
if not name:
|
||||
print("Le nom est obligatoire. Opération annulée.")
|
||||
return
|
||||
|
||||
company = input("Entreprise: ")
|
||||
email = input("Email: ")
|
||||
phone = input("Téléphone: ")
|
||||
source = input("Source (site web, référence, salon, etc.): ")
|
||||
notes = input("Notes: ")
|
||||
|
||||
# Statut du prospect
|
||||
print("\nStatut du prospect:")
|
||||
print("1. Nouveau")
|
||||
print("2. Contacté")
|
||||
print("3. Qualifié")
|
||||
print("4. Proposition envoyée")
|
||||
print("5. Non intéressé")
|
||||
|
||||
status_choice = input("\nSélectionnez le statut (1-5): ")
|
||||
status = "Nouveau" # Défaut
|
||||
|
||||
if status_choice == "1":
|
||||
status = "Nouveau"
|
||||
elif status_choice == "2":
|
||||
status = "Contacté"
|
||||
elif status_choice == "3":
|
||||
status = "Qualifié"
|
||||
elif status_choice == "4":
|
||||
status = "Proposition envoyée"
|
||||
elif status_choice == "5":
|
||||
status = "Non intéressé"
|
||||
|
||||
# Créer le nouveau prospect
|
||||
new_prospect = Prospect(
|
||||
name=name,
|
||||
company=company,
|
||||
email=email,
|
||||
phone=phone,
|
||||
source=source,
|
||||
notes=notes,
|
||||
status=status
|
||||
)
|
||||
|
||||
# Ajouter le prospect
|
||||
prospect_handler.add_prospect(new_prospect)
|
||||
print(f"\nProspect '{name}' ajouté avec succès!")
|
||||
|
||||
def edit_prospect(prospect_handler):
|
||||
"""Modifie un prospect existant"""
|
||||
clear_screen()
|
||||
print("=== Modification d'un Prospect ===\n")
|
||||
|
||||
# Afficher tous les prospects pour sélection
|
||||
prospects = prospect_handler.get_all_prospects()
|
||||
|
||||
if not prospects:
|
||||
print("Aucun prospect trouvé.")
|
||||
return
|
||||
|
||||
for i, prospect in enumerate(prospects, 1):
|
||||
print(f"{i}. {prospect.name} ({prospect.company}) - {prospect.status}")
|
||||
|
||||
# Sélectionner un prospect à modifier
|
||||
choice = input("\nEntrez le numéro du prospect à modifier (ou Entrée pour annuler): ")
|
||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(prospects):
|
||||
print("Opération annulée ou sélection invalide.")
|
||||
return
|
||||
|
||||
prospect = prospects[int(choice) - 1]
|
||||
|
||||
# Afficher les détails actuels
|
||||
print(f"\nModification de '{prospect.name}':")
|
||||
print(f"1. Nom: {prospect.name}")
|
||||
print(f"2. Entreprise: {prospect.company}")
|
||||
print(f"3. Email: {prospect.email}")
|
||||
print(f"4. Téléphone: {prospect.phone}")
|
||||
print(f"5. Source: {prospect.source}")
|
||||
print(f"6. Statut: {prospect.status}")
|
||||
print(f"7. Notes: {prospect.notes}")
|
||||
print("8. Tout modifier")
|
||||
print("9. Annuler")
|
||||
|
||||
# Choisir ce qu'il faut modifier
|
||||
edit_choice = input("\nQue souhaitez-vous modifier (1-9): ")
|
||||
|
||||
if edit_choice == "9":
|
||||
print("Modification annulée.")
|
||||
return
|
||||
|
||||
if edit_choice == "1" or edit_choice == "8":
|
||||
prospect.name = input(f"Nouveau nom [{prospect.name}]: ") or prospect.name
|
||||
|
||||
if edit_choice == "2" or edit_choice == "8":
|
||||
prospect.company = input(f"Nouvelle entreprise [{prospect.company}]: ") or prospect.company
|
||||
|
||||
if edit_choice == "3" or edit_choice == "8":
|
||||
prospect.email = input(f"Nouvel email [{prospect.email}]: ") or prospect.email
|
||||
|
||||
if edit_choice == "4" or edit_choice == "8":
|
||||
prospect.phone = input(f"Nouveau téléphone [{prospect.phone}]: ") or prospect.phone
|
||||
|
||||
if edit_choice == "5" or edit_choice == "8":
|
||||
prospect.source = input(f"Nouvelle source [{prospect.source}]: ") or prospect.source
|
||||
|
||||
if edit_choice == "6" or edit_choice == "8":
|
||||
print("\nStatut du prospect:")
|
||||
print("1. Nouveau")
|
||||
print("2. Contacté")
|
||||
print("3. Qualifié")
|
||||
print("4. Proposition envoyée")
|
||||
print("5. Non intéressé")
|
||||
|
||||
status_choice = input(f"\nSélectionnez le statut (1-5) [Actuel: {prospect.status}]: ")
|
||||
|
||||
if status_choice == "1":
|
||||
prospect.status = "Nouveau"
|
||||
elif status_choice == "2":
|
||||
prospect.status = "Contacté"
|
||||
elif status_choice == "3":
|
||||
prospect.status = "Qualifié"
|
||||
elif status_choice == "4":
|
||||
prospect.status = "Proposition envoyée"
|
||||
elif status_choice == "5":
|
||||
prospect.status = "Non intéressé"
|
||||
|
||||
if edit_choice == "7" or edit_choice == "8":
|
||||
prospect.notes = input(f"Nouvelles notes [{prospect.notes}]: ") or prospect.notes
|
||||
|
||||
# Mettre à jour le prospect
|
||||
prospect_handler.update_prospect(prospect)
|
||||
print(f"\nProspect '{prospect.name}' mis à jour avec succès!")
|
||||
|
||||
def delete_prospect(prospect_handler):
|
||||
"""Supprime un prospect"""
|
||||
clear_screen()
|
||||
print("=== Suppression d'un Prospect ===\n")
|
||||
|
||||
# Afficher tous les prospects pour sélection
|
||||
prospects = prospect_handler.get_all_prospects()
|
||||
|
||||
if not prospects:
|
||||
print("Aucun prospect trouvé.")
|
||||
return
|
||||
|
||||
for i, prospect in enumerate(prospects, 1):
|
||||
print(f"{i}. {prospect.name} ({prospect.company}) - {prospect.status}")
|
||||
|
||||
# Sélectionner un prospect à supprimer
|
||||
choice = input("\nEntrez le numéro du prospect à supprimer (ou Entrée pour annuler): ")
|
||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(prospects):
|
||||
print("Opération annulée ou sélection invalide.")
|
||||
return
|
||||
|
||||
prospect = prospects[int(choice) - 1]
|
||||
|
||||
# Confirmer la suppression
|
||||
confirm = input(f"Êtes-vous sûr de vouloir supprimer '{prospect.name}'? (o/n): ")
|
||||
|
||||
if confirm.lower() != 'o':
|
||||
print("Suppression annulée.")
|
||||
return
|
||||
|
||||
# Supprimer le prospect
|
||||
prospect_handler.delete_prospect(prospect.id)
|
||||
print(f"\nProspect '{prospect.name}' supprimé avec succès!")
|
||||
|
||||
def convert_prospect_to_client(prospect_handler, client_handler):
|
||||
"""Convertit un prospect en client"""
|
||||
clear_screen()
|
||||
print("=== Conversion d'un Prospect en Client ===\n")
|
||||
|
||||
# Afficher tous les prospects pour sélection
|
||||
prospects = prospect_handler.get_all_prospects()
|
||||
|
||||
if not prospects:
|
||||
print("Aucun prospect trouvé.")
|
||||
return
|
||||
|
||||
for i, prospect in enumerate(prospects, 1):
|
||||
print(f"{i}. {prospect.name} ({prospect.company}) - {prospect.status}")
|
||||
|
||||
# Sélectionner un prospect à convertir
|
||||
choice = input("\nEntrez le numéro du prospect à convertir en client (ou Entrée pour annuler): ")
|
||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(prospects):
|
||||
print("Opération annulée ou sélection invalide.")
|
||||
return
|
||||
|
||||
prospect = prospects[int(choice) - 1]
|
||||
|
||||
# Confirmer la conversion
|
||||
confirm = input(f"Êtes-vous sûr de vouloir convertir '{prospect.name}' en client? (o/n): ")
|
||||
|
||||
if confirm.lower() != 'o':
|
||||
print("Conversion annulée.")
|
||||
return
|
||||
|
||||
# Convertir le prospect en client
|
||||
client = prospect_handler.convert_to_client(prospect.id, client_handler)
|
||||
if client:
|
||||
print(f"\nProspect '{prospect.name}' converti en client avec succès!")
|
||||
else:
|
||||
print("\nErreur lors de la conversion du prospect en client.")
|
||||
|
||||
def display_prospect_details(prospect):
|
||||
"""Affiche les détails d'un prospect"""
|
||||
clear_screen()
|
||||
print(f"=== Détails du Prospect: {prospect.name} ===\n")
|
||||
|
||||
print(f"ID: {prospect.id}")
|
||||
print(f"Nom: {prospect.name}")
|
||||
print(f"Entreprise: {prospect.company}")
|
||||
print(f"Email: {prospect.email}")
|
||||
print(f"Téléphone: {prospect.phone}")
|
||||
print(f"Source: {prospect.source}")
|
||||
print(f"Statut: {prospect.status}")
|
||||
print(f"Notes: {prospect.notes}")
|
||||
print(f"Dernière interaction: {prospect.last_contact}")
|
||||
print(f"Prochaine action: {prospect.next_action}")
|
||||
|
||||
if prospect.tags:
|
||||
print(f"Tags: {', '.join(prospect.tags)}")
|
||||
|
||||
def display_prospects_by_status(prospect_handler):
|
||||
"""Affiche les prospects regroupés par statut"""
|
||||
clear_screen()
|
||||
print("=== Prospects par Statut ===\n")
|
||||
|
||||
# Récupérer tous les prospects
|
||||
prospects = prospect_handler.get_all_prospects()
|
||||
|
||||
if not prospects:
|
||||
print("Aucun prospect trouvé.")
|
||||
return
|
||||
|
||||
# Regrouper les prospects par statut
|
||||
status_groups = {}
|
||||
for prospect in prospects:
|
||||
status = prospect.status
|
||||
if status not in status_groups:
|
||||
status_groups[status] = []
|
||||
status_groups[status].append(prospect)
|
||||
|
||||
# Afficher les prospects par groupe de statut
|
||||
for status, prospects_list in status_groups.items():
|
||||
print(f"\n=== {status} ({len(prospects_list)}) ===")
|
||||
for i, prospect in enumerate(prospects_list, 1):
|
||||
print(f"{i}. {prospect.name} ({prospect.company})")
|
||||
print(f" Email: {prospect.email} | Téléphone: {prospect.phone}")
|
||||
|
||||
print("\n")
|
||||
|
||||
# Option pour filtrer par un statut spécifique
|
||||
print("Voulez-vous voir les détails d'un statut spécifique?")
|
||||
print("1. Nouveau")
|
||||
print("2. Contacté")
|
||||
print("3. Qualifié")
|
||||
print("4. Proposition envoyée")
|
||||
print("5. Non intéressé")
|
||||
print("6. Retour")
|
||||
|
||||
choice = input("\nSélectionnez une option (1-6): ")
|
||||
|
||||
status_mapping = {
|
||||
"1": "Nouveau",
|
||||
"2": "Contacté",
|
||||
"3": "Qualifié",
|
||||
"4": "Proposition envoyée",
|
||||
"5": "Non intéressé"
|
||||
}
|
||||
|
||||
if choice in status_mapping:
|
||||
selected_status = status_mapping[choice]
|
||||
filtered_prospects = prospect_handler.get_prospects_by_status(selected_status)
|
||||
|
||||
if filtered_prospects:
|
||||
clear_screen()
|
||||
print(f"=== Prospects avec statut: {selected_status} ===\n")
|
||||
|
||||
for i, prospect in enumerate(filtered_prospects, 1):
|
||||
print(f"{i}. {prospect.name} ({prospect.company})")
|
||||
print(f" Email: {prospect.email} | Téléphone: {prospect.phone}")
|
||||
print(f" Dernière interaction: {prospect.last_contact}")
|
||||
print(f" Notes: {prospect.notes[:50]}..." if len(prospect.notes) > 50 else f" Notes: {prospect.notes}")
|
||||
print("")
|
||||
|
||||
# Option pour voir les détails d'un prospect spécifique
|
||||
detail_choice = input("\nEntrez le numéro du prospect pour voir les détails (ou Entrée pour annuler): ")
|
||||
if detail_choice.isdigit() and 1 <= int(detail_choice) <= len(filtered_prospects):
|
||||
prospect = filtered_prospects[int(detail_choice) - 1]
|
||||
display_prospect_details(prospect)
|
||||
else:
|
||||
print(f"\nAucun prospect avec le statut '{selected_status}' trouvé.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
54
modules/crm/client.py
Normal file
54
modules/crm/client.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from datetime import date
|
||||
import uuid
|
||||
|
||||
from datetime import date
|
||||
import uuid
|
||||
|
||||
class Client:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
company: str = "",
|
||||
email: str = "",
|
||||
phone: str = "",
|
||||
notes: str = "",
|
||||
tags: list = None,
|
||||
last_contact: str = None,
|
||||
next_action: str = "",
|
||||
linked_docs: dict = None,
|
||||
id: str = None,
|
||||
created_at: str = None
|
||||
):
|
||||
self.id = id or f"cli_{uuid.uuid4().hex[:8]}"
|
||||
self.name = name
|
||||
self.company = company
|
||||
self.email = email
|
||||
self.phone = phone
|
||||
self.notes = notes
|
||||
self.tags = tags or []
|
||||
self.last_contact = last_contact or str(date.today())
|
||||
self.next_action = next_action
|
||||
self.linked_docs = linked_docs or {"devis": [], "propositions": [], "factures": []}
|
||||
self.created_at = created_at or str(date.today())
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"company": self.company,
|
||||
"email": self.email,
|
||||
"phone": self.phone,
|
||||
"notes": self.notes,
|
||||
"tags": self.tags,
|
||||
"last_contact": self.last_contact,
|
||||
"next_action": self.next_action,
|
||||
"linked_docs": self.linked_docs,
|
||||
"created_at": self.created_at
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict):
|
||||
return Client(**data)
|
||||
|
||||
def __str__(self):
|
||||
return f"Client({self.name}, {self.company}, {self.email}, {self.phone})"
|
||||
172
modules/crm/handler.py
Normal file
172
modules/crm/handler.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
from datetime import date
|
||||
import os
|
||||
import json
|
||||
from .client import Client
|
||||
from core.data import Data
|
||||
|
||||
class ClientHandler:
|
||||
def __init__(self, clients_folder="Data/clients"):
|
||||
self.clients_folder = clients_folder
|
||||
self.clients = []
|
||||
self.load_clients()
|
||||
|
||||
def load_clients(self):
|
||||
"""Charge tous les clients depuis les fichiers existants"""
|
||||
self.clients = []
|
||||
# Charger les clients depuis les fichiers JSON existants
|
||||
if os.path.exists(self.clients_folder):
|
||||
for filename in os.listdir(self.clients_folder):
|
||||
if filename.endswith('.json'):
|
||||
client_path = os.path.join(self.clients_folder, filename)
|
||||
try:
|
||||
# Utiliser le gestionnaire de données pour charger le fichier JSON
|
||||
data_manager = Data(client_path)
|
||||
client_data = data_manager.load_data()
|
||||
|
||||
# Si le fichier a la structure d'un client, créer un objet Client
|
||||
if "client_name" in client_data:
|
||||
# Conversion du format existant vers le format Client
|
||||
client_obj = {
|
||||
"name": client_data.get("client_name", ""),
|
||||
"email": client_data.get("client_email", ""),
|
||||
"phone": client_data.get("client_phone", ""),
|
||||
"notes": client_data.get("additional_info", ""),
|
||||
"company": client_data.get("project_name", ""),
|
||||
"tags": [], # Pas de tags dans les données existantes
|
||||
"next_action": "", # Pas d'action suivante dans les données existantes
|
||||
"linked_docs": self._find_linked_docs(client_data.get("client_name", ""))
|
||||
}
|
||||
client = Client(**client_obj)
|
||||
self.clients.append(client)
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement du client {filename}: {e}")
|
||||
|
||||
return self.clients
|
||||
|
||||
def _find_linked_docs(self, client_name):
|
||||
"""Trouve les documents associés à un client"""
|
||||
linked_docs = {"devis": [], "propositions": [], "factures": []}
|
||||
client_name_normalized = client_name.replace(" ", "_").lower()
|
||||
|
||||
# Rechercher les devis
|
||||
devis_folder = "output/devis"
|
||||
if os.path.exists(devis_folder):
|
||||
for filename in os.listdir(devis_folder):
|
||||
if filename.startswith(client_name_normalized) and filename.endswith(".pdf"):
|
||||
linked_docs["devis"].append(os.path.join(devis_folder, filename))
|
||||
|
||||
# Rechercher les propositions
|
||||
propositions_folder = "output/propositions"
|
||||
if os.path.exists(propositions_folder):
|
||||
for filename in os.listdir(propositions_folder):
|
||||
if filename.startswith(client_name_normalized) and filename.endswith(".pdf"):
|
||||
linked_docs["propositions"].append(os.path.join(propositions_folder, filename))
|
||||
|
||||
# Rechercher les factures (si elles existent)
|
||||
factures_folder = "output/factures"
|
||||
if os.path.exists(factures_folder):
|
||||
for filename in os.listdir(factures_folder):
|
||||
if filename.startswith(client_name_normalized) and filename.endswith(".pdf"):
|
||||
linked_docs["factures"].append(os.path.join(factures_folder, filename))
|
||||
|
||||
return linked_docs
|
||||
|
||||
def get_all_clients(self):
|
||||
"""Retourne tous les clients"""
|
||||
return self.clients
|
||||
|
||||
def get_client_by_id(self, client_id):
|
||||
"""Retourne un client par son ID"""
|
||||
for client in self.clients:
|
||||
if client.id == client_id:
|
||||
return client
|
||||
return None
|
||||
|
||||
def get_client_by_name(self, client_name):
|
||||
"""Retourne un client par son nom"""
|
||||
for client in self.clients:
|
||||
if client.name.lower() == client_name.lower():
|
||||
return client
|
||||
return None
|
||||
|
||||
def add_client(self, client):
|
||||
"""Ajoute un nouveau client"""
|
||||
self.clients.append(client)
|
||||
# Sauvegarder le client dans un fichier JSON
|
||||
self._save_client(client)
|
||||
return client
|
||||
|
||||
def update_client(self, client):
|
||||
"""Met à jour un client existant"""
|
||||
for i, existing_client in enumerate(self.clients):
|
||||
if existing_client.id == client.id:
|
||||
self.clients[i] = client
|
||||
# Sauvegarder le client mis à jour
|
||||
self._save_client(client)
|
||||
return client
|
||||
return None
|
||||
|
||||
def delete_client(self, client_id):
|
||||
"""Supprime un client"""
|
||||
for i, client in enumerate(self.clients):
|
||||
if client.id == client_id:
|
||||
deleted_client = self.clients.pop(i)
|
||||
# Supprimer le fichier JSON du client
|
||||
client_file = os.path.join(self.clients_folder, f"{deleted_client.name.replace(' ', '_').lower()}.json")
|
||||
if os.path.exists(client_file):
|
||||
os.remove(client_file)
|
||||
return deleted_client
|
||||
return None
|
||||
|
||||
def _save_client(self, client):
|
||||
"""Sauvegarde un client dans un fichier JSON"""
|
||||
client_data = {
|
||||
"client_name": client.name,
|
||||
"client_email": client.email,
|
||||
"client_phone": client.phone,
|
||||
"additional_info": client.notes,
|
||||
"project_name": client.company,
|
||||
# Autres champs pour maintenir la compatibilité avec le format existant
|
||||
"project_type": "",
|
||||
"project_description": "",
|
||||
"features": [],
|
||||
"budget": "",
|
||||
"payment_terms": "",
|
||||
"contact_info": f"{client.name}, {client.phone}",
|
||||
"deadline": ""
|
||||
}
|
||||
|
||||
client_file = os.path.join(self.clients_folder, f"{client.name.replace(' ', '_').lower()}.json")
|
||||
data_manager = Data(client_file)
|
||||
data_manager.save_data(client_data)
|
||||
|
||||
return client_file
|
||||
|
||||
def add_task_for_client(self, client_id: str, title: str, due_date: str, description: str = "", priority: str = "normale", metadata: dict = None):
|
||||
"""
|
||||
Crée une tâche liée à un client.
|
||||
:param client_id: ID du client
|
||||
:param title: Titre de la tâche
|
||||
:param due_date: Date d'échéance au format ISO (YYYY-MM-DD)
|
||||
:param description: Description optionnelle
|
||||
:param priority: 'basse' | 'normale' | 'haute'
|
||||
:param metadata: métadonnées optionnelles
|
||||
:return: ID de la tâche créée ou None si client introuvable
|
||||
"""
|
||||
client = self.get_client_by_id(client_id)
|
||||
if not client:
|
||||
return None
|
||||
# Import local pour éviter dépendances circulaires au chargement
|
||||
from modules.tasks.task import Task
|
||||
from modules.tasks.task_handler import TaskHandler
|
||||
|
||||
task = Task(
|
||||
title=title,
|
||||
due_date=due_date,
|
||||
description=description,
|
||||
entity_type="client",
|
||||
entity_id=client_id,
|
||||
priority=priority,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
return TaskHandler().add_task(task)
|
||||
95
modules/crm/prospect.py
Normal file
95
modules/crm/prospect.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from datetime import date
|
||||
import uuid
|
||||
from typing import Union
|
||||
|
||||
from .status import ProspectStatus
|
||||
|
||||
|
||||
class Prospect:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
company: str = "",
|
||||
email: str = "",
|
||||
phone: str = "",
|
||||
source: str = "", # D'où vient ce prospect (site web, référence, etc.)
|
||||
notes: str = "",
|
||||
status: Union[ProspectStatus, str] = ProspectStatus.NOUVEAU, # Nouveau, Contacté, Qualifié, Proposition, Non intéressé
|
||||
tags: list = None,
|
||||
last_contact: str = None,
|
||||
next_action: str = "",
|
||||
linked_docs: dict = None,
|
||||
id: str = None,
|
||||
created_at: str = None,
|
||||
score: int = 0,
|
||||
last_interaction: str = None
|
||||
):
|
||||
self.id = id or f"pros_{uuid.uuid4().hex[:8]}"
|
||||
self.name = name
|
||||
self.company = company
|
||||
self.email = email
|
||||
self.phone = phone
|
||||
self.source = source
|
||||
self.notes = notes
|
||||
# Stockage interne en Enum
|
||||
self._status: ProspectStatus = ProspectStatus.from_value(status)
|
||||
self.tags = tags or []
|
||||
self.last_contact = last_contact or str(date.today())
|
||||
self.next_action = next_action
|
||||
self.linked_docs = linked_docs or {"propositions": []}
|
||||
self.created_at = created_at or str(date.today())
|
||||
self.score = score or 0
|
||||
self.last_interaction = last_interaction
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""Exposition publique du statut en chaîne pour compatibilité (templates, JSON)."""
|
||||
return self._status.value
|
||||
|
||||
@status.setter
|
||||
def status(self, value: Union[ProspectStatus, str]) -> None:
|
||||
"""Permet d'assigner soit une chaîne, soit l'enum directement."""
|
||||
self._status = ProspectStatus.from_value(value)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"company": self.company,
|
||||
"email": self.email,
|
||||
"phone": self.phone,
|
||||
"source": self.source,
|
||||
"notes": self.notes,
|
||||
"status": self.status, # sérialise en chaîne
|
||||
"tags": self.tags,
|
||||
"last_contact": self.last_contact,
|
||||
"next_action": self.next_action,
|
||||
"linked_docs": self.linked_docs,
|
||||
"created_at": self.created_at,
|
||||
"score": self.score,
|
||||
"last_interaction": self.last_interaction
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict):
|
||||
# __init__ gère la conversion status -> Enum
|
||||
return Prospect(**data)
|
||||
|
||||
def convert_to_client(self):
|
||||
"""Convertit ce prospect en client"""
|
||||
from .client import Client
|
||||
|
||||
return Client(
|
||||
name=self.name,
|
||||
company=self.company,
|
||||
email=self.email,
|
||||
phone=self.phone,
|
||||
notes=self.notes,
|
||||
tags=self.tags,
|
||||
last_contact=self.last_contact,
|
||||
next_action=self.next_action,
|
||||
linked_docs=self.linked_docs
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Prospect({self.name}, {self.company}, {self.status}, {self.email})"
|
||||
157
modules/crm/prospect_handler.py
Normal file
157
modules/crm/prospect_handler.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
from datetime import date
|
||||
import os
|
||||
import json
|
||||
from .prospect import Prospect
|
||||
from core.data import Data
|
||||
|
||||
class ProspectHandler:
|
||||
def __init__(self, prospects_folder="Data/prospects"):
|
||||
self.prospects_folder = prospects_folder
|
||||
self.prospects = []
|
||||
|
||||
# Créer le dossier des prospects s'il n'existe pas
|
||||
if not os.path.exists(self.prospects_folder):
|
||||
os.makedirs(self.prospects_folder)
|
||||
|
||||
self.load_prospects()
|
||||
|
||||
def load_prospects(self):
|
||||
"""Charge tous les prospects depuis les fichiers existants"""
|
||||
self.prospects = []
|
||||
# Charger les prospects depuis les fichiers JSON existants
|
||||
if os.path.exists(self.prospects_folder):
|
||||
for filename in os.listdir(self.prospects_folder):
|
||||
if filename.endswith('.json'):
|
||||
prospect_path = os.path.join(self.prospects_folder, filename)
|
||||
try:
|
||||
# Utiliser le gestionnaire de données pour charger le fichier JSON
|
||||
data_manager = Data(prospect_path)
|
||||
prospect_data = data_manager.load_data()
|
||||
|
||||
# Si le fichier a la structure d'un prospect, créer un objet Prospect
|
||||
if "name" in prospect_data:
|
||||
prospect = Prospect.from_dict(prospect_data)
|
||||
self.prospects.append(prospect)
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement du prospect {filename}: {e}")
|
||||
|
||||
return self.prospects
|
||||
|
||||
def get_all_prospects(self):
|
||||
"""Retourne tous les prospects"""
|
||||
return self.prospects
|
||||
|
||||
def get_prospect_by_id(self, prospect_id):
|
||||
"""Retourne un prospect par son ID"""
|
||||
for prospect in self.prospects:
|
||||
if prospect.id == prospect_id:
|
||||
return prospect
|
||||
return None
|
||||
|
||||
def get_prospect_by_name(self, prospect_name):
|
||||
"""Retourne un prospect par son nom"""
|
||||
for prospect in self.prospects:
|
||||
if prospect.name.lower() == prospect_name.lower():
|
||||
return prospect
|
||||
return None
|
||||
|
||||
def add_prospect(self, prospect):
|
||||
"""Ajoute un nouveau prospect"""
|
||||
self.prospects.append(prospect)
|
||||
# Sauvegarder le prospect dans un fichier JSON
|
||||
self._save_prospect(prospect)
|
||||
return prospect
|
||||
|
||||
def update_prospect(self, prospect):
|
||||
"""Met à jour un prospect existant"""
|
||||
for i, existing_prospect in enumerate(self.prospects):
|
||||
if existing_prospect.id == prospect.id:
|
||||
self.prospects[i] = prospect
|
||||
# Sauvegarder le prospect mis à jour
|
||||
self._save_prospect(prospect)
|
||||
return prospect
|
||||
return None
|
||||
|
||||
def delete_prospect(self, prospect_id):
|
||||
"""Supprime un prospect"""
|
||||
for i, prospect in enumerate(self.prospects):
|
||||
if prospect.id == prospect_id:
|
||||
deleted_prospect = self.prospects.pop(i)
|
||||
# Supprimer le fichier JSON du prospect
|
||||
prospect_file = os.path.join(self.prospects_folder, f"{deleted_prospect.id}.json")
|
||||
if os.path.exists(prospect_file):
|
||||
os.remove(prospect_file)
|
||||
return deleted_prospect
|
||||
return None
|
||||
|
||||
def convert_to_client(self, prospect_id, client_handler):
|
||||
"""Convertit un prospect en client et le supprime de la liste des prospects"""
|
||||
prospect = self.get_prospect_by_id(prospect_id)
|
||||
if prospect:
|
||||
# Créer un client à partir du prospect
|
||||
client = prospect.convert_to_client()
|
||||
# Ajouter le client
|
||||
client_handler.add_client(client)
|
||||
# Supprimer le prospect
|
||||
self.delete_prospect(prospect_id)
|
||||
return client
|
||||
return None
|
||||
|
||||
def _save_prospect(self, prospect):
|
||||
"""Sauvegarde un prospect dans un fichier JSON"""
|
||||
prospect_data = prospect.to_dict()
|
||||
|
||||
prospect_file = os.path.join(self.prospects_folder, f"{prospect.id}.json")
|
||||
data_manager = Data(prospect_file)
|
||||
data_manager.save_data(prospect_data)
|
||||
|
||||
return prospect_file
|
||||
|
||||
def get_prospects_by_status(self, status):
|
||||
"""Retourne les prospects par statut"""
|
||||
return [p for p in self.prospects if p.status.lower() == status.lower()]
|
||||
|
||||
def get_recent_prospects(self, days=30):
|
||||
"""Retourne les prospects créés dans les X derniers jours"""
|
||||
today = date.today()
|
||||
recent_prospects = []
|
||||
|
||||
for prospect in self.prospects:
|
||||
try:
|
||||
created_date = date.fromisoformat(prospect.created_at)
|
||||
delta = today - created_date
|
||||
if delta.days <= days:
|
||||
recent_prospects.append(prospect)
|
||||
except ValueError:
|
||||
# Si la date n'est pas au format ISO, ignorer ce prospect
|
||||
pass
|
||||
|
||||
return recent_prospects
|
||||
|
||||
def add_task_for_prospect(self, prospect_id: str, title: str, due_date: str, description: str = "", priority: str = "normale", metadata: dict = None):
|
||||
"""
|
||||
Crée une tâche liée à un prospect.
|
||||
:param prospect_id: ID du prospect
|
||||
:param title: Titre de la tâche
|
||||
:param due_date: Date d'échéance au format ISO (YYYY-MM-DD)
|
||||
:param description: Description optionnelle
|
||||
:param priority: 'basse' | 'normale' | 'haute'
|
||||
:param metadata: métadonnées optionnelles
|
||||
:return: ID de la tâche créée ou None si prospect introuvable
|
||||
"""
|
||||
prospect = self.get_prospect_by_id(prospect_id)
|
||||
if not prospect:
|
||||
return None
|
||||
from modules.tasks.task import Task
|
||||
from modules.tasks.task_handler import TaskHandler
|
||||
|
||||
task = Task(
|
||||
title=title,
|
||||
due_date=due_date,
|
||||
description=description,
|
||||
entity_type="prospect",
|
||||
entity_id=prospect_id,
|
||||
priority=priority,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
return TaskHandler().add_task(task)
|
||||
52
modules/crm/scoring.py
Normal file
52
modules/crm/scoring.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from datetime import date
|
||||
from typing import Literal
|
||||
|
||||
from modules.crm.prospect_handler import ProspectHandler
|
||||
|
||||
|
||||
def categorize(score: int) -> Literal["Froid", "Tiède", "Chaud"]:
|
||||
if score is None:
|
||||
score = 0
|
||||
if score >= 50:
|
||||
return "Chaud"
|
||||
if score >= 20:
|
||||
return "Tiède"
|
||||
return "Froid"
|
||||
|
||||
|
||||
def adjust_score(prospect_id: str, delta: int) -> int:
|
||||
"""
|
||||
Ajuste le score d'un prospect et met à jour la dernière interaction.
|
||||
Retourne le score à jour.
|
||||
"""
|
||||
handler = ProspectHandler()
|
||||
p = handler.get_prospect_by_id(prospect_id)
|
||||
if not p:
|
||||
return 0
|
||||
try:
|
||||
current = int(getattr(p, "score", 0) or 0)
|
||||
except Exception:
|
||||
current = 0
|
||||
new_score = max(0, current + int(delta))
|
||||
setattr(p, "score", new_score)
|
||||
setattr(p, "last_interaction", date.today().isoformat())
|
||||
# On ne change pas le statut ici, sauf règle spécifique (reply)
|
||||
handler.update_prospect(p)
|
||||
return new_score
|
||||
|
||||
|
||||
def adjust_score_for_event(prospect_id: str, event: Literal["open", "click", "reply"]) -> int:
|
||||
if event == "open":
|
||||
return adjust_score(prospect_id, 10)
|
||||
if event == "click":
|
||||
return adjust_score(prospect_id, 20)
|
||||
if event == "reply":
|
||||
score = adjust_score(prospect_id, 30)
|
||||
# Règle: un reply passe le prospect en statut "Chaud"
|
||||
handler = ProspectHandler()
|
||||
p = handler.get_prospect_by_id(prospect_id)
|
||||
if p:
|
||||
p.status = "Chaud"
|
||||
handler.update_prospect(p)
|
||||
return score
|
||||
return adjust_score(prospect_id, 0)
|
||||
181
modules/crm/search.py
Normal file
181
modules/crm/search.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
def _project_root() -> str:
|
||||
# modules/crm/search.py -> remonter à la racine projet
|
||||
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def _prospects_dir() -> str:
|
||||
return os.path.join(_project_root(), "Data", "prospects")
|
||||
|
||||
|
||||
def _read_json(path: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _norm_date(date_str: Optional[str]) -> Optional[str]:
|
||||
if not date_str:
|
||||
return None
|
||||
# Tente de normaliser vers YYYY-MM-DD
|
||||
s = str(date_str)
|
||||
# Si déjà au format ISO ou contient "T"
|
||||
if len(s) >= 10:
|
||||
return s[:10]
|
||||
return None
|
||||
|
||||
|
||||
def _extract_fields(data: Dict[str, Any], fallback_id: str) -> Dict[str, Any]:
|
||||
# Essaye plusieurs variantes de clés
|
||||
name = data.get("name") or data.get("nom") or data.get("full_name") or data.get("contact_name") or ""
|
||||
email = data.get("email") or data.get("mail") or ""
|
||||
company = data.get("company") or data.get("societe") or data.get("entreprise") or data.get("organization") or ""
|
||||
status = data.get("status") or data.get("statut") or ""
|
||||
city = data.get("city") or data.get("ville") or ""
|
||||
tags = data.get("tags") or []
|
||||
if isinstance(tags, str):
|
||||
# transformer "a, b, c" -> ["a","b","c"]
|
||||
tags = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
created_at = data.get("created_at") or data.get("created") or data.get("date_created") or ""
|
||||
created_at = _norm_date(created_at) or ""
|
||||
pid = data.get("id") or fallback_id
|
||||
|
||||
# Liaisons potentielles
|
||||
client_id = data.get("client_id") or data.get("clientId") or data.get("client")
|
||||
project_ids = data.get("project_ids") or data.get("projects") or data.get("projectId") or []
|
||||
if isinstance(project_ids, (str, int)):
|
||||
project_ids = [str(project_ids)]
|
||||
elif isinstance(project_ids, list):
|
||||
project_ids = [str(x) for x in project_ids]
|
||||
|
||||
return {
|
||||
"id": pid,
|
||||
"entity_type": "prospect",
|
||||
"name": name,
|
||||
"email": email,
|
||||
"company": company,
|
||||
"status": status,
|
||||
"city": city,
|
||||
"tags": tags,
|
||||
"created_at": created_at,
|
||||
"client_id": client_id,
|
||||
"project_ids": project_ids,
|
||||
}
|
||||
|
||||
|
||||
def load_all_prospects() -> List[Dict[str, Any]]:
|
||||
base_dir = _prospects_dir()
|
||||
os.makedirs(base_dir, exist_ok=True)
|
||||
results: List[Dict[str, Any]] = []
|
||||
for fn in os.listdir(base_dir):
|
||||
if not fn.endswith(".json"):
|
||||
continue
|
||||
path = os.path.join(base_dir, fn)
|
||||
data = _read_json(path)
|
||||
if not data:
|
||||
continue
|
||||
pid = os.path.splitext(fn)[0]
|
||||
results.append(_extract_fields(data, pid))
|
||||
return results
|
||||
|
||||
|
||||
def _contains(hay: str, needle: str) -> bool:
|
||||
return needle in hay
|
||||
|
||||
|
||||
def _safe_lower(s: Optional[str]) -> str:
|
||||
return (s or "").lower()
|
||||
|
||||
|
||||
def filter_prospects(
|
||||
*,
|
||||
q: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
company: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
city: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
date_from: Optional[str] = None, # YYYY-MM-DD
|
||||
date_to: Optional[str] = None, # YYYY-MM-DD
|
||||
) -> List[Dict[str, Any]]:
|
||||
items = load_all_prospects()
|
||||
|
||||
q = _safe_lower(q)
|
||||
name = _safe_lower(name)
|
||||
email = _safe_lower(email)
|
||||
company = _safe_lower(company)
|
||||
status = _safe_lower(status)
|
||||
city = _safe_lower(city)
|
||||
tags = [t.strip().lower() for t in (tags or []) if t.strip()]
|
||||
|
||||
df = None
|
||||
dt = None
|
||||
try:
|
||||
if date_from:
|
||||
df = datetime.strptime(date_from, "%Y-%m-%d").date()
|
||||
except Exception:
|
||||
df = None
|
||||
try:
|
||||
if date_to:
|
||||
dt = datetime.strptime(date_to, "%Y-%m-%d").date()
|
||||
except Exception:
|
||||
dt = None
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for it in items:
|
||||
iname = _safe_lower(it.get("name"))
|
||||
iemail = _safe_lower(it.get("email"))
|
||||
icompany = _safe_lower(it.get("company"))
|
||||
istatus = _safe_lower(it.get("status"))
|
||||
icity = _safe_lower(it.get("city"))
|
||||
itags = [str(t).lower() for t in (it.get("tags") or [])]
|
||||
icreated = it.get("created_at") or ""
|
||||
icreated_d = None
|
||||
try:
|
||||
if icreated:
|
||||
icreated_d = datetime.strptime(icreated[:10], "%Y-%m-%d").date()
|
||||
except Exception:
|
||||
icreated_d = None
|
||||
|
||||
# Filtre global q
|
||||
if q and not (
|
||||
_contains(iname, q) or _contains(iemail, q) or _contains(icompany, q) or _contains(icity, q)
|
||||
):
|
||||
continue
|
||||
|
||||
# Filtres spécifiques
|
||||
if name and not _contains(iname, name):
|
||||
continue
|
||||
if email and not _contains(iemail, email):
|
||||
continue
|
||||
if company and not _contains(icompany, company):
|
||||
continue
|
||||
if status and istatus != status:
|
||||
continue
|
||||
if city and not _contains(icity, city):
|
||||
continue
|
||||
|
||||
if tags:
|
||||
# exiger que tous les tags demandés soient présents
|
||||
itags_set = set(itags)
|
||||
if not all(tag in itags_set for tag in tags):
|
||||
continue
|
||||
|
||||
if df and (not icreated_d or icreated_d < df):
|
||||
continue
|
||||
if dt and (not icreated_d or icreated_d > dt):
|
||||
continue
|
||||
|
||||
out.append(it)
|
||||
|
||||
# On peut trier par date de création desc, puis nom
|
||||
out.sort(key=lambda d: (d.get("created_at") or "", d.get("name") or ""), reverse=True)
|
||||
return out
|
||||
51
modules/crm/status.py
Normal file
51
modules/crm/status.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import unicodedata
|
||||
from enum import Enum
|
||||
from typing import Union, Optional
|
||||
|
||||
|
||||
def _normalize_no_accents(s: str) -> str:
|
||||
"""Normalise une chaîne en supprimant les accents et en la passant en minuscule."""
|
||||
return "".join(c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c)).lower()
|
||||
|
||||
|
||||
class ProspectStatus(str, Enum):
|
||||
NOUVEAU = "Nouveau"
|
||||
CONTACTE = "Contacté"
|
||||
RELANCE = "Relancé"
|
||||
QUALIFIE = "Qualifié"
|
||||
PROPOSITION = "Proposition"
|
||||
NON_INTERESSE = "Non intéressé"
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, value: Optional[Union["ProspectStatus", str]]) -> "ProspectStatus":
|
||||
"""Convertit une valeur (enum ou chaîne) en ProspectStatus, avec tolérance sur la casse/accents."""
|
||||
if isinstance(value, cls):
|
||||
return value
|
||||
if value is None:
|
||||
return cls.NOUVEAU
|
||||
s = str(value).strip()
|
||||
if not s:
|
||||
return cls.NOUVEAU
|
||||
|
||||
n = _normalize_no_accents(s)
|
||||
|
||||
if n in {"nouveau", "new"}:
|
||||
return cls.NOUVEAU
|
||||
if n in {"contacte", "contacted"}:
|
||||
return cls.CONTACTE
|
||||
if n in {"relance", "relaunched"}:
|
||||
return cls.RELANCE
|
||||
if n in {"qualifie", "qualified"}:
|
||||
return cls.QUALIFIE
|
||||
if n in {"proposition", "proposal"}:
|
||||
return cls.PROPOSITION
|
||||
if n in {"non interesse", "noninteresse", "not interested", "refuse", "refus", "abandon"}:
|
||||
return cls.NON_INTERESSE
|
||||
|
||||
# Tentative de correspondance exacte insensible aux accents/casse avec les valeurs de l'enum
|
||||
for member in cls:
|
||||
if _normalize_no_accents(member.value) == n:
|
||||
return member
|
||||
|
||||
# Valeur inconnue -> par défaut
|
||||
return cls.NOUVEAU
|
||||
78
modules/crm/web.py
Normal file
78
modules/crm/web.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from flask import Blueprint, request, jsonify, url_for
|
||||
from typing import List
|
||||
from modules.crm.search import filter_prospects
|
||||
|
||||
crm_api_bp = Blueprint("crm_api", __name__, url_prefix="/api/crm")
|
||||
|
||||
|
||||
@crm_api_bp.get("/prospects/search")
|
||||
def prospects_search():
|
||||
"""
|
||||
API de recherche/filtre prospects.
|
||||
Paramètres (query string):
|
||||
- q: recherche globale (nom, email, société, ville)
|
||||
- name, email, company, status, city: filtres spécifiques (contient, sauf status: égal insensible à la casse)
|
||||
- tags: liste séparée par des virgules (tous doivent être présents)
|
||||
- date_from, date_to: période sur created_at (YYYY-MM-DD)
|
||||
"""
|
||||
q = request.args.get("q")
|
||||
name = request.args.get("name")
|
||||
email = request.args.get("email")
|
||||
company = request.args.get("company")
|
||||
status = request.args.get("status")
|
||||
city = request.args.get("city")
|
||||
tags_raw = request.args.get("tags") or ""
|
||||
tags: List[str] = [t.strip() for t in tags_raw.split(",")] if tags_raw else []
|
||||
date_from = request.args.get("date_from") or None
|
||||
date_to = request.args.get("date_to") or None
|
||||
|
||||
results = filter_prospects(
|
||||
q=q,
|
||||
name=name,
|
||||
email=email,
|
||||
company=company,
|
||||
status=status,
|
||||
city=city,
|
||||
tags=tags,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
)
|
||||
|
||||
# Enrichit avec des URLs navigables
|
||||
enriched = []
|
||||
for it in results:
|
||||
new_it = dict(it)
|
||||
# URL principale (prospect)
|
||||
try:
|
||||
new_it["url"] = url_for("prospect_details", prospect_id=it.get("id"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
related = []
|
||||
cid = it.get("client_id")
|
||||
if cid:
|
||||
try:
|
||||
related.append({"type": "client", "url": url_for("client_details", client_id=cid)})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pids = it.get("project_ids") or []
|
||||
for pid in pids:
|
||||
try:
|
||||
related.append({"type": "project", "url": url_for("project_details", project_id=pid)})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if related:
|
||||
new_it["related"] = related
|
||||
|
||||
# S'assure que le type est présent
|
||||
new_it["entity_type"] = new_it.get("entity_type") or "prospect"
|
||||
enriched.append(new_it)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"count": len(enriched),
|
||||
"results": enriched,
|
||||
}
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue