From c999ab12a95536d9eea9b3f916fdb0518efa937f Mon Sep 17 00:00:00 2001 From: Ladebeze66 Date: Mon, 17 Mar 2025 19:39:32 +0100 Subject: [PATCH] firstcommit --- README.md | 58 +++++++++++++ README.md:Zone.Identifier | 0 config.py | 13 +++ config.py:Zone.Identifier | 0 data_filter.py | 39 +++++++++ data_filter.py:Zone.Identifier | 0 main.py | 8 ++ main.py:Zone.Identifier | 0 menu_handlers.py | 129 ++++++++++++++++++++++++++++ menu_handlers.py:Zone.Identifier | 0 menu_principal.py | 36 ++++++++ menu_principal.py:Zone.Identifier | 0 odoo_connection.py | 33 ++++++++ odoo_connection.py:Zone.Identifier | 0 ticket_manager.py | 131 +++++++++++++++++++++++++++++ ticket_manager.py:Zone.Identifier | 0 utils.py | 38 +++++++++ utils.py:Zone.Identifier | 0 18 files changed, 485 insertions(+) create mode 100644 README.md create mode 100644 README.md:Zone.Identifier create mode 100644 config.py create mode 100644 config.py:Zone.Identifier create mode 100644 data_filter.py create mode 100644 data_filter.py:Zone.Identifier create mode 100644 main.py create mode 100644 main.py:Zone.Identifier create mode 100644 menu_handlers.py create mode 100644 menu_handlers.py:Zone.Identifier create mode 100644 menu_principal.py create mode 100644 menu_principal.py:Zone.Identifier create mode 100644 odoo_connection.py create mode 100644 odoo_connection.py:Zone.Identifier create mode 100644 ticket_manager.py create mode 100644 ticket_manager.py:Zone.Identifier create mode 100644 utils.py create mode 100644 utils.py:Zone.Identifier diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b9045c --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Gestionnaire de Tickets Odoo Simplifié + +Ce projet est une version simplifiée et optimisée du gestionnaire de tickets Odoo. Il permet d'interagir avec une instance Odoo pour gérer des tickets de projet. + +## Fonctionnalités + +1. **Afficher la liste des modèles** - Affiche tous les modèles disponibles dans l'instance Odoo. +2. **Afficher les champs d'un modèle** - Affiche tous les champs d'un modèle donné. +3. **Exporter les informations des champs en JSON** - Exporte la structure des champs d'un modèle en format JSON. +4. **Exporter les tickets d'un project_id par étape** - Exporte tous les tickets d'un project_id, classés par étape (stage_id). + +## Structure du projet + +- `main.py` - Point d'entrée principal du programme +- `menu_principal.py` - Gestion du menu principal +- `menu_handlers.py` - Gestionnaires d'actions pour chaque option du menu +- `ticket_manager.py` - Classe principale pour la gestion des tickets et modèles +- `odoo_connection.py` - Gestion de la connexion à l'instance Odoo +- `data_filter.py` - Fonctions pour filtrer et nettoyer les données des tickets +- `utils.py` - Fonctions utilitaires diverses +- `config.py` - Configuration de l'application (connexion Odoo, chemins d'export, etc.) + +## Prérequis + +- Python 3.6 ou supérieur +- Package `odoorpc` pour la connexion à Odoo +- Package `bs4` (BeautifulSoup) pour le nettoyage des données HTML + +## Installation + +1. Installer les dépendances : + ``` + pip install odoorpc bs4 + ``` + +2. Configurer les variables d'environnement (ou modifier `config.py`) : + - `ODOO_HOST` : Hôte de l'instance Odoo + - `ODOO_DB` : Nom de la base de données Odoo + - `ODOO_USER` : Nom d'utilisateur Odoo + - `ODOO_PASSWORD` : Mot de passe Odoo + +## Utilisation + +1. Exécuter le programme : + ``` + python main.py + ``` + +2. Suivre les instructions du menu pour utiliser les différentes fonctionnalités. + +## Exemple d'utilisation + +### Exporter les tickets d'un projet par étape + +1. Sélectionner l'option 4 dans le menu +2. Entrer l'ID du projet (par exemple, "5") +3. Confirmer l'action +4. Les tickets seront exportés dans le répertoire `exported_tickets/project_5_NomDuProjet/`, classés par étape \ No newline at end of file diff --git a/README.md:Zone.Identifier b/README.md:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/config.py b/config.py new file mode 100644 index 0000000..4d33cc3 --- /dev/null +++ b/config.py @@ -0,0 +1,13 @@ +import os + +# Configuration Odoo +ODOO_HOST = os.getenv('ODOO_HOST', 'odoo.cbao.fr') +ODOO_DB = os.getenv('ODOO_DB', 'production_cbao') +ODOO_USER = os.getenv('ODOO_USER', 'fernand@cbao.fr') +ODOO_PASSWORD = os.getenv('ODOO_PASSWORD', 'Lestat66!') + +# Configuration export +EXPORT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "exported_tickets") + +# Créer le répertoire d'export s'il n'existe pas +os.makedirs(EXPORT_DIR, exist_ok=True) \ No newline at end of file diff --git a/config.py:Zone.Identifier b/config.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/data_filter.py b/data_filter.py new file mode 100644 index 0000000..2df1681 --- /dev/null +++ b/data_filter.py @@ -0,0 +1,39 @@ +import re +import html +from bs4 import BeautifulSoup + +def clean_html(content): + """Nettoie le contenu HTML en supprimant les balises.""" + if not content: + return "" + soup = BeautifulSoup(content, 'html.parser') + return soup.get_text(separator='\n', strip=True) + +def filter_ticket_data(ticket_data): + """Filtre les données d'un ticket pour ne garder que les informations essentielles.""" + # Créer un nouveau dictionnaire pour le ticket filtré + filtered_ticket = { + "ID du Ticket": ticket_data["ID du Ticket"], + "Nom": ticket_data["Nom"], + "Code": ticket_data.get("Code", "N/A"), + "Date Limite": ticket_data["Date Limite"], + "Champs Simples": ticket_data["Champs Simples"], # Conserver tous les champs simples + "Champs Relationnels": ticket_data["Champs Relationnels"], # Conserver tous les champs relationnels + "Discussions": [] # Initialiser la liste des discussions + } + + # Nettoyer le champ description dans Champs Simples + if "description" in filtered_ticket["Champs Simples"]: + filtered_ticket["Champs Simples"]["description"] = clean_html(filtered_ticket["Champs Simples"]["description"]) + + # Garder uniquement les discussions nécessaires + for msg in ticket_data["Discussions"]: + filtered_ticket["Discussions"].append({ + "ID Message": msg["ID Message"], + "Sujet": msg["Sujet"], + "Contenu": clean_html(msg["Contenu"]), # Nettoyage du contenu HTML + "Auteur": msg["Auteur"], + "Date": msg["Date"] + }) + + return filtered_ticket \ No newline at end of file diff --git a/data_filter.py:Zone.Identifier b/data_filter.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..3d894a2 --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +from menu_principal import run_menu + +def main(): + + run_menu() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py:Zone.Identifier b/main.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/menu_handlers.py b/menu_handlers.py new file mode 100644 index 0000000..747e676 --- /dev/null +++ b/menu_handlers.py @@ -0,0 +1,129 @@ +from ticket_manager import TicketManager + +# Initialisation de l'objet +ticket_manager = TicketManager() + +def handle_list_models(): + """Gère l'affichage de la liste des modèles""" + ticket_manager.list_models() + + +def handle_list_model_fields(): + """Gère l'affichage des champs d'un modèle""" + model_name = input("\nEntrez le nom du modèle: ") + if not model_name: + print("Aucun nom de modèle fourni.") + return + ticket_manager.list_model_fields(model_name) + + +def handle_export_model_fields_to_json(): + """Gère l'exportation des informations des champs d'un modèle en JSON""" + model_name = input("\nEntrez le nom du modèle: ") + if not model_name: + print("Aucun nom de modèle fourni.") + return + filename = input("Entrez le nom du fichier pour l'exportation: ") + if not filename: + print("Aucun nom de fichier fourni.") + return + ticket_manager.export_model_fields_to_json(model_name, filename) + + +def handle_project_tickets_by_stage(): + """Gère l'exportation des tickets d'un projet par étape""" + # Récupérer la liste des projets disponibles + projects = ticket_manager.get_available_projects() + if not projects: + print("Aucun projet disponible. Impossible de continuer.") + return + + # Demander à l'utilisateur de choisir un projet + project_id_input = input("\nEntrez l'ID du projet (ou 'q' pour quitter): ") + if project_id_input.lower() == 'q': + return + + try: + project_id = int(project_id_input) + if project_id not in projects: + print(f"Aucun projet trouvé avec l'ID: {project_id}") + return + except ValueError: + print("L'ID du projet doit être un nombre entier.") + return + + # Récupérer les étapes (stage_id) du projet + print(f"\nRécupération des étapes du projet: {projects[project_id]} (ID: {project_id})") + stages = ticket_manager.get_project_stages(project_id) + + if not stages: + print("Aucune étape trouvée pour ce projet. Impossible de continuer.") + return + + # Afficher les étapes disponibles + print("\nÉtapes disponibles:") + for stage_id, stage_name in stages.items(): + print(f"ID: {stage_id} - {stage_name}") + + # Demander à l'utilisateur s'il veut sélectionner ou exclure des étapes + selection_mode = input("\nSouhaitez-vous:\n1. Exporter toutes les étapes\n2. Sélectionner des étapes spécifiques par ID\n3. Exclure certaines étapes par ID\nVotre choix (1/2/3): ") + + selected_stage_ids = None + + if selection_mode == '2': + # Sélectionner des étapes spécifiques par ID + stage_id_input = input("Entrez les IDs des étapes à inclure (séparés par des virgules): ") + try: + selected_stage_ids = [int(x.strip()) for x in stage_id_input.split(',') if x.strip()] + # Vérifier que les IDs existent dans les étapes disponibles + valid_ids = [stage_id for stage_id in selected_stage_ids if stage_id in stages] + invalid_ids = [stage_id for stage_id in selected_stage_ids if stage_id not in stages] + + selected_stage_ids = valid_ids + + if invalid_ids: + print(f"Attention: Les IDs suivants ne sont pas valides et seront ignorés: {', '.join(map(str, invalid_ids))}") + + if not selected_stage_ids: + print("Aucun ID d'étape valide sélectionné. Exportation annulée.") + return + + print(f"Étapes sélectionnées: {', '.join([f'{stages[stage_id]} (ID: {stage_id})' for stage_id in selected_stage_ids])}") + except ValueError: + print("Erreur dans la sélection des étapes. Format attendu: 1,2,3,...") + return + + elif selection_mode == '3': + # Exclure certaines étapes par ID + stage_id_input = input("Entrez les IDs des étapes à exclure (séparés par des virgules): ") + try: + excluded_stage_ids = [int(x.strip()) for x in stage_id_input.split(',') if x.strip()] + # Vérifier que les IDs existent dans les étapes disponibles + valid_excluded_ids = [stage_id for stage_id in excluded_stage_ids if stage_id in stages] + invalid_ids = [stage_id for stage_id in excluded_stage_ids if stage_id not in stages] + + excluded_stage_ids = valid_excluded_ids + + if invalid_ids: + print(f"Attention: Les IDs suivants ne sont pas valides et seront ignorés: {', '.join(map(str, invalid_ids))}") + + # Sélectionner toutes les étapes sauf les exclues + selected_stage_ids = [stage_id for stage_id in stages.keys() if stage_id not in excluded_stage_ids] + + if not selected_stage_ids: + print("Toutes les étapes ont été exclues. Exportation annulée.") + return + + print(f"Étapes sélectionnées: {', '.join([f'{stages[stage_id]} (ID: {stage_id})' for stage_id in selected_stage_ids])}") + except ValueError: + print("Erreur dans la sélection des étapes. Format attendu: 1,2,3,...") + return + + # Confirmer l'action + confirmation = input(f"\nVoulez-vous exporter les tickets du projet {projects[project_id]}? (o/n): ") + if confirmation.lower() != 'o': + print("Exportation annulée.") + return + + # Exporter les tickets + ticket_manager.export_tickets_by_project_and_stage(project_id, selected_stage_ids) \ No newline at end of file diff --git a/menu_handlers.py:Zone.Identifier b/menu_handlers.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/menu_principal.py b/menu_principal.py new file mode 100644 index 0000000..7bab97b --- /dev/null +++ b/menu_principal.py @@ -0,0 +1,36 @@ +from menu_handlers import ( + handle_list_models, + handle_list_model_fields, + handle_export_model_fields_to_json, + handle_project_tickets_by_stage +) + +def display_main_menu(): + """Affiche le menu principal de l'application""" + print("\n==== GESTIONNAIRE DE TICKETS ODOO ====") + print("1. Afficher la liste des modèles") + print("2. Afficher les champs d'un modèle") + print("3. Exporter les informations des champs d'un modèle en JSON") + print("4. Exporter les tickets d'un project_id par étape") + print("5. Quitter") + return input("\nChoisissez une option (1-5): ") + + +def run_menu(): + """Exécute la boucle du menu principal""" + while True: + choice = display_main_menu() + + if choice == '1': + handle_list_models() + elif choice == '2': + handle_list_model_fields() + elif choice == '3': + handle_export_model_fields_to_json() + elif choice == '4': + handle_project_tickets_by_stage() + elif choice == '5': + print("Au revoir!") + break + else: + print("Option invalide. Veuillez choisir entre 1 et 5.") \ No newline at end of file diff --git a/menu_principal.py:Zone.Identifier b/menu_principal.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/odoo_connection.py b/odoo_connection.py new file mode 100644 index 0000000..75a8d0c --- /dev/null +++ b/odoo_connection.py @@ -0,0 +1,33 @@ +import odoorpc +from config import ODOO_HOST, ODOO_DB, ODOO_USER, ODOO_PASSWORD + +class OdooConnection: + """Gère la connexion à l'instance Odoo""" + + def __init__(self): + self.odoo = None + self.connected = False + + def connect(self): + """Établit la connexion à Odoo""" + try: + self.odoo = odoorpc.ODOO(ODOO_HOST, port=443, protocol='jsonrpc+ssl') + print(f"Connexion réussie à {ODOO_HOST}") + + self.odoo.login(ODOO_DB, ODOO_USER, ODOO_PASSWORD) + print(f"Authentifié en tant que {ODOO_USER}") + + self.connected = True + return True + except odoorpc.error.RPCError as e: + print(f"Erreur RPC Odoo : {e}") + return False + except Exception as e: + print(f"Erreur inattendue : {e}") + return False + + def get_odoo_instance(self): + """Retourne l'instance Odoo connectée""" + if not self.connected: + self.connect() + return self.odoo \ No newline at end of file diff --git a/odoo_connection.py:Zone.Identifier b/odoo_connection.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/ticket_manager.py b/ticket_manager.py new file mode 100644 index 0000000..7c468b8 --- /dev/null +++ b/ticket_manager.py @@ -0,0 +1,131 @@ +from odoo_connection import OdooConnection +import os +import json +from utils import save_json, ensure_export_directory +from config import EXPORT_DIR +from data_filter import filter_ticket_data + +class TicketManager: + """Gestionnaire de tickets simplifié avec seulement les fonctionnalités essentielles""" + + def __init__(self): + """Initialise le gestionnaire de tickets""" + self.conn = OdooConnection() + self.odoo = self.conn.get_odoo_instance() + self.model_name = "project.task" + + def _check_connection(self): + """Vérifie la connexion Odoo""" + if self.odoo is None: + try: + self.conn = OdooConnection() + self.odoo = self.conn.get_odoo_instance() + except Exception as e: + print(f"Erreur de connexion: {e}") + self.odoo = None + return self.odoo is not None + + def _safe_execute(self, model, method, *args): + """Exécute une méthode Odoo de manière sécurisée""" + if not self._check_connection(): + return None + try: + return self.odoo.execute(model, method, *args) + except Exception as e: + print(f"Erreur lors de {method} sur {model}: {e}") + return None + + def list_models(self): + """Affiche la liste des modèles disponibles dans Odoo""" + models = self._safe_execute('ir.model', 'search_read', [], ['model', 'name']) + if not models: + print("Aucun modèle disponible.") + return [] + + print("\nListe des modèles disponibles:") + for model in models: + print(f"Modèle: {model['name']} (ID: {model['model']})") + return models + + def list_model_fields(self, model_name): + """Affiche les champs d'un modèle donné""" + fields_info = self._safe_execute(model_name, 'fields_get') + if not fields_info: + print(f"Aucun champ trouvé pour le modèle {model_name}.") + return [] + + print(f"\nChamps du modèle {model_name}:") + for field_name, field_data in fields_info.items(): + print(f"Champ: {field_name} - Type: {field_data['type']}") + return fields_info + + def get_project_tickets_summary(self, project_id): + """Récupère un résumé des tickets d'un projet pour permettre la sélection""" + domain = [('project_id', '=', project_id)] + ticket_ids = self._safe_execute(self.model_name, 'search', domain, 0, 200) + + if not ticket_ids: + print(f"Aucun ticket trouvé pour le projet ID: {project_id}") + return [] + + fields_to_read = ['id', 'name', 'code', 'stage_id', 'date_deadline'] + tickets_data = self._safe_execute(self.model_name, 'read', ticket_ids, fields_to_read) + + if not tickets_data: + print("Erreur lors de la récupération des données des tickets.") + return [] + + summary_tickets = [] + for ticket in tickets_data: + stage_name = "Non défini" + if ticket.get('stage_id'): + stage_name = ticket['stage_id'][1] if isinstance(ticket['stage_id'], list) and len(ticket['stage_id']) > 1 else str(ticket['stage_id']) + + summary_tickets.append({ + 'id': ticket['id'], + 'name': ticket['name'], + 'code': ticket.get('code', 'N/A'), + 'stage_id': ticket.get('stage_id', [0, "Non défini"]), + 'stage_name': stage_name, + 'date_deadline': ticket.get('date_deadline', 'Non défini') + }) + + return summary_tickets + + def export_tickets_by_project_and_stage(self, project_id, selected_stage_ids=None): + """Exporte les tickets d'un projet classés par étape""" + project_data = self._safe_execute('project.project', 'search_read', [('id', '=', project_id)], ['id', 'name']) + if not project_data: + print(f"Projet ID {project_id} introuvable") + return + + project_name = project_data[0]['name'] + domain = [('project_id', '=', project_id)] + if selected_stage_ids: + domain.append(('stage_id', 'in', selected_stage_ids)) + + ticket_ids = self._safe_execute(self.model_name, 'search', domain, 0, 1000) + if not ticket_ids: + print("Aucun ticket trouvé") + return + + tickets = [self.get_ticket_by_id(ticket_id) for ticket_id in ticket_ids] + + tickets_by_stage = {} + for ticket in tickets: + stage_id = ticket["Champs Relationnels"].get("stage_id", [0, "Non classé"])[0] + stage_name = ticket["Champs Relationnels"].get("stage_id", [0, "Non classé"])[1] + key = f"{stage_id}_{stage_name}" + + if key not in tickets_by_stage: + tickets_by_stage[key] = [] + tickets_by_stage[key].append(ticket) + + project_dir = ensure_export_directory(f"project_{project_id}_{project_name.replace(' ', '_')}") + + for stage_key, stage_tickets in tickets_by_stage.items(): + stage_dir = os.path.join(project_dir, stage_key) + os.makedirs(stage_dir, exist_ok=True) + save_json(os.path.join(stage_dir, "all_tickets.json"), stage_tickets) + + print(f"Exportation terminée dans {project_dir}/") diff --git a/ticket_manager.py:Zone.Identifier b/ticket_manager.py:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..fe484c9 --- /dev/null +++ b/utils.py @@ -0,0 +1,38 @@ +import os +import json +from config import EXPORT_DIR + +def ensure_export_directory(subdir=None): + """Assure que le répertoire d'export existe""" + if subdir: + directory = os.path.join(EXPORT_DIR, subdir) + else: + directory = EXPORT_DIR + os.makedirs(directory, exist_ok=True) + return directory + +def save_json(filename, data): + """Sauvegarde des données au format JSON""" + try: + with open(filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + return True + except Exception as e: + print(f"Erreur lors de la sauvegarde du fichier {filename}: {e}") + return False + +def get_user_choice(prompt, options): + """Obtient un choix utilisateur parmi une liste d'options""" + while True: + print(prompt) + for i, option in enumerate(options, 1): + print(f"{i}. {option}") + + try: + choice = int(input("Votre choix: ")) + if 1 <= choice <= len(options): + return choice + else: + print("Choix invalide. Veuillez réessayer.") + except ValueError: + print("Veuillez entrer un nombre valide.") \ No newline at end of file diff --git a/utils.py:Zone.Identifier b/utils.py:Zone.Identifier new file mode 100644 index 0000000..e69de29