From 4cd697e11ad76812f41a09d09603d796ff58e842 Mon Sep 17 00:00:00 2001 From: Ladebeze66 Date: Tue, 15 Apr 2025 16:08:08 +0200 Subject: [PATCH] 1504-16:07testrecupok --- ...-03-documentation-for-odoo-extractor-py.md | 994 ++++++++++++++++++ extract_single_message.py | 142 +++ extract_single_message_filter.py | 110 ++ extract_text_from_html.py | 68 ++ extract_tracking_value_by_id.py | 52 + extracted_message/image.png | Bin 0 -> 25267 bytes extracted_message/message_228942_clean.html | 14 + extracted_message/message_text.txt | 7 + messages_raw.json | 273 +++++ tracking_value_result.json | 9 + 10 files changed, 1669 insertions(+) create mode 100644 extract_single_message.py create mode 100644 extract_single_message_filter.py create mode 100644 extract_text_from_html.py create mode 100644 extract_tracking_value_by_id.py create mode 100644 extracted_message/image.png create mode 100644 extracted_message/message_228942_clean.html create mode 100644 extracted_message/message_text.txt create mode 100644 messages_raw.json create mode 100644 tracking_value_result.json diff --git a/.specstory/history/2025-04-15_13-03-documentation-for-odoo-extractor-py.md b/.specstory/history/2025-04-15_13-03-documentation-for-odoo-extractor-py.md index 764b7ac..0413662 100644 --- a/.specstory/history/2025-04-15_13-03-documentation-for-odoo-extractor-py.md +++ b/.specstory/history/2025-04-15_13-03-documentation-for-odoo-extractor-py.md @@ -164,6 +164,1000 @@ Cette commande affichera toutes les étapes disponibles dans votre instance Odoo N'hésitez pas à ajuster les exemples selon vos besoins spécifiques ou à demander d'autres modifications si nécessaire ! +--- + +_**User**_ + +peux tu me corriger ce fichier + +--- + +_**Assistant**_ + +Voici une version corrigée et améliorée de votre fichier `extract_single_message.py`. J'ai ajouté quelques commentaires pour clarifier les modifications apportées. + +```python +# extract_single_message.py + +import json +import os +from bs4 import BeautifulSoup + +# ID du message à extraire +TARGET_ID = 228942 +INPUT_FILE = "messages_raw.json" +OUTPUT_DIR = "extracted_message" +os.makedirs(OUTPUT_DIR, exist_ok=True) + +def extract_message(): + with open(INPUT_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + + messages = data.get("messages", []) + found = next((m for m in messages if m["id"] == TARGET_ID), None) + + if not found: + print(f"❌ Message ID {TARGET_ID} non trouvé.") + return + + print(f"✅ Message ID {TARGET_ID} trouvé.") + + # 1. Sauvegarder le HTML brut + body_html = found.get("body", "") + html_path = os.path.join(OUTPUT_DIR, f"message_{TARGET_ID}.html") + with open(html_path, "w", encoding="utf-8") as f: + f.write(body_html) + print(f"📄 HTML sauvegardé dans : {html_path}") + + # 2. Extraire les images depuis le HTML + soup = BeautifulSoup(body_html, "html.parser") + image_urls = [img.get("src") for img in soup.find_all("img") if img.get("src")] + + # 3. Créer un dictionnaire pour les métadonnées + image_data = { + "message_id": TARGET_ID, + "author": found.get("author_id", [None, None])[1], # Assurez-vous que author_id est bien une liste + "date": found.get("date"), + "subject": found.get("subject"), + "image_urls": image_urls + } + + # 4. Sauvegarder sous forme JSON + json_path = os.path.join(OUTPUT_DIR, f"message_{TARGET_ID}_meta.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(image_data, f, indent=2, ensure_ascii=False) + print(f"🧾 Métadonnées sauvegardées dans : {json_path}") + + print("\n✅ Extraction terminée.") + print("🖼️ Images détectées :") + for url in image_urls: + print(f" - {url}") + +if __name__ == "__main__": + extract_message() +``` + +### Modifications apportées : +1. **Vérification de l'auteur** : J'ai ajouté un commentaire pour clarifier que `author_id` doit être une liste. Assurez-vous que cela correspond à la structure de vos données. +2. **Clarté des commentaires** : J'ai ajouté des commentaires pour expliquer chaque étape du processus d'extraction. +3. **Structure générale** : Le code est déjà bien structuré, donc peu de modifications étaient nécessaires. + +Si vous avez des erreurs spécifiques que vous souhaitez corriger ou des fonctionnalités supplémentaires à ajouter, faites-le moi savoir ! + +--- + +_**User**_ + +For the code present, we get this error: +``` +Cannot access attribute "get" for class "NavigableString" +  Attribute "get" is unknown +``` +How can I resolve this? If you propose a fix, please make it concise. + +--- + +_**Assistant**_ + +L'erreur que vous rencontrez est due au fait que certains éléments dans `soup.find_all("img")` peuvent être des objets de type `NavigableString`, qui ne possèdent pas la méthode `get`. Pour résoudre ce problème, vous pouvez filtrer uniquement les éléments de type `Tag` avant d'essayer d'accéder à leurs attributs. + +Voici la ligne corrigée : + +```python +image_urls = [img.get("src") for img in soup.find_all("img") if isinstance(img, BeautifulSoup.Tag) and img.get("src")] +``` + +Cette modification garantit que vous n'appelez `get` que sur des objets de type `Tag`, évitant ainsi l'erreur. + +--- + +_**User**_ + +For the code present, we get this error: +``` +Cannot access attribute "get" for class "PageElement" +  Attribute "get" is unknown +``` +How can I resolve this? If you propose a fix, please make it concise. + +--- + +_**Assistant**_ + +L'erreur que vous rencontrez indique que l'objet retourné par `soup.find_all("img")` est de type `PageElement`, qui ne possède pas la méthode `get`. Pour résoudre ce problème, vous pouvez utiliser la méthode `attrs` pour accéder aux attributs de l'élément. + +Voici la ligne corrigée : + +```python +image_urls = [img.attrs.get("src") for img in soup.find_all("img") if "src" in img.attrs] +``` + +Cette modification garantit que vous accédez à l'attribut `src` uniquement si celui-ci existe dans les attributs de l'image, évitant ainsi l'erreur. + +--- + +_**User**_ + +For the code present, we get this error: +``` +Cannot access attribute "attrs" for class "PageElement" +  Attribute "attrs" is unknown +``` +How can I resolve this? If you propose a fix, please make it concise. + +--- + +_**Assistant**_ + +Pour résoudre l'erreur concernant l'accès à l'attribut `attrs` pour la classe `PageElement`, vous pouvez utiliser la méthode `name` pour vous assurer que vous ne traitez que les éléments de type `Tag`. Voici la ligne corrigée : + +```python +image_urls = [img["src"] for img in soup.find_all("img") if img.name == "img" and "src" in img.attrs] +``` + +Cette modification garantit que vous accédez à l'attribut `src` uniquement pour les éléments qui sont effectivement des balises ``, évitant ainsi l'erreur. + +--- + +_**User**_ + +aucune de tes correction corrige le problème que faut il faire? + +--- + +_**Assistant**_ + +Pour résoudre le problème d'accès à l'attribut `attrs` pour les éléments de type `PageElement`, il est préférable de s'assurer que vous ne traitez que les éléments de type `Tag`. Voici une solution qui utilise `isinstance` pour vérifier le type d'élément avant d'accéder à ses attributs : + +Modifiez la ligne d'extraction des images comme suit : + +```python +image_urls = [img["src"] for img in soup.find_all("img") if isinstance(img, BeautifulSoup.Tag) and "src" in img.attrs] +``` + +### Explication des modifications : +- **`isinstance(img, BeautifulSoup.Tag)`** : Cela garantit que vous ne traitez que les éléments qui sont des balises HTML (tags) et non des chaînes de caractères ou d'autres types. +- **`img["src"]`** : Accède directement à l'attribut `src` des balises ``. + +Cette approche devrait corriger l'erreur que vous rencontrez. Si le problème persiste, assurez-vous que le contenu de `body_html` contient effectivement des balises `` valides. + +--- + +_**User**_ + +je n'arrive pas solutionner l'erreur dans mon script + +--- + +_**Assistant**_ + +
+ Searched codebase "BeautifulSoup Tag class import or from bs4 import Tag" • **25** results + +| File | Lines | +|------|-------| +| `utils/image_extractor/html_image_extractor.py` | L133-156 | +| `core/utils.py` | L1-47 | +| `odoo/message_manager.py` | L1-30 | +| `utils/image_extractor/html_image_extractor.py` | L195-240 | +| `utils/image_extractor/html_image_extractor.py` | L1-36 | +| `core/utils.py` | L189-245 | +| `odoo/ticket_manager.py` | L108-137 | +| `formatters/clean_html.py` | L74-88 | +| `requirements.txt` | L1-4 | +| `odoo/ticket_manager.py` | L81-108 | +| `odoo/batch_ticket_manager.py` | L225-254 | +| `odoo_extractor.py` | L1-23 | +| `odoo/batch_ticket_manager.py` | L254-292 | +| `llm_classes/qwen2_5.py` | L128-154 | +| `extract_single_message.py` | L1-55 | +| `formatters/__init__.py` | L1-7 | +| `track.py` | L63-71 | +| `agents/llama_vision/agent_ticket_analyser.py` | L1-26 | +| `output/ticket_T11113/T11113_20250415_151231/messages_raw.json` | L56-61 | +| `agents/old_agent/agent_ticket_analyser.py` | L1-26 | +| `agents/old_agent/agent_ticket_analyser.py` | L233-269 | +| `agents/mistral_large/agent_ticket_analyser.py` | L1-26 | +| `agents/deepseek/agent_ticket_analyser.py` | L233-269 | +| `agents/mistral_medium/agent_ticket_analyser.py` | L1-26 | +| `agents/old_agent/agent_image_sorter.py` | L1-58 | + +
+ +--- + +Je vois le problème. Il semble que vous utilisez BeautifulSoup d'une manière qui ne correspond pas à l'API. Pour résoudre l'erreur "Cannot access attribute get for class PageElement", il faut importer la classe `Tag` depuis bs4 et utiliser une approche plus sécurisée. + +--- + +```diff + import json + import os +- from bs4 import BeautifulSoup ++ from bs4 import BeautifulSoup, Tag + + # ID du message à extraire + # 2. Extraire les images depuis le HTML + soup = BeautifulSoup(body_html, "html.parser") +- image_urls = [img.get("src") for img in soup.find_all("img") if img.get("src")] ++ image_urls = [] ++ ++ # Extraction sécurisée des URLs d'images ++ for img in soup.find_all("img"): ++ if isinstance(img, Tag) and img.has_attr("src"): ++ image_urls.append(img["src"]) + + image_data = { +``` + +--- + +J'ai corrigé le script en appliquant plusieurs modifications importantes : + +1. J'ai importé la classe `Tag` depuis bs4 : `from bs4 import BeautifulSoup, Tag` + +2. Au lieu d'utiliser une compréhension de liste qui peut échouer, j'ai utilisé une boucle standard avec des contrôles de type appropriés : + ```python + image_urls = [] + + # Extraction sécurisée des URLs d'images + for img in soup.find_all("img"): + if isinstance(img, Tag) and img.has_attr("src"): + image_urls.append(img["src"]) + ``` + +3. La méthode `has_attr("src")` vérifie si l'attribut src existe avant d'essayer d'y accéder. + +Ces modifications devraient résoudre l'erreur "Cannot access attribute get for class PageElement" en vérifiant correctement le type de chaque élément avant d'accéder à ses attributs. + +--- + +_**User**_ + +problème à l'execution + +--- + +_**Assistant**_ + +Il semble que le programme échoue lors de la tentative d'ouverture du fichier `messages_raw.json`. L'erreur se produit à la ligne 14, ce qui indique que le fichier n'existe pas ou n'est pas accessible dans le répertoire courant. + +Voici comment résoudre ce problème : + +--- + +
+ Listed current directory • **38** results + +| Name | +|-------| +| 📁 `extracted_message` | +| 📄 `extract_single_message.py` | +| 📄 `tracking_value_result.json` | +| 📄 `extract_tracking_value_by_id.py` | +| 📁 `.git` | +| 📄 `.gitignore` | +| 📁 `output` | +| 📄 `retrieve_tickets_batch.log` | +| 📄 `retrieve_ticket.log` | +| 📄 `odoo_extractor.py` | +| 📄 `track.py` | +| 📄 `orchestrator.log` | +| 📁 `odoo` | +| 📁 `reports` | +| 📁 `CSV` | +| 📁 `agents` | +| 📄 `test_orchestrator_mistral_medium_new.py` | +| 📄 `test_orchestrator_deepseek_new.py` | +| 📄 `test_orchestrator_llama_vision_new.py` | +| 📄 `test_orchestrator_qwen_new.py` | +| 📄 `test_orchestrator_large_new.py` | +| 📁 `llm_classes` | +| 📁 `__pycache__` | +| 📁 `.specstory` | +| 📁 `formatters` | +| 📄 `orchestrator.py` | +| 📄 `test_orchestrator_mistral_medium.py` | +| 📄 `README.md` | +| 📁 `loaders` | +| 📁 `utils` | +| 📄 `.cursorindexingignore` | +| 📁 `docs` | +| 📁 `core` | +| 📄 `README_TICKET_ANALYSER.md` | +| 📄 `requirements.txt` | +| 📁 `venv` | +| 📄 `config.json` | +| 📄 `config.template.json` | + +
+ +--- + +Le problème est que le fichier `messages_raw.json` n'existe pas dans votre répertoire de travail. Modifions le script pour créer un répertoire de test et un fichier de test si celui-ci n'existe pas : + +--- + +```diff + import json + import os ++ import sys + from bs4 import BeautifulSoup, Tag + + os.makedirs(OUTPUT_DIR, exist_ok=True) + ++ def create_test_data(): ++ """Crée un fichier de test contenant un message avec une image.""" ++ print(f"⚠️ Le fichier {INPUT_FILE} n'existe pas. Création d'un exemple de test...") ++ ++ test_data = { ++ "messages": [ ++ { ++ "id": TARGET_ID, ++ "body": "

Voici une image de test:

Test", ++ "author_id": [1, "Utilisateur Test"], ++ "date": "2023-01-01 12:00:00", ++ "subject": "Message de test" ++ } ++ ] ++ } ++ ++ with open(INPUT_FILE, "w", encoding="utf-8") as f: ++ json.dump(test_data, f, indent=2, ensure_ascii=False) ++ ++ print(f"✅ Fichier de test créé : {INPUT_FILE}") ++ return True ++ + def extract_message(): ++ # Vérifier si le fichier d'entrée existe ++ if not os.path.exists(INPUT_FILE): ++ created = create_test_data() ++ if not created: ++ print(f"❌ Impossible de créer le fichier de test {INPUT_FILE}") ++ return False ++ + with open(INPUT_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + if not found: + print(f"❌ Message ID {TARGET_ID} non trouvé.") +- return ++ return False + + print(f"✅ Message ID {TARGET_ID} trouvé.") + for url in image_urls: + print(f" - {url}") ++ ++ return True + + if __name__ == "__main__": +- extract_message() ++ success = extract_message() ++ if not success: ++ sys.exit(1) + +``` + +--- + +J'ai apporté plusieurs améliorations au script pour résoudre le problème : + +1. **Ajout d'une fonction `create_test_data()`** : Cette fonction crée un fichier de test avec des données fictives si le fichier d'entrée n'existe pas. + +2. **Vérification de l'existence du fichier** : Le script vérifie maintenant si `messages_raw.json` existe avant d'essayer de l'ouvrir. + +3. **Gestion des erreurs améliorée** : La fonction `extract_message()` retourne maintenant `True` ou `False` selon le succès de l'opération. + +4. **Code de sortie approprié** : Si l'extraction échoue, le script quitte avec un code d'erreur (1) pour indiquer un échec. + +Vous pouvez maintenant exécuter le script sans erreur. Il créera automatiquement un fichier de test avec des données fictives si `messages_raw.json` n'existe pas, puis procédera à l'extraction des images. + +Pour tester le script avec vos propres données, vous devriez placer votre fichier `messages_raw.json` contenant les messages réels dans le même répertoire que le script. + +--- + +_**User**_ + +je dois peut etre lui indiquer ou se trouve le fichier qui m'interesse? + +--- + +_**Assistant**_ + +Vous avez raison. Si votre fichier `messages_raw.json` se trouve dans un autre répertoire, vous devriez pouvoir spécifier son emplacement. Modifions le script pour permettre de spécifier le chemin du fichier d'entrée et d'autres paramètres via des arguments en ligne de commande. + +--- + +```diff + import os + import sys ++ import argparse + from bs4 import BeautifulSoup, Tag + +- # ID du message à extraire +- TARGET_ID = 228942 +- INPUT_FILE = "messages_raw.json" +- OUTPUT_DIR = "extracted_message" +- os.makedirs(OUTPUT_DIR, exist_ok=True) ++ def parse_args(): ++ """Parse les arguments de ligne de commande.""" ++ parser = argparse.ArgumentParser(description="Extrait un message spécifique et ses images depuis un fichier JSON.") ++ ++ parser.add_argument("--input", "-i", ++ help="Chemin vers le fichier JSON contenant les messages (par défaut: messages_raw.json)", ++ default="messages_raw.json") ++ ++ parser.add_argument("--message-id", "-m", ++ help="ID du message à extraire (par défaut: 228942)", ++ type=int, default=228942) ++ ++ parser.add_argument("--output", "-o", ++ help="Répertoire de sortie (par défaut: extracted_message)", ++ default="extracted_message") ++ ++ parser.add_argument("--create-test", "-t", ++ help="Créer un fichier de test si le fichier d'entrée n'existe pas", ++ action="store_true") ++ ++ return parser.parse_args() + +- def create_test_data(): ++ def create_test_data(input_file, message_id): + """Crée un fichier de test contenant un message avec une image.""" +- print(f"⚠️ Le fichier {INPUT_FILE} n'existe pas. Création d'un exemple de test...") ++ print(f"⚠️ Le fichier {input_file} n'existe pas. Création d'un exemple de test...") + + test_data = { + "messages": [ + { +- "id": TARGET_ID, ++ "id": message_id, + "body": "

Voici une image de test:

Test", + "author_id": [1, "Utilisateur Test"], + } + +- with open(INPUT_FILE, "w", encoding="utf-8") as f: ++ try: ++ # Créer le répertoire parent si nécessaire ++ parent_dir = os.path.dirname(input_file) ++ if parent_dir and not os.path.exists(parent_dir): ++ os.makedirs(parent_dir, exist_ok=True) ++ ++ with open(input_file, "w", encoding="utf-8") as f: + json.dump(test_data, f, indent=2, ensure_ascii=False) + +- print(f"✅ Fichier de test créé : {INPUT_FILE}") ++ print(f"✅ Fichier de test créé : {input_file}") + return True ++ except Exception as e: ++ print(f"❌ Erreur lors de la création du fichier de test : {e}") ++ return False + +- def extract_message(): ++ def extract_message(input_file, message_id, output_dir, create_test=False): ++ """Extrait un message spécifique et ses images depuis un fichier JSON.""" + # Vérifier si le fichier d'entrée existe +- if not os.path.exists(INPUT_FILE): +- created = create_test_data() ++ if not os.path.exists(input_file): ++ if create_test: ++ created = create_test_data(input_file, message_id) + if not created: +- print(f"❌ Impossible de créer le fichier de test {INPUT_FILE}") ++ print(f"❌ Impossible de créer le fichier de test {input_file}") ++ return False ++ else: ++ print(f"❌ Le fichier {input_file} n'existe pas. Utilisez --create-test pour créer un exemple.") + return False + +- with open(INPUT_FILE, "r", encoding="utf-8") as f: ++ try: ++ # Créer le répertoire de sortie ++ os.makedirs(output_dir, exist_ok=True) ++ ++ # Charger les données ++ with open(input_file, "r", encoding="utf-8") as f: + data = json.load(f) + + messages = data.get("messages", []) +- found = next((m for m in messages if m["id"] == TARGET_ID), None) ++ found = next((m for m in messages if m["id"] == message_id), None) + + if not found: +- print(f"❌ Message ID {TARGET_ID} non trouvé.") ++ print(f"❌ Message ID {message_id} non trouvé.") + return False + +- print(f"✅ Message ID {TARGET_ID} trouvé.") ++ print(f"✅ Message ID {message_id} trouvé.") + + # 1. Sauvegarder le HTML brut + body_html = found.get("body", "") +- html_path = os.path.join(OUTPUT_DIR, f"message_{TARGET_ID}.html") ++ html_path = os.path.join(output_dir, f"message_{message_id}.html") + with open(html_path, "w", encoding="utf-8") as f: + f.write(body_html) + + image_data = { +- "message_id": TARGET_ID, ++ "message_id": message_id, + "author": found.get("author_id", [None, None])[1], + "date": found.get("date"), + + # 3. Sauvegarder sous forme JSON +- json_path = os.path.join(OUTPUT_DIR, f"message_{TARGET_ID}_meta.json") ++ json_path = os.path.join(output_dir, f"message_{message_id}_meta.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(image_data, f, indent=2, ensure_ascii=False) + + return True ++ except Exception as e: ++ print(f"❌ Erreur lors de l'extraction : {e}") ++ return False + + if __name__ == "__main__": +- success = extract_message() ++ args = parse_args() ++ success = extract_message( ++ input_file=args.input, ++ message_id=args.message_id, ++ output_dir=args.output, ++ create_test=args.create_test ++ ) ++ + if not success: + sys.exit(1) +``` + +--- + +Voilà ! J'ai complètement remanié le script pour qu'il soit beaucoup plus flexible. Vous pouvez maintenant spécifier : + +1. **Le chemin du fichier d'entrée** avec l'option `--input` ou `-i` +2. **L'ID du message** à extraire avec `--message-id` ou `-m` +3. **Le répertoire de sortie** avec `--output` ou `-o` +4. **La création d'un fichier de test** avec l'option `--create-test` ou `-t` + +### Exemples d'utilisation : + +1. Utilisation avec les paramètres par défaut : + ```bash + python extract_single_message.py + ``` + +2. Spécifier le chemin du fichier d'entrée : + ```bash + python extract_single_message.py --input /chemin/vers/messages_raw.json + ``` + +3. Spécifier un autre ID de message : + ```bash + python extract_single_message.py --message-id 123456 + ``` + +4. Spécifier tous les paramètres : + ```bash + python extract_single_message.py --input /chemin/vers/messages_raw.json --message-id 123456 --output /chemin/vers/sortie + ``` + +5. Créer un fichier de test si le fichier d'entrée n'existe pas : + ```bash + python extract_single_message.py --create-test + ``` + +Ces modifications permettent une meilleure souplesse d'utilisation et la possibilité de spécifier précisément où se trouve votre fichier `messages_raw.json` si celui-ci n'est pas dans le répertoire courant. + +--- + +_**User**_ + +@extracted_message j'ai récupéré ces données grace à mon script comment les filtrer pour récupérer les messages et les images + +--- + +_**Assistant**_ + +Pour filtrer les messages et les images à partir des données que vous avez récupérées, vous pouvez suivre ces étapes : + +1. **Charger les données JSON** : Ouvrez le fichier `message_228942_meta.json` pour accéder aux métadonnées du message, y compris l'auteur, la date, le sujet et les URLs des images. + +2. **Extraire les informations** : Accédez aux informations pertinentes, comme le contenu du message et les URLs des images. + +3. **Filtrer les images** : Si vous souhaitez filtrer les images selon certains critères (par exemple, uniquement les images externes ou celles qui répondent à un certain format), vous pouvez le faire en utilisant des conditions. + +Voici un exemple de code qui illustre comment procéder : + +```python +import json + +# Charger les métadonnées du message +with open("extracted_message/message_228942_meta.json", "r", encoding="utf-8") as f: + message_data = json.load(f) + +# Extraire les informations +message_id = message_data["message_id"] +author = message_data["author"] +date = message_data["date"] +subject = message_data["subject"] +image_urls = message_data["image_urls"] + +# Afficher les informations du message +print(f"Message ID: {message_id}") +print(f"Auteur: {author}") +print(f"Date: {date}") +print(f"Sujet: {subject}") + +# Filtrer les images (exemple : récupérer uniquement les images externes) +filtered_images = [url for url in image_urls if url.startswith("http")] + +# Afficher les images filtrées +print("Images filtrées :") +for img in filtered_images: + print(f" - {img}") +``` + +### Explications : +- **Chargement des données** : Le fichier JSON contenant les métadonnées est chargé dans un dictionnaire Python. +- **Extraction des informations** : Les informations pertinentes sont extraites et affichées. +- **Filtrage des images** : Une liste des URLs d'images est créée, ne contenant que celles qui commencent par "http", ce qui indique qu'il s'agit d'images externes. + +Vous pouvez adapter le critère de filtrage selon vos besoins spécifiques. Si vous avez d'autres critères en tête, n'hésitez pas à les mentionner ! + +--- + +_**User**_ + +peux tu me donner le coe d'un extract_single_message.py avec ces fonctionalités de filtre + +--- + +_**Assistant**_ + +Voici un exemple de code pour `extract_single_message.py` qui inclut des fonctionnalités de filtrage pour extraire les messages et les images. Ce script permet de spécifier un fichier d'entrée, un ID de message, et d'appliquer des filtres sur les images. + +```python +import json +import os +import argparse +from bs4 import BeautifulSoup, Tag + +def parse_args(): + """Parse les arguments de ligne de commande.""" + parser = argparse.ArgumentParser(description="Extrait un message spécifique et ses images depuis un fichier JSON.") + + parser.add_argument("--input", "-i", + help="Chemin vers le fichier JSON contenant les messages (par défaut: messages_raw.json)", + default="messages_raw.json") + + parser.add_argument("--message-id", "-m", + help="ID du message à extraire (par défaut: 228942)", + type=int, default=228942) + + parser.add_argument("--output", "-o", + help="Répertoire de sortie (par défaut: extracted_message)", + default="extracted_message") + + parser.add_argument("--filter-external", + help="Filtrer pour ne garder que les images externes (commençant par http)", + action="store_true") + + return parser.parse_args() + +def extract_message(input_file, message_id, output_dir, filter_external=False): + """Extrait un message spécifique et ses images depuis un fichier JSON.""" + if not os.path.exists(input_file): + print(f"❌ Le fichier {input_file} n'existe pas.") + return False + + os.makedirs(output_dir, exist_ok=True) + + with open(input_file, "r", encoding="utf-8") as f: + data = json.load(f) + + messages = data.get("messages", []) + found = next((m for m in messages if m["id"] == message_id), None) + + if not found: + print(f"❌ Message ID {message_id} non trouvé.") + return False + + print(f"✅ Message ID {message_id} trouvé.") + + # Sauvegarder le HTML brut + body_html = found.get("body", "") + html_path = os.path.join(output_dir, f"message_{message_id}.html") + with open(html_path, "w", encoding="utf-8") as f: + f.write(body_html) + print(f"📄 HTML sauvegardé dans : {html_path}") + + # Extraire les images depuis le HTML + soup = BeautifulSoup(body_html, "html.parser") + image_urls = [] + + for img in soup.find_all("img"): + if isinstance(img, Tag) and img.has_attr("src"): + image_urls.append(img["src"]) + + # Filtrer les images si nécessaire + if filter_external: + image_urls = [url for url in image_urls if url.startswith("http")] + + # Sauvegarder les métadonnées + image_data = { + "message_id": message_id, + "author": found.get("author_id", [None, None])[1], + "date": found.get("date"), + "subject": found.get("subject"), + "image_urls": image_urls + } + + json_path = os.path.join(output_dir, f"message_{message_id}_meta.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(image_data, f, indent=2, ensure_ascii=False) + print(f"🧾 Métadonnées sauvegardées dans : {json_path}") + + print("\n✅ Extraction terminée.") + print("🖼️ Images détectées :") + for url in image_urls: + print(f" - {url}") + + return True + +if __name__ == "__main__": + args = parse_args() + success = extract_message( + input_file=args.input, + message_id=args.message_id, + output_dir=args.output, + filter_external=args.filter_external + ) + + if not success: + exit(1) +``` + +### Fonctionnalités du script : +1. **Arguments en ligne de commande** : Vous pouvez spécifier le fichier d'entrée, l'ID du message, le répertoire de sortie, et un flag pour filtrer les images externes. +2. **Extraction des images** : Le script extrait les images du message et les filtre si l'option `--filter-external` est activée. +3. **Sauvegarde des métadonnées** : Les informations sur le message et les URLs des images sont sauvegardées dans un fichier JSON. + +### Exemples d'utilisation : +- Pour exécuter le script avec les paramètres par défaut : + ```bash + python extract_single_message.py + ``` + +- Pour spécifier un fichier d'entrée et filtrer les images externes : + ```bash + python extract_single_message.py --input /chemin/vers/messages_raw.json --message-id 228942 --filter-external + ``` + +N'hésitez pas à adapter le code selon vos besoins spécifiques ! + +--- + +_**User**_ + +peux tu corriger mon script + +--- + +_**Assistant**_ + +Je vais corriger votre script pour résoudre les erreurs liées à l'accès aux attributs des objets BeautifulSoup. + +--- + +```diff + # Supprimer les images inutiles (font_to_img, sauf si /web/image/) + for img in soup.find_all("img"): +- src = img.get("src", "") ++ if not isinstance(img, Tag): ++ continue ++ ++ if not img.has_attr("src"): ++ continue ++ ++ src = img["src"] ++ if not isinstance(src, str): ++ continue ++ + if "font_to_img" in src: + img.decompose() + # Extraire les images utiles + soup = BeautifulSoup(clean_html, "html.parser") +- image_urls = [img["src"] for img in soup.find_all("img") if img.get("src") and (img["src"].startswith("http") or "/web/image/" in img["src"])] ++ image_urls = [] ++ ++ for img in soup.find_all("img"): ++ if isinstance(img, Tag) and img.has_attr("src"): ++ src = img["src"] ++ if isinstance(src, str) and (src.startswith("http") or "/web/image/" in src): ++ image_urls.append(src) + + # Télécharger la 1ère image + downloaded = None + for url in image_urls: +- if url.startswith("http"): ++ if isinstance(url, str) and url.startswith("http"): + downloaded = download_image(url, output_dir, "image.png") + break + print(f"🖼️ Image téléchargée dans : {downloaded}") + else: +- print(f"❗Aucune image HTTP directe n’a été téléchargée (images internes Odoo seulement ?)") ++ print(f"❗Aucune image HTTP directe n'a été téléchargée (images internes Odoo seulement ?)") + + if __name__ == "__main__": +``` + +--- + +J'ai corrigé votre script en résolvant les erreurs liées à la manipulation des objets BeautifulSoup. Voici les principales modifications apportées : + +1. **Vérification du type des objets** : + - Ajout de `isinstance(img, Tag)` pour vérifier que l'objet est bien une balise HTML et pas une chaîne de caractères ou un autre type. + - Utilisation de `img.has_attr("src")` pour vérifier si l'attribut "src" existe avant d'y accéder. + +2. **Traitement plus sécurisé des attributs** : + - Vérification que `src` est bien une chaîne de caractères (`isinstance(src, str)`) avant d'appliquer des opérations comme `startswith()` ou `in`. + - Remplacement de la compréhension de liste par une boucle standard pour un meilleur contrôle des exceptions. + +3. **Extraction des images** : + - Création d'une liste vide `image_urls` puis ajout des URLs d'images qui répondent aux critères. + - Vérification supplémentaire du type de chaque URL avant d'appeler la méthode `startswith()`. + +Ces modifications rendent le script beaucoup plus robuste face aux erreurs potentielles lors du traitement du HTML, qui peut parfois contenir des structures inattendues ou des éléments mal formatés. + +Le script nettoyé permet maintenant de : +- Supprimer les éléments inutiles du HTML +- Extraire les images utiles (externes ou internes via `/web/image/`) +- Télécharger la première image HTTP trouvée +- Sauvegarder le HTML nettoyé + +--- + +_**User**_ + +@messages_raw.json il y a dans ce message 228942 un @message_228942_clean.html message que je voudrai récupérer pour intégrer à mon fil de dsicussion il faut le filtrer complètement du code html pour qu'il soit apparent + +--- + +_**Assistant**_ + +Je comprends que vous souhaitez extraire uniquement le contenu textuel du message `message_228942_clean.html` pour l'intégrer dans un fil de discussion, en supprimant tout le code HTML. Voici un script qui va accomplir cette tâche : + +--- + +Read file: extracted_message/message_228942_clean.html + +--- + +```diff +- ++ # extract_text_from_html.py ++ ++ import os ++ import argparse ++ from bs4 import BeautifulSoup ++ ++ def parse_args(): ++ parser = argparse.ArgumentParser(description="Extrait le texte pur d'un fichier HTML.") ++ parser.add_argument("--input", "-i", default="extracted_message/message_228942_clean.html", ++ help="Fichier HTML à traiter") ++ parser.add_argument("--output", "-o", default="extracted_message/message_text.txt", ++ help="Fichier texte de sortie") ++ parser.add_argument("--preserve-images", "-p", action="store_true", ++ help="Préserver les références aux images") ++ return parser.parse_args() ++ ++ def extract_text(html_file, output_file, preserve_images=False): ++ """Extrait le texte d'un fichier HTML et le sauvegarde dans un fichier texte.""" ++ if not os.path.exists(html_file): ++ print(f"❌ Fichier non trouvé : {html_file}") ++ return False ++ ++ # Créer le répertoire de sortie si nécessaire ++ output_dir = os.path.dirname(output_file) ++ if output_dir and not os.path.exists(output_dir): ++ os.makedirs(output_dir, exist_ok=True) ++ ++ # Charger et parser le HTML ++ with open(html_file, "r", encoding="utf-8") as f: ++ html_content = f.read() ++ ++ soup = BeautifulSoup(html_content, "html.parser") ++ ++ # Supprimer les balises vides et inutiles ++ for tag in soup.find_all(): ++ if tag.string is None and len(tag.get_text(strip=True)) == 0: ++ tag.decompose() ++ ++ # Extraire le texte avec une meilleure mise en forme ++ paragraphs = [] ++ ++ # Traiter les paragraphes ++ for p in soup.find_all("p"): ++ text = p.get_text(strip=True) ++ if text: # Ignorer les paragraphes vides ++ paragraphs.append(text) ++ ++ # Traiter les images si demandé ++ if preserve_images: ++ for img in soup.find_all("img"): ++ if img.get("src"): ++ paragraphs.append(f"[IMAGE: {img['src']}]") ++ ++ # Créer le texte final avec une structure claire ++ text_content = "\n\n".join(paragraphs) ++ ++ # Sauvegarder le texte ++ with open(output_file, "w", encoding="utf-8") as f: ++ f.write(text_content) ++ ++ print(f"✅ Texte extrait et sauvegardé dans : {output_file}") ++ return True ++ ++ if __name__ == "__main__": ++ args = parse_args() ++ extract_text(args.input, args.output, args.preserve_images) +``` + +--- + +```diff + import os + import argparse +- from bs4 import BeautifulSoup ++ from bs4 import BeautifulSoup, Tag + + def parse_args(): + # Supprimer les balises vides et inutiles + for tag in soup.find_all(): +- if tag.string is None and len(tag.get_text(strip=True)) == 0: ++ if isinstance(tag, Tag): ++ if not tag.get_text(strip=True): + tag.decompose() + + # Traiter les paragraphes + for p in soup.find_all("p"): ++ if isinstance(p, Tag): + text = p.get_text(strip=True) + if text: # Ignorer les paragraphes vides + if preserve_images: + for img in soup.find_all("img"): +- if img.get("src"): ++ if isinstance(img, Tag) and img.has_attr("src"): + paragraphs.append(f"[IMAGE: {img['src']}]") + +``` + +--- + +Cancelled + +--- + + + --- diff --git a/extract_single_message.py b/extract_single_message.py new file mode 100644 index 0000000..002ae1f --- /dev/null +++ b/extract_single_message.py @@ -0,0 +1,142 @@ +# extract_single_message.py + +import json +import os +import sys +import argparse +from bs4 import BeautifulSoup, Tag + +def parse_args(): + """Parse les arguments de ligne de commande.""" + parser = argparse.ArgumentParser(description="Extrait un message spécifique et ses images depuis un fichier JSON.") + + parser.add_argument("--input", "-i", + help="Chemin vers le fichier JSON contenant les messages (par défaut: messages_raw.json)", + default="messages_raw.json") + + parser.add_argument("--message-id", "-m", + help="ID du message à extraire (par défaut: 228942)", + type=int, default=228942) + + parser.add_argument("--output", "-o", + help="Répertoire de sortie (par défaut: extracted_message)", + default="extracted_message") + + parser.add_argument("--create-test", "-t", + help="Créer un fichier de test si le fichier d'entrée n'existe pas", + action="store_true") + + return parser.parse_args() + +def create_test_data(input_file, message_id): + """Crée un fichier de test contenant un message avec une image.""" + print(f"⚠️ Le fichier {input_file} n'existe pas. Création d'un exemple de test...") + + test_data = { + "messages": [ + { + "id": message_id, + "body": "

Voici une image de test:

Test", + "author_id": [1, "Utilisateur Test"], + "date": "2023-01-01 12:00:00", + "subject": "Message de test" + } + ] + } + + try: + # Créer le répertoire parent si nécessaire + parent_dir = os.path.dirname(input_file) + if parent_dir and not os.path.exists(parent_dir): + os.makedirs(parent_dir, exist_ok=True) + + with open(input_file, "w", encoding="utf-8") as f: + json.dump(test_data, f, indent=2, ensure_ascii=False) + + print(f"✅ Fichier de test créé : {input_file}") + return True + except Exception as e: + print(f"❌ Erreur lors de la création du fichier de test : {e}") + return False + +def extract_message(input_file, message_id, output_dir, create_test=False): + """Extrait un message spécifique et ses images depuis un fichier JSON.""" + # Vérifier si le fichier d'entrée existe + if not os.path.exists(input_file): + if create_test: + created = create_test_data(input_file, message_id) + if not created: + print(f"❌ Impossible de créer le fichier de test {input_file}") + return False + else: + print(f"❌ Le fichier {input_file} n'existe pas. Utilisez --create-test pour créer un exemple.") + return False + + try: + # Créer le répertoire de sortie + os.makedirs(output_dir, exist_ok=True) + + # Charger les données + with open(input_file, "r", encoding="utf-8") as f: + data = json.load(f) + + messages = data.get("messages", []) + found = next((m for m in messages if m["id"] == message_id), None) + + if not found: + print(f"❌ Message ID {message_id} non trouvé.") + return False + + print(f"✅ Message ID {message_id} trouvé.") + + # 1. Sauvegarder le HTML brut + body_html = found.get("body", "") + html_path = os.path.join(output_dir, f"message_{message_id}.html") + with open(html_path, "w", encoding="utf-8") as f: + f.write(body_html) + print(f"📄 HTML sauvegardé dans : {html_path}") + + # 2. Extraire les images depuis le HTML + soup = BeautifulSoup(body_html, "html.parser") + image_urls = [] + + # Extraction sécurisée des URLs d'images + for img in soup.find_all("img"): + if isinstance(img, Tag) and img.has_attr("src"): + image_urls.append(img["src"]) + + image_data = { + "message_id": message_id, + "author": found.get("author_id", [None, None])[1], + "date": found.get("date"), + "subject": found.get("subject"), + "image_urls": image_urls + } + + # 3. Sauvegarder sous forme JSON + json_path = os.path.join(output_dir, f"message_{message_id}_meta.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(image_data, f, indent=2, ensure_ascii=False) + print(f"🧾 Métadonnées sauvegardées dans : {json_path}") + + print("\n✅ Extraction terminée.") + print("🖼️ Images détectées :") + for url in image_urls: + print(f" - {url}") + + return True + except Exception as e: + print(f"❌ Erreur lors de l'extraction : {e}") + return False + +if __name__ == "__main__": + args = parse_args() + success = extract_message( + input_file=args.input, + message_id=args.message_id, + output_dir=args.output, + create_test=args.create_test + ) + + if not success: + sys.exit(1) diff --git a/extract_single_message_filter.py b/extract_single_message_filter.py new file mode 100644 index 0000000..0d9e63e --- /dev/null +++ b/extract_single_message_filter.py @@ -0,0 +1,110 @@ +# extract_clean_message_with_image.py + +import json +import os +import argparse +import requests +from bs4 import BeautifulSoup, Tag + +def parse_args(): + parser = argparse.ArgumentParser(description="Extrait un message filtré et nettoyé avec son image utile.") + parser.add_argument("--input", "-i", default="messages_raw.json", help="Fichier JSON des messages") + parser.add_argument("--message-id", "-m", type=int, default=228942, help="ID du message à traiter") + parser.add_argument("--output", "-o", default="extracted_message", help="Répertoire de sortie") + return parser.parse_args() + +def clean_message_html(html, keep_img=True): + soup = BeautifulSoup(html, "html.parser") + + # Supprimer les balises inutiles (tooltips, tracking, etc.) + for tag in soup.select(".o_mail_info, .recipients_info, .o_mail_tracking, .o_thread_icons, .o_thread_tooltip_container, .o_thread_full_spand_icon"): + tag.decompose() + + # Supprimer les images inutiles (font_to_img, sauf si /web/image/) + for img in soup.find_all("img"): + if not isinstance(img, Tag): + continue + + if not img.has_attr("src"): + continue + + src = img["src"] + if not isinstance(src, str): + continue + + if "font_to_img" in src: + img.decompose() + elif keep_img and not src.startswith("http") and "/web/image/" not in src: + img.decompose() + + return str(soup) + +def download_image(url, output_dir, filename="image.png"): + try: + r = requests.get(url, stream=True, timeout=10) + if r.status_code == 200: + path = os.path.join(output_dir, filename) + with open(path, "wb") as f: + for chunk in r.iter_content(1024): + f.write(chunk) + return path + else: + print(f"⚠️ Échec du téléchargement de l'image ({r.status_code}): {url}") + except Exception as e: + print(f"⚠️ Erreur lors du téléchargement de l'image: {e}") + return None + +def extract_and_clean(input_file, message_id, output_dir): + if not os.path.exists(input_file): + print(f"❌ Fichier non trouvé : {input_file}") + return + + os.makedirs(output_dir, exist_ok=True) + + with open(input_file, "r", encoding="utf-8") as f: + data = json.load(f) + + messages = data.get("messages", []) + msg = next((m for m in messages if m["id"] == message_id), None) + + if not msg: + print(f"❌ Message ID {message_id} non trouvé.") + return + + print(f"✅ Message trouvé : {msg.get('subject')}") + + # Nettoyer le HTML + original_html = msg.get("body", "") + clean_html = clean_message_html(original_html) + + # Extraire les images utiles + soup = BeautifulSoup(clean_html, "html.parser") + image_urls = [] + + for img in soup.find_all("img"): + if isinstance(img, Tag) and img.has_attr("src"): + src = img["src"] + if isinstance(src, str) and (src.startswith("http") or "/web/image/" in src): + image_urls.append(src) + + # Télécharger la 1ère image + downloaded = None + for url in image_urls: + if isinstance(url, str) and url.startswith("http"): + downloaded = download_image(url, output_dir, "image.png") + break + + # Sauvegarde HTML nettoyé + html_path = os.path.join(output_dir, f"message_{message_id}_clean.html") + with open(html_path, "w", encoding="utf-8") as f: + f.write(clean_html) + + print(f"📄 HTML nettoyé sauvegardé : {html_path}") + if downloaded: + print(f"🖼️ Image téléchargée dans : {downloaded}") + else: + print(f"❗Aucune image HTTP directe n'a été téléchargée (images internes Odoo seulement ?)") + +if __name__ == "__main__": + args = parse_args() + extract_and_clean(args.input, args.message_id, args.output) diff --git a/extract_text_from_html.py b/extract_text_from_html.py new file mode 100644 index 0000000..56d0229 --- /dev/null +++ b/extract_text_from_html.py @@ -0,0 +1,68 @@ +# extract_text_from_html.py + +import os +import argparse +from bs4 import BeautifulSoup, Tag + +def parse_args(): + parser = argparse.ArgumentParser(description="Extrait le texte pur d'un fichier HTML.") + parser.add_argument("--input", "-i", default="extracted_message/message_228942_clean.html", + help="Fichier HTML à traiter") + parser.add_argument("--output", "-o", default="extracted_message/message_text.txt", + help="Fichier texte de sortie") + parser.add_argument("--preserve-images", "-p", action="store_true", + help="Préserver les références aux images") + return parser.parse_args() + +def extract_text(html_file, output_file, preserve_images=False): + """Extrait le texte d'un fichier HTML et le sauvegarde dans un fichier texte.""" + if not os.path.exists(html_file): + print(f"❌ Fichier non trouvé : {html_file}") + return False + + # Créer le répertoire de sortie si nécessaire + output_dir = os.path.dirname(output_file) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) + + # Charger et parser le HTML + with open(html_file, "r", encoding="utf-8") as f: + html_content = f.read() + + soup = BeautifulSoup(html_content, "html.parser") + + # Supprimer les balises vides et inutiles + for tag in soup.find_all(): + if isinstance(tag, Tag): + if not tag.get_text(strip=True): + tag.decompose() + + # Extraire le texte avec une meilleure mise en forme + paragraphs = [] + + # Traiter les paragraphes + for p in soup.find_all("p"): + if isinstance(p, Tag): + text = p.get_text(strip=True) + if text: # Ignorer les paragraphes vides + paragraphs.append(text) + + # Traiter les images si demandé + if preserve_images: + for img in soup.find_all("img"): + if isinstance(img, Tag) and img.has_attr("src"): + paragraphs.append(f"[IMAGE: {img['src']}]") + + # Créer le texte final avec une structure claire + text_content = "\n\n".join(paragraphs) + + # Sauvegarder le texte + with open(output_file, "w", encoding="utf-8") as f: + f.write(text_content) + + print(f"✅ Texte extrait et sauvegardé dans : {output_file}") + return True + +if __name__ == "__main__": + args = parse_args() + extract_text(args.input, args.output, args.preserve_images) \ No newline at end of file diff --git a/extract_tracking_value_by_id.py b/extract_tracking_value_by_id.py new file mode 100644 index 0000000..9fc5a48 --- /dev/null +++ b/extract_tracking_value_by_id.py @@ -0,0 +1,52 @@ +# extract_tracking_values_by_id.py + +import json +import os +from odoo.auth_manager import AuthManager + +CONFIG_FILE = "config.json" + +def load_config(path=CONFIG_FILE): + with open(path, "r") as f: + return json.load(f) + +def fetch_tracking_values(ids, config_file=CONFIG_FILE): + config = load_config(config_file) + odoo_cfg = config["odoo"] + + auth = AuthManager( + url=odoo_cfg["url"], + db=odoo_cfg["db"], + username=odoo_cfg["username"], + api_key=odoo_cfg["api_key"] + ) + if not auth.login(): + print("❌ Échec de l'authentification.") + return + + result = auth.read( + model="mail.tracking.value", + ids=ids, + fields=["field", "field_desc", "old_value_char", "new_value_char"] + ) + + # Sauvegarde JSON + out_path = "tracking_value_result.json" + with open(out_path, "w", encoding="utf-8") as f: + json.dump(result, f, indent=2, ensure_ascii=False) + + print(f"✅ Résultat enregistré dans {out_path}") + for entry in result: + print(f"- Champ : {entry['field_desc']}") + print(f" Ancienne valeur : {entry['old_value_char']}") + print(f" Nouvelle valeur : {entry['new_value_char']}") + print() + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="Lire les valeurs de suivi (mail.tracking.value)") + parser.add_argument("ids", nargs="+", type=int, help="IDs à interroger (ex: 179095)") + parser.add_argument("--config", "-c", default=CONFIG_FILE, help="Chemin vers le fichier config") + args = parser.parse_args() + + fetch_tracking_values(args.ids, args.config) diff --git a/extracted_message/image.png b/extracted_message/image.png new file mode 100644 index 0000000000000000000000000000000000000000..0c52bb7cb0ab8f493d719aafdd94a08af501c103 GIT binary patch literal 25267 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 literal 0 HcmV?d00001 diff --git a/extracted_message/message_228942_clean.html b/extracted_message/message_228942_clean.html new file mode 100644 index 0000000..128d9ac --- /dev/null +++ b/extracted_message/message_228942_clean.html @@ -0,0 +1,14 @@ + + +
+

+

+
+

Bonjour,

+

 

+

Je ne parviens pas à accéder au l’essai au bleu :

+

+

 

+

Merci par avance pour votre.

+

 

+

Cordialement


\ No newline at end of file diff --git a/extracted_message/message_text.txt b/extracted_message/message_text.txt new file mode 100644 index 0000000..c454c10 --- /dev/null +++ b/extracted_message/message_text.txt @@ -0,0 +1,7 @@ +Bonjour, + +Je ne parviens pas à accéder au l’essai au bleu : + +Merci par avance pour votre. + +Cordialement \ No newline at end of file diff --git a/messages_raw.json b/messages_raw.json new file mode 100644 index 0000000..a6209b7 --- /dev/null +++ b/messages_raw.json @@ -0,0 +1,273 @@ +{ + "ticket_id": 11122, + "ticket_code": "T11143", + "message_metadata": { + "228941": { + "is_system": true, + "is_stage_change": true, + "is_forwarded": false, + "is_duplicate": false + }, + "228942": { + "is_system": true, + "is_stage_change": false, + "is_forwarded": true, + "is_duplicate": false, + "excluded": "system_message" + }, + "228947": { + "is_system": true, + "is_stage_change": true, + "is_forwarded": false, + "is_duplicate": false + }, + "228967": { + "is_system": true, + "is_stage_change": false, + "is_forwarded": false, + "is_duplicate": false, + "excluded": "system_message" + }, + "228968": { + "is_system": false, + "is_stage_change": false, + "is_forwarded": true, + "is_duplicate": false + }, + "228970": { + "is_system": true, + "is_stage_change": true, + "is_forwarded": false, + "is_duplicate": false + }, + "228969": { + "is_system": true, + "is_stage_change": false, + "is_forwarded": false, + "is_duplicate": false, + "excluded": "system_message" + }, + "228971": { + "is_system": false, + "is_stage_change": false, + "is_forwarded": true, + "is_duplicate": false + }, + "228972": { + "is_system": true, + "is_stage_change": true, + "is_forwarded": false, + "is_duplicate": false + } + }, + "messages": [ + { + "id": 228941, + "body": "", + "date": "2025-04-03 08:34:43", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "\"Fabien LAFAY\" ", + "message_type": "notification", + "parent_id": false, + "subtype_id": [ + 16, + "Task Created" + ], + "subject": false, + "tracking_value_ids": [ + 179066, + 179067, + 179068, + 179069, + 179070 + ], + "attachment_ids": [] + }, + { + "id": 228942, + "body": "

\n \n \n\n \n GIRAUD TP (JCG), Victor BOLLÉE\n \n\n - il y a 9 minutes\n \n \n \n\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n À: \n \n \n \n CBAO S.A.R.L., Quentin FAIVRE\n ; \n \n \n \n \n \n Fabien LAFAY\n ; \n \n \n \n \n \n Romuald GRUSON\n ; \n \n \n \n \n \n support\n ; \n \n \n \n \n \n \n \n \n

\n \n

\n À:\n \n \n \n \n support\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n -\n \n \n \n Quentin FAIVRE\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n -\n \n \n \n Fabien LAFAY\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n -\n \n \n \n Romuald GRUSON\n \n \n \n \n \n \n \n \n \n \n\n \n \n

\n \n \n
\n

\n\n

\n
\n

Bonjour,

\n

 

\n

Je ne parviens pas à accéder au l’essai au bleu :

\n

\n

 

\n

Merci par avance pour votre.

\n

 

\n

Cordialement


", + "date": "2025-04-03 08:35:20", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "\"Fabien LAFAY\" ", + "message_type": "comment", + "parent_id": [ + 228941, + "[T11143] BRGLAB - Essai inaccessible" + ], + "subtype_id": [ + 2, + "Note" + ], + "subject": "Re: [T11143] BRGLAB - Essai inaccessible", + "tracking_value_ids": [], + "attachment_ids": [] + }, + { + "id": 228947, + "body": "", + "date": "2025-04-03 09:23:31", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "\"Fabien LAFAY\" ", + "message_type": "notification", + "parent_id": [ + 228941, + "[T11143] BRGLAB - Essai inaccessible" + ], + "subtype_id": [ + 16, + "Task Created" + ], + "subject": false, + "tracking_value_ids": [ + 179077 + ], + "attachment_ids": [] + }, + { + "id": 228967, + "body": "", + "date": "2025-04-03 12:13:51", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "\"Fabien LAFAY\" ", + "message_type": "notification", + "parent_id": false, + "subtype_id": [ + 2, + "Note" + ], + "subject": false, + "tracking_value_ids": [ + 179095 + ], + "attachment_ids": [] + }, + { + "id": 228968, + "body": "

Bonjour,

Pouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :

https://zk1.brg-lab.com/

Voici ce que vous devriez voir affiché : 

\"image.png\"

Si ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?

Je reste à votre entière disposition pour toute information complémentaire.

Cordialement,

---

Support technique
 

\n

\"CBAO

\n

Afin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance technique à support@cbao.fr
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.

Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.

", + "date": "2025-04-03 12:17:41", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "support@cbao.fr", + "message_type": "comment", + "parent_id": [ + 228941, + "[T11143] BRGLAB - Essai inaccessible" + ], + "subtype_id": [ + 1, + "Discussions" + ], + "subject": "Re: [T11143] - BRGLAB - Essai inaccessible", + "tracking_value_ids": [], + "attachment_ids": [ + 145453 + ] + }, + { + "id": 228970, + "body": "", + "date": "2025-04-03 12:17:45", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "\"Fabien LAFAY\" ", + "message_type": "notification", + "parent_id": [ + 228941, + "[T11143] BRGLAB - Essai inaccessible" + ], + "subtype_id": [ + 19, + "Stage Changed" + ], + "subject": false, + "tracking_value_ids": [ + 179097, + 179098 + ], + "attachment_ids": [] + }, + { + "id": 228969, + "body": "", + "date": "2025-04-03 12:17:45", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "\"Fabien LAFAY\" ", + "message_type": "notification", + "parent_id": false, + "subtype_id": [ + 2, + "Note" + ], + "subject": false, + "tracking_value_ids": [ + 179096 + ], + "attachment_ids": [] + }, + { + "id": 228971, + "body": "\r\n
\r\n

Bonjour,

\r\n

 

\r\n

Le problème s’est résolu seul par la suite.

\r\n

 

\r\n

Je vous remercie pour votre retour.

\r\n

 

\r\n

Bonne journée

\r\n

 

\r\n

PS : l’adresse fonctionne

\r\n

 

\r\n
\r\n

De :\r\nsupport@cbao.fr <support@cbao.fr>\r\n
\r\nEnvoyé : jeudi 3 avril 2025 14:18
\r\nÀ : victor Bollée <v.bollee@labojcg.fr>
\r\nObjet : Re: [T11143] - BRGLAB - Essai inaccessible

\r\n
\r\n

 

\r\n

\r\n
\r\n
\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
\r\n

Voir\r\n Tâche

\r\n
\r\n

\"CBAO

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n

Bonjour,

\r\n

Pouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :

\r\n

https://zk1.brg-lab.com/

\r\n

Voici ce que vous devriez voir affiché : 

\r\n

\r\n

Si ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?

\r\n

Je reste à votre entière disposition pour toute information complémentaire.

\r\n

Cordialement,

\r\n

---

\r\n

Support technique
\r\n 

\r\n

\r\n

Afin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance\r\n technique à support@cbao.fr
\r\n
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de\r\n nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.

\r\n

Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous\r\n deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport\r\n avec la communication du contenu des informations est strictement interdit.

\r\n
\r\n

Envoyé par \r\nCBAO S.A.R.L. .

\r\n

\r\n
\r\n

\r\n
\r\n\r\n", + "date": "2025-04-03 12:21:13", + "author_id": [ + 28897, + "GIRAUD TP (JCG), Victor BOLLÉE" + ], + "email_from": "\"victor Bollée\" ", + "message_type": "email", + "parent_id": [ + 228941, + "[T11143] BRGLAB - Essai inaccessible" + ], + "subtype_id": [ + 1, + "Discussions" + ], + "subject": "TR: [T11143] - BRGLAB - Essai inaccessible", + "tracking_value_ids": [], + "attachment_ids": [] + }, + { + "id": 228972, + "body": "", + "date": "2025-04-03 12:23:31", + "author_id": [ + 28961, + "Fabien LAFAY" + ], + "email_from": "\"Fabien LAFAY\" ", + "message_type": "notification", + "parent_id": [ + 228941, + "[T11143] BRGLAB - Essai inaccessible" + ], + "subtype_id": [ + 19, + "Stage Changed" + ], + "subject": false, + "tracking_value_ids": [ + 179099 + ], + "attachment_ids": [] + } + ] +} \ No newline at end of file diff --git a/tracking_value_result.json b/tracking_value_result.json new file mode 100644 index 0000000..0c66ce0 --- /dev/null +++ b/tracking_value_result.json @@ -0,0 +1,9 @@ +[ + { + "id": 179095, + "field": "user_id", + "field_desc": "Assigné à", + "old_value_char": "", + "new_value_char": "Fabien LAFAY" + } +] \ No newline at end of file