diff --git a/__pycache__/menu_handlers.cpython-312.pyc b/__pycache__/menu_handlers.cpython-312.pyc index a062c40..656b46b 100644 Binary files a/__pycache__/menu_handlers.cpython-312.pyc and b/__pycache__/menu_handlers.cpython-312.pyc differ diff --git a/__pycache__/menu_principal.cpython-312.pyc b/__pycache__/menu_principal.cpython-312.pyc index 5ee4a87..393aa6a 100644 Binary files a/__pycache__/menu_principal.cpython-312.pyc and b/__pycache__/menu_principal.cpython-312.pyc differ diff --git a/__pycache__/ticket_manager.cpython-312.pyc b/__pycache__/ticket_manager.cpython-312.pyc index f390b49..3202604 100644 Binary files a/__pycache__/ticket_manager.cpython-312.pyc and b/__pycache__/ticket_manager.cpython-312.pyc differ diff --git a/menu_handlers.py b/menu_handlers.py index d259bf3..8e0abe0 100644 --- a/menu_handlers.py +++ b/menu_handlers.py @@ -18,12 +18,34 @@ def handle_list_model_fields(): if model_name.lower() == 'q': return + # Assurons-nous que model_name est bien une chaîne de caractères + model_name = str(model_name) + fields = ticket_manager.get_model_fields_with_types(model_name) if fields: print(f"\nChamps du modèle '{model_name}':") for field, info in fields.items(): relation_info = f" (relation avec {info['relation']})" if info["relation"] else "" - print(f"- {field} : {info['type']}{relation_info}") + required_info = " [Obligatoire]" if info.get("required") else "" + readonly_info = " [Lecture seule]" if info.get("readonly") else "" + + print(f"- {field} : {info['type']}{relation_info}{required_info}{readonly_info}") + + # Afficher la description du champ si disponible + if info.get("string") and info.get("string") != field: + print(f" Label: {info['string']}") + + # Afficher l'aide si disponible + if info.get("help"): + print(f" Description: {info['help']}") + + # Afficher les options si c'est un champ de sélection + if info.get("selection"): + print(" Options:") + for key, value in info["selection"]: + print(f" - {key}: {value}") + + print("") # Ligne vide pour séparer les champs def handle_search_ticket_by_id(): """Gère la recherche d'un ticket par ID""" @@ -114,4 +136,25 @@ def handle_project_tickets_by_stage(): return # Exporter les tickets selon les filtres choisis - ticket_manager.export_tickets_by_project_and_stage(project_id, stage_id) \ No newline at end of file + ticket_manager.export_tickets_by_project_and_stage(project_id, stage_id) + +def handle_extract_ticket_attachments(): + """Gère l'extraction des pièces jointes et messages d'un ticket""" + # Demander à l'utilisateur d'entrer l'ID du ticket + ticket_id_input = input("\nEntrez l'ID du ticket à extraire (ou 'q' pour quitter): ") + if ticket_id_input.lower() == 'q': + return + + try: + ticket_id = int(ticket_id_input) + except ValueError: + print_error("L'ID du ticket doit être un nombre entier.") + return + + # Extraire les pièces jointes et les messages + output_dir = ticket_manager.extract_ticket_attachments_and_messages(ticket_id) + + if output_dir: + print(f"\nExtraction terminée avec succès. Les fichiers ont été enregistrés dans: {output_dir}") + else: + print_error(f"L'extraction a échoué pour le ticket avec l'ID {ticket_id}") \ No newline at end of file diff --git a/menu_principal.py b/menu_principal.py index a46f602..62c66a0 100644 --- a/menu_principal.py +++ b/menu_principal.py @@ -3,7 +3,8 @@ from menu_handlers import ( handle_search_ticket_by_id, handle_search_ticket_by_code, handle_list_models, - handle_list_model_fields + handle_list_model_fields, + handle_extract_ticket_attachments ) def display_main_menu(): @@ -14,8 +15,9 @@ def display_main_menu(): print("3. Rechercher un ticket par Code") print("4. Afficher la liste des modèles disponibles") print("5. Afficher les champs d'un modèle donné") - print("6. Quitter") - return input("\nChoisissez une option (1-6): ") + print("6. Extraire les pièces jointes, messages et informations détaillées d'un ticket") + print("7. Quitter") + return input("\nChoisissez une option (1-7): ") def run_menu(): @@ -33,7 +35,9 @@ def run_menu(): elif choice == '5': handle_list_model_fields() elif choice == '6': + handle_extract_ticket_attachments() + elif choice == '7': print("Au revoir!") break else: - print("Option invalide. Veuillez choisir entre 1 et 6.") \ No newline at end of file + print("Option invalide. Veuillez choisir entre 1 et 7.") \ No newline at end of file diff --git a/ticket_manager.py b/ticket_manager.py index 71e3370..e54fc2a 100644 --- a/ticket_manager.py +++ b/ticket_manager.py @@ -1,6 +1,7 @@ from odoo_connection import OdooConnection import os import json +import base64 from utils import print_error from config import EXPORT_DIR @@ -75,17 +76,27 @@ class TicketManager: return models_dict def get_model_fields_with_types(self, model_name): - """Récupère et sauvegarde les champs d'un modèle avec leurs types""" - fields_info = self._safe_execute(model_name, 'fields_get', [], ['name', 'type', 'relation']) + """Récupère et sauvegarde les champs d'un modèle avec tous leurs attributs""" + # Récupérer tous les attributs disponibles pour chaque champ + fields_info = self._safe_execute(model_name, 'fields_get', []) if not fields_info: print_error(f"Impossible de récupérer les champs pour {model_name}") return {} - # Construire un dictionnaire {champ: {type, relation (si relationnel)}} + # Sauvegarde en JSON (tous les attributs sont conservés) + self.save_raw_ticket_data(fields_info, f"fields_{model_name}_complete.json") + print(f"Liste complète des champs du modèle '{model_name}' sauvegardée dans 'fields_{model_name}_complete.json'") + + # Pour la compatibilité, construire aussi le dictionnaire simplifié fields_dict = { field: { - "type": info["type"], - "relation": info.get("relation", None) + "type": info.get("type", "unknown"), + "relation": info.get("relation", None), + "string": info.get("string", field), + "help": info.get("help", ""), + "required": info.get("required", False), + "readonly": info.get("readonly", False), + "selection": info.get("selection", []) if info.get("type") == "selection" else None } for field, info in fields_info.items() } @@ -255,3 +266,190 @@ class TicketManager: print(f"Exportation terminée. Les fichiers sont organisés dans : {export_base_dir}/") + def extract_ticket_attachments_and_messages(self, ticket_id): + """ + Extrait toutes les pièces jointes et messages d'un ticket + et les sauvegarde dans un répertoire dédié. + + Args: + ticket_id: ID du ticket à extraire + """ + # Récupérer les informations du ticket + ticket = self.get_ticket_by_id(ticket_id) + if not ticket: + print_error(f"Impossible de trouver le ticket avec l'ID {ticket_id}") + return + + # Créer un répertoire pour ce ticket + ticket_name = ticket.get('name', 'Sans nom').replace('/', '_').replace('\\', '_') + ticket_dir = os.path.join(EXPORT_DIR, f"ticket_{ticket_id}_{ticket_name}") + os.makedirs(ticket_dir, exist_ok=True) + + # Sauvegarder les données du ticket + self.save_raw_ticket_data(ticket, os.path.join(ticket_dir, "ticket_info.json")) + + # Récupérer les informations de contact supplémentaires si disponibles + contact_info = {} + if ticket.get('partner_id'): + partner_id = ticket.get('partner_id')[0] if isinstance(ticket.get('partner_id'), list) else ticket.get('partner_id') + partner_details = self._safe_execute('res.partner', 'read', [partner_id], + ['name', 'email', 'phone', 'mobile', 'street', 'city', 'zip', 'country_id', 'comment']) + if partner_details: + contact_info = partner_details[0] + self.save_raw_ticket_data(contact_info, os.path.join(ticket_dir, "contact_info.json")) + print(f"Informations de contact extraites pour le partenaire {partner_id}") + + # Récupérer les activités liées au ticket + activity_ids = ticket.get('activity_ids', []) + if activity_ids: + activities = self._safe_execute('mail.activity', 'read', activity_ids, + ['id', 'res_id', 'activity_type_id', 'summary', 'note', 'date_deadline', 'user_id', 'create_date']) + if activities: + self.save_raw_ticket_data(activities, os.path.join(ticket_dir, "activities.json")) + print(f"{len(activities)} activités extraites pour le ticket {ticket_id}") + + # Récupérer les messages (discussions) + message_ids = ticket.get('message_ids', []) + if message_ids: + # Récupérer tous les messages avec leurs détails + messages = self._safe_execute('mail.message', 'read', message_ids, + ['id', 'body', 'date', 'author_id', 'email_from', 'subject', 'parent_id', 'message_type', 'subtype_id', 'attachment_ids']) + + if messages: + # Sauvegarder tous les messages dans un fichier + self.save_raw_ticket_data(messages, os.path.join(ticket_dir, "messages.json")) + + # Organiser les messages par fil de discussion + messages_by_thread = {} + for message in messages: + parent_id = message.get('parent_id', False) + parent_id = parent_id[0] if isinstance(parent_id, list) and len(parent_id) > 0 else False + + if not parent_id: # Message principal + thread_id = message['id'] + if thread_id not in messages_by_thread: + messages_by_thread[thread_id] = { + 'main_message': message, + 'replies': [] + } + else: # Réponse à un message + if parent_id not in messages_by_thread: + messages_by_thread[parent_id] = { + 'main_message': None, + 'replies': [] + } + messages_by_thread[parent_id]['replies'].append(message) + + # Sauvegarder les fils de discussion + self.save_raw_ticket_data(messages_by_thread, os.path.join(ticket_dir, "message_threads.json")) + + # Sauvegarder chaque message dans un fichier séparé pour une meilleure lisibilité + messages_dir = os.path.join(ticket_dir, "messages") + os.makedirs(messages_dir, exist_ok=True) + + for message in messages: + message_id = message.get('id', 0) + message_date = message.get('date', '').replace(':', '-').replace(' ', '_') + message_path = os.path.join(messages_dir, f"message_{message_id}_{message_date}.json") + + # Récupérer les détails de l'auteur si disponible + if message.get('author_id') and isinstance(message.get('author_id'), list) and len(message.get('author_id')) > 0: + author_id = message.get('author_id')[0] + author_details = self._safe_execute('res.partner', 'read', [author_id], ['name', 'email', 'phone', 'function', 'company_id']) + if author_details: + message['author_details'] = author_details[0] + + # Récupérer les détails du sous-type si disponible + if message.get('subtype_id') and isinstance(message.get('subtype_id'), list) and len(message.get('subtype_id')) > 0: + subtype_id = message.get('subtype_id')[0] + subtype_details = self._safe_execute('mail.message.subtype', 'read', [subtype_id], ['name', 'description', 'default']) + if subtype_details: + message['subtype_details'] = subtype_details + + with open(message_path, "w", encoding="utf-8") as f: + json.dump(message, f, indent=4, ensure_ascii=False) + + print(f"{len(messages)} messages extraits pour le ticket {ticket_id}") + + # Récupérer les suiveurs du ticket + follower_ids = ticket.get('message_follower_ids', []) + if follower_ids: + followers = self._safe_execute('mail.followers', 'read', follower_ids, ['id', 'partner_id', 'name', 'email', 'subtype_ids']) + if followers: + # Enrichir les informations des suiveurs + for follower in followers: + if follower.get('partner_id') and isinstance(follower.get('partner_id'), list) and len(follower.get('partner_id')) > 0: + partner_id = follower.get('partner_id')[0] + partner_details = self._safe_execute('res.partner', 'read', [partner_id], ['name', 'email', 'phone', 'function', 'company_id']) + if partner_details: + follower['partner_details'] = partner_details[0] + + # Récupérer les détails des sous-types + if follower.get('subtype_ids'): + subtype_details = self._safe_execute('mail.message.subtype', 'read', follower.get('subtype_ids'), ['name', 'description', 'default']) + if subtype_details: + follower['subtype_details'] = subtype_details + + self.save_raw_ticket_data(followers, os.path.join(ticket_dir, "followers.json")) + print(f"{len(followers)} suiveurs extraits pour le ticket {ticket_id}") + + # Récupérer les pièces jointes + attachment_ids = self._safe_execute('ir.attachment', 'search', + [('res_model', '=', self.model_name), ('res_id', '=', ticket_id)]) + + if attachment_ids: + attachments = self._safe_execute('ir.attachment', 'read', attachment_ids, + ['id', 'name', 'datas', 'mimetype', 'create_date', 'create_uid', 'description', 'res_name', 'public', 'type']) + + if attachments: + # Sauvegarder les métadonnées des pièces jointes + attachments_info = [{ + 'id': att.get('id'), + 'name': att.get('name'), + 'mimetype': att.get('mimetype'), + 'create_date': att.get('create_date'), + 'description': att.get('description'), + 'res_name': att.get('res_name'), + 'type': att.get('type'), + 'file_path': f"attachments/{att.get('id')}_{att.get('name', '').replace('/', '_')}" + } for att in attachments] + + self.save_raw_ticket_data(attachments_info, os.path.join(ticket_dir, "attachments_info.json")) + + # Créer un répertoire pour les pièces jointes + attachments_dir = os.path.join(ticket_dir, "attachments") + os.makedirs(attachments_dir, exist_ok=True) + + # Sauvegarder chaque pièce jointe + for attachment in attachments: + attachment_id = attachment.get('id', 0) + attachment_name = attachment.get('name', f"attachment_{attachment_id}").replace('/', '_') + attachment_data = attachment.get('datas') + + if attachment_data: + try: + # Décoder les données base64 + binary_data = base64.b64decode(attachment_data) + file_path = os.path.join(attachments_dir, f"{attachment_id}_{attachment_name}") + + with open(file_path, "wb") as f: + f.write(binary_data) + + print(f"Pièce jointe {attachment_name} sauvegardée dans {file_path}") + except Exception as e: + print_error(f"Erreur lors de la sauvegarde de la pièce jointe {attachment_name}: {e}") + + print(f"{len(attachments)} pièces jointes extraites pour le ticket {ticket_id}") + + # Extraire les historiques de timesheet si disponibles + timesheet_ids = ticket.get('timesheet_ids', []) + if timesheet_ids: + timesheets = self._safe_execute('account.analytic.line', 'read', timesheet_ids, + ['id', 'date', 'user_id', 'name', 'unit_amount', 'employee_id', 'department_id']) + if timesheets: + self.save_raw_ticket_data(timesheets, os.path.join(ticket_dir, "timesheets.json")) + print(f"{len(timesheets)} feuilles de temps extraites pour le ticket {ticket_id}") + + print(f"Extraction terminée. Les fichiers sont disponibles dans: {ticket_dir}") + return ticket_dir +