1504-17:40

This commit is contained in:
Ladebeze66 2025-04-15 17:40:48 +02:00
parent 43da046555
commit 215e7d95ed
24 changed files with 6238 additions and 396 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,230 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour extraire les images manquantes des messages HTML dans un ticket Odoo
et les ajouter aux pièces jointes.
"""
import os
import json
import re
import requests
import sys
import shutil
import argparse
from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple
def load_json_file(file_path: str) -> Any:
"""
Charge un fichier JSON.
Args:
file_path: Chemin du fichier JSON à charger
Returns:
Contenu du fichier JSON
"""
try:
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
else:
return None
except Exception as e:
print(f"Erreur lors du chargement du fichier {file_path}: {e}")
return None
def save_json_file(file_path: str, data: Any) -> bool:
"""
Sauvegarde des données dans un fichier JSON.
Args:
file_path: Chemin du fichier JSON à sauvegarder
data: Données à sauvegarder
Returns:
True si la sauvegarde a réussi, False sinon
"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Erreur lors de la sauvegarde du fichier {file_path}: {e}")
return False
def download_image(url: str, save_path: str) -> bool:
"""
Télécharge une image depuis une URL.
Args:
url: URL de l'image à télécharger
save_path: Chemin sauvegarder l'image
Returns:
True si le téléchargement a réussi, False sinon
"""
try:
# Créer le répertoire parent si nécessaire
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# Télécharger l'image
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(save_path, 'wb') as f:
response.raw.decode_content = True
shutil.copyfileobj(response.raw, f)
print(f"Image téléchargée et sauvegardée dans: {save_path}")
return True
else:
print(f"Erreur lors du téléchargement de l'image: {response.status_code}")
return False
except Exception as e:
print(f"Erreur lors du téléchargement de l'image: {e}")
return False
def extract_missing_attachments(ticket_dir: str) -> None:
"""
Extrait les images manquantes d'un ticket et les ajoute aux pièces jointes.
Args:
ticket_dir: Répertoire du ticket
"""
# Vérifier que le répertoire existe
if not os.path.exists(ticket_dir):
print(f"Répertoire introuvable: {ticket_dir}")
return
# Chemins des fichiers
messages_file = os.path.join(ticket_dir, "all_messages.json")
attachments_file = os.path.join(ticket_dir, "attachments_info.json")
attachments_dir = os.path.join(ticket_dir, "attachments")
# Vérifier que les fichiers nécessaires existent
if not os.path.exists(messages_file):
print(f"Fichier de messages introuvable: {messages_file}")
return
# Charger les messages
messages_data = load_json_file(messages_file)
if not messages_data:
print("Impossible de charger les messages")
return
# Charger les pièces jointes existantes
attachments_info = load_json_file(attachments_file) or []
# Vérifier si le dossier des attachements existe, sinon le créer
if not os.path.exists(attachments_dir):
os.makedirs(attachments_dir)
# Extraire les IDs des pièces jointes existantes
existing_attachment_ids = set()
for attachment in attachments_info:
if "id" in attachment:
existing_attachment_ids.add(attachment["id"])
# Parcourir les messages pour trouver les images manquantes
messages = messages_data.get("messages", [])
newly_added_attachments = []
for message in messages:
message_id = message.get("id")
# Traiter uniquement les messages avec body_original contenant des images
body_original = message.get("body_original", "")
if not body_original:
continue
# Chercher toutes les références d'images
image_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', body_original)
for match in image_matches:
img_url = match.group(1)
# Extraire l'ID de l'image
img_id = None
access_token = None
# Pattern 1: /web/image/ID?access_token=...
id_match = re.search(r"/web/image/(\d+)", img_url)
if id_match:
img_id = int(id_match.group(1))
# Extraire le token d'accès
token_match = re.search(r"access_token=([^&]+)", img_url)
if token_match:
access_token = token_match.group(1)
# Vérifier si l'image existe déjà dans les pièces jointes
if img_id and img_id not in existing_attachment_ids:
print(f"Image manquante trouvée: ID {img_id} dans le message {message_id}")
# Déterminer le nom du fichier
file_name = f"image_{img_id}.png" # Nom par défaut
# Chercher un attribut alt ou title qui pourrait contenir le nom
alt_match = re.search(r'<img[^>]+alt=["\']([^"\']+)["\'][^>]*>', match.group(0))
if alt_match and alt_match.group(1).strip():
alt_text = alt_match.group(1).strip()
# Nettoyer et limiter la longueur du nom
alt_text = re.sub(r'[^\w\s.-]', '', alt_text)
alt_text = alt_text[:50] # Limiter la longueur
if alt_text:
file_name = f"{alt_text}_{img_id}.png"
# Chemin de destination pour l'image
img_save_path = os.path.join(attachments_dir, file_name)
# Télécharger l'image
if download_image(img_url, img_save_path):
# Taille du fichier
file_size = os.path.getsize(img_save_path)
# Ajouter l'information de la pièce jointe
attachment_info = {
"id": img_id,
"name": file_name,
"mimetype": "image/png", # Type par défaut
"file_size": file_size,
"create_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"creator_name": message.get("author_details", {}).get("name", "Inconnu"),
"download_status": "success",
"local_path": img_save_path,
"error": "",
"was_missing": True,
"message_id": message_id,
"access_token": access_token
}
attachments_info.append(attachment_info)
existing_attachment_ids.add(img_id)
newly_added_attachments.append(attachment_info)
# Sauvegarder immédiatement pour éviter la perte en cas d'erreur
save_json_file(attachments_file, attachments_info)
# Afficher un résumé
if newly_added_attachments:
print(f"Ajouté {len(newly_added_attachments)} nouvelles pièces jointes:")
for att in newly_added_attachments:
print(f" - {att['name']} (ID: {att['id']}, Taille: {att['file_size']} octets)")
else:
print("Aucune nouvelle pièce jointe ajoutée.")
def main():
"""
Point d'entrée principal du script.
"""
parser = argparse.ArgumentParser(description="Extrait les images manquantes des messages HTML dans un ticket Odoo.")
parser.add_argument("ticket_dir", help="Répertoire du ticket contenant les messages et pièces jointes")
args = parser.parse_args()
extract_missing_attachments(args.ticket_dir)
if __name__ == "__main__":
main()

View File

@ -8,230 +8,387 @@ Version simplifiée et robuste: ignore les lignes problématiques.
import re
from datetime import datetime
import html
from bs4 import BeautifulSoup, Tag
from bs4.element import NavigableString, PageElement
from typing import Union, List, Tuple, Optional, Any, Dict, cast
import logging
import html2text
def clean_html(html_content, is_description=False, strategy="standard", preserve_links=False, preserve_images=False):
def clean_html(html_content: Union[str, None], is_forwarded: bool = False):
if html_content is None or not isinstance(html_content, str) or html_content.strip() == "":
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
try:
# Sauvegarder les références d'images avant de nettoyer le HTML
image_references: List[Tuple[str, str]] = []
img_pattern = re.compile(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>')
for match in img_pattern.finditer(html_content):
full_tag = match.group(0)
img_url = match.group(1)
# Vérifier si c'est une image Odoo
if "/web/image/" in img_url:
image_references.append((full_tag, img_url))
# Nettoyer le HTML
soup = BeautifulSoup(html_content, 'html.parser')
# Supprimer les éléments script, style et head
for elem in soup.find_all(['script', 'style', 'head']):
elem.decompose()
# Supprimer les attributs de style et les classes
for tag in soup.recursiveChildGenerator():
if isinstance(tag, Tag):
if tag.attrs and 'style' in tag.attrs:
del tag.attrs['style']
if tag.attrs and 'class' in tag.attrs:
del tag.attrs['class']
# Conserver uniquement les balises HTML essentielles
allowed_tags = ['p', 'br', 'b', 'i', 'u', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code', 'pre', 'hr', 'div', 'span',
'table', 'tr', 'td', 'th', 'thead', 'tbody']
# Supprimer les balises HTML inutiles mais conserver leur contenu
for tag in soup.find_all():
if isinstance(tag, Tag) and tag.name.lower() not in allowed_tags:
tag.unwrap()
# Amélioration: vérifier si nous avons du contenu significatif
text_content = soup.get_text().strip()
if not text_content and not image_references:
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
# Obtenir le HTML nettoyé
clean_content = str(soup)
# Vérifier si le contenu a été vidé par le nettoyage
if clean_content.strip() == "" or clean_content.strip() == "<html><body></body></html>":
# Si nous avons des références d'images mais pas de texte
if image_references:
image_descriptions = []
for _, img_url in image_references:
img_id = None
id_match = re.search(r"/web/image/(\d+)", img_url)
if id_match:
img_id = id_match.group(1)
image_descriptions.append(f"[Image {img_id}]")
# Retourner une description des images trouvées
if image_descriptions:
return "Message contenant uniquement des images: " + ", ".join(image_descriptions)
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
return clean_content
except Exception as e:
logging.error(f"Erreur lors du nettoyage HTML: {str(e)}")
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
def extract_from_complex_html(html_content, preserve_images=False):
"""
Nettoie le contenu HTML pour le Markdown en identifiant et ignorant les parties problématiques.
Extrait le contenu d'un HTML complexe en utilisant BeautifulSoup.
Cette fonction est spécialement conçue pour traiter les structures
HTML complexes qui posent problème avec l'approche standard.
Args:
html_content (str): Contenu HTML à nettoyer
is_description (bool): Indique si le contenu est une description de ticket
strategy (str): Stratégie de nettoyage à utiliser ("standard", "strict", ou "raw")
preserve_links (bool): Indique s'il faut préserver les liens
preserve_images (bool): Indique s'il faut préserver les images
html_content (str): Contenu HTML à traiter
preserve_images (bool): Conserver les images
Returns:
str: Texte nettoyé
str: Contenu extrait et nettoyé
"""
if not html_content:
return "*Contenu vide*"
# 0. PRÉVENIR LES DOUBLONS - Détecter et supprimer les messages dupliqués
# Cette étape permet d'éliminer les messages qui apparaissent en double
# D'abord, nettoyer le HTML pour comparer les sections de texte réel
cleaned_for_comparison = pre_clean_html(html_content)
# Détection des doublons basée sur les premières lignes
# Si le même début apparaît deux fois, ne garder que jusqu'à la première occurrence
first_paragraph = ""
for line in cleaned_for_comparison.split('\n'):
if len(line.strip()) > 10: # Ignorer les lignes vides ou trop courtes
first_paragraph = line.strip()
break
try:
soup = BeautifulSoup(html_content, 'html.parser')
# Extraction d'images - Étape 1: Rechercher toutes les images avant toute modification
image_markdowns = []
if preserve_images or True: # Toujours préserver les images
# Chercher directement les balises img dans le HTML brut
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
for match in img_matches:
src = match.group(1)
if '/web/image/' in src or 'access_token' in src or src.startswith('http'):
image_markdowns.append(f"![Image]({src})")
if first_paragraph and first_paragraph in cleaned_for_comparison[len(first_paragraph):]:
# Le premier paragraphe apparaît deux fois - couper au début de la deuxième occurrence
pos = cleaned_for_comparison.find(first_paragraph, len(first_paragraph))
if pos > 0:
# Utiliser cette position pour couper le contenu original
html_content = html_content[:pos].strip()
# Diviser le contenu en sections potentielles (souvent séparées par des lignes vides doubles)
sections = re.split(r'\n\s*\n\s*\n', html_content)
# Si le contenu a plusieurs sections, ne garder que la première section significative
if len(sections) > 1:
# Rechercher la première section qui contient du texte significatif (non des en-têtes/métadonnées)
significant_content = ""
for section in sections:
# Ignorer les sections très courtes ou qui ressemblent à des en-têtes
if len(section.strip()) > 50 and not re.search(r'^(?:Subject|Date|From|To|Cc|Objet|De|À|Copie à):', section, re.IGNORECASE):
significant_content = section
# Méthode alternative avec BeautifulSoup
images = soup.find_all('img')
for img in images:
try:
if isinstance(img, Tag) and img.has_attr('src'):
src = img['src']
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
alt = img['alt'] if img.has_attr('alt') else 'Image'
image_markdowns.append(f"![{alt}]({src})")
except Exception:
continue
# 1. Rechercher d'abord le contenu du message principal
# Essayer différents sélecteurs en ordre de priorité
content_selectors = [
'.o_thread_message_content', # Contenu principal
'.o_mail_body', # Corps du message
'.o_mail_note_content', # Contenu d'une note
'.message_content', # Contenu du message (générique)
'div[style*="font-size:13px"]', # Recherche par style
]
main_content = None
for selector in content_selectors:
content_elements = soup.select(selector)
if content_elements:
main_content = content_elements[0]
break
# Si on a trouvé une section significative, l'utiliser comme contenu
if significant_content:
html_content = significant_content
# 1. CAS SPÉCIAUX - Traités en premier avec leurs propres règles
# 1.1. Traitement spécifique pour les descriptions
if is_description:
# Suppression complète des balises HTML de base
content = pre_clean_html(html_content)
content = re.sub(r'\n\s*\n', '\n\n', content)
return content.strip()
# 1.2. Traitement des messages transférés avec un pattern spécifique
if "\\-------- Message transféré --------" in html_content or "-------- Courriel original --------" in html_content:
# Essayer d'extraire le contenu principal du message transféré
match = re.search(r'(?:De|From|Copie à|Cc)\s*:.*?\n\s*\n(.*?)(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
html_content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
else:
# Essayer une autre approche si la première échoue
match = re.search(r'Bonjour.*?(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
html_content, re.DOTALL)
if match:
return match.group(0).strip()
# 1.3. Traitement des notifications d'appel
if "Notification d'appel" in html_content:
match = re.search(r'(?:Sujet d\'appel:[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n)[^\n]*\n[^\n]*([^|]+)', html_content, re.DOTALL)
if match:
message_content = match.group(1).strip()
# Construire un message formaté avec les informations essentielles
infos = {}
date_match = re.search(r'Date:.*?\|(.*?)(?:\n|$)', html_content)
appelant_match = re.search(r'\*\*Appel de:\*\*.*?\|(.*?)(?:\n|$)', html_content)
telephone_match = re.search(r'Téléphone principal:.*?\|(.*?)(?:\n|$)', html_content)
mobile_match = re.search(r'Mobile:.*?\|(.*?)(?:\n|$)', html_content)
sujet_match = re.search(r'Sujet d\'appel:.*?\|(.*?)(?:\n|$)', html_content)
# Si aucun contenu principal n'est trouvé, prendre le premier paragraphe non vide
if not main_content:
paragraphs = soup.find_all('p')
for p in paragraphs:
try:
if isinstance(p, Tag) and p.text.strip():
classes = p['class'] if p.has_attr('class') else []
if not any(cls in str(classes) for cls in ['o_mail_info', 'recipient_link']):
main_content = p
break
except Exception:
continue
# Si toujours rien, prendre la première div non vide
if not main_content:
divs = soup.find_all('div')
for div in divs:
try:
if isinstance(div, Tag) and div.text.strip():
classes = div['class'] if div.has_attr('class') else []
if not any(cls in str(classes) for cls in ['o_mail_info', 'o_thread']):
main_content = div
break
except Exception:
continue
# 2. Si on a trouvé du contenu, l'extraire
if main_content:
# Extraire toutes les images si demandé
if preserve_images or True: # Toujours préserver les images
try:
if isinstance(main_content, Tag):
content_images = main_content.find_all('img')
for img in content_images:
try:
if isinstance(img, Tag) and img.has_attr('src'):
src = img['src']
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
alt = img['alt'] if img.has_attr('alt') else 'Image'
image_markdowns.append(f"![{alt}]({src})")
# Supprimer l'image pour éviter qu'elle apparaisse dans le texte
img.decompose()
except Exception:
continue
except Exception:
pass
if date_match:
infos["date"] = date_match.group(1).strip()
if appelant_match:
infos["appelant"] = appelant_match.group(1).strip()
if telephone_match:
infos["telephone"] = telephone_match.group(1).strip()
if mobile_match:
infos["mobile"] = mobile_match.group(1).strip()
if sujet_match:
infos["sujet"] = sujet_match.group(1).strip()
# Construire le message formaté
formatted_message = f"**Notification d'appel**\n\n"
if "appelant" in infos:
formatted_message += f"De: {infos['appelant']}\n"
if "date" in infos:
formatted_message += f"Date: {infos['date']}\n"
if "telephone" in infos:
formatted_message += f"Téléphone: {infos['telephone']}\n"
if "mobile" in infos:
formatted_message += f"Mobile: {infos['mobile']}\n"
if "sujet" in infos:
formatted_message += f"Sujet: {infos['sujet']}\n\n"
# Extraire le texte
try:
if isinstance(main_content, Tag):
text_content = main_content.get_text(separator='\n', strip=True)
# Nettoyer le texte
text_content = re.sub(r'\n{3,}', '\n\n', text_content)
text_content = text_content.strip()
# Recherche spécifique pour certaines phrases clés
if "Je ne parviens pas à accéder" in html_content:
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
acces_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder[^<]*</p>', html_content, re.DOTALL)
specific_content = []
if bonjour_match:
specific_content.append(pre_clean_html(bonjour_match.group(0)))
if acces_match:
specific_content.append(pre_clean_html(acces_match.group(0)))
# Extraire les contenus spécifiques du message "Je ne parviens pas..."
merci_match = re.search(r'<p[^>]*>.*?Merci par avance.*?</p>', html_content, re.DOTALL)
if merci_match:
specific_content.append(pre_clean_html(merci_match.group(0)))
cordial_match = re.search(r'<p[^>]*>.*?Cordialement.*?</p>', html_content, re.DOTALL)
if cordial_match:
specific_content.append(pre_clean_html(cordial_match.group(0)))
if specific_content:
text_content = '\n'.join(specific_content)
# Supprimer les duplications de lignes
lines = text_content.split('\n')
unique_lines = []
for line in lines:
if line not in unique_lines:
unique_lines.append(line)
text_content = '\n'.join(unique_lines)
# Ajouter les images à la fin
if image_markdowns:
# Supprimer les doublons d'images
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
text_content += "\n\n" + "\n".join(unique_images)
return text_content if text_content else "*Contenu non extractible*"
except Exception as e:
print(f"Erreur lors de l'extraction du texte: {e}")
formatted_message += f"Message: {message_content}"
# 3. Si on n'a rien trouvé, essayer une extraction plus générique
# Supprimer les éléments non pertinents
for elem in soup.select('.o_mail_info, .o_mail_tracking, .o_thread_tooltip, .o_thread_icons, .recipients_info'):
try:
elem.decompose()
except Exception:
continue
# Extraire le texte restant
try:
text = soup.get_text(separator='\n', strip=True)
text = re.sub(r'\n{3,}', '\n\n', text)
return formatted_message
# 2. NOUVELLE APPROCHE SIMPLE - Filtrer les lignes problématiques
# 2.1. D'abord nettoyer le HTML
cleaned_content = pre_clean_html(html_content)
# 2.2. Diviser en lignes et filtrer les lignes problématiques
filtered_lines = []
# Liste modifiée - moins restrictive pour les informations de contact
problematic_indicators = [
"!/web/image/", # Garder celui-ci car c'est spécifique aux images embarquées
"[CBAO - développeur de rentabilité", # Signature standard à filtrer
"Afin d'assurer une meilleure traçabilité" # Début de disclaimer standard
]
# Mémoriser l'indice de la ligne contenant "Cordialement" ou équivalent
signature_line_idx = -1
lines = cleaned_content.split('\n')
for i, line in enumerate(lines):
# Détecter la signature
if any(sig in line.lower() for sig in ["cordialement", "cdlt", "bien à vous", "salutation"]):
signature_line_idx = i
# Préserver les images si demandé
if preserve_images or True: # Toujours préserver les images
# Les images ont déjà été extraites au début de la fonction
if image_markdowns:
# Supprimer les doublons d'images
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
text += "\n\n" + "\n".join(unique_images)
# Si on a du contenu, le retourner
if text and len(text.strip()) > 5:
return text
except Exception as e:
print(f"Erreur lors de l'extraction générique: {e}")
# Vérifier si la ligne contient un indicateur problématique
is_problematic = any(indicator in line for indicator in problematic_indicators)
# Si rien n'a fonctionné mais qu'on a des images, au moins les retourner
if image_markdowns:
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
if any("Je ne parviens pas à accéder" in html_content for img in image_markdowns):
return "Bonjour,\nJe ne parviens pas à accéder au l'essai au bleu :\n\n" + "\n".join(unique_images) + "\n\nMerci par avance pour votre.\nCordialement"
else:
return "Images extraites :\n\n" + "\n".join(unique_images)
# Si la ligne est très longue (plus de 500 caractères), la considérer comme problématique
if len(line) > 500:
is_problematic = True
# Ajouter la ligne seulement si elle n'est pas problématique
if not is_problematic:
filtered_lines.append(line)
# 2.3. Si on a trouvé une signature, ne garder que 2 lignes après maximum
if signature_line_idx >= 0:
# Suppression de la limitation à 2 lignes après la signature
# Gardons toutes les lignes après la signature si ce sont des informations techniques
# Ce commentaire est laissé intentionnellement pour référence historique
pass
# filtered_lines = filtered_lines[:min(signature_line_idx + 3, len(filtered_lines))]
# 2.4. Recombiner les lignes filtrées
content = '\n'.join(filtered_lines)
# 2.5. Nettoyer les espaces et lignes vides
content = re.sub(r'\n{3,}', '\n\n', content)
content = content.strip()
# 2.6. VÉRIFICATION FINALE: S'assurer qu'il n'y a pas de duplication dans le contenu final
# Si le même paragraphe apparaît deux fois, ne garder que jusqu'à la première occurrence
lines = content.split('\n')
unique_lines = []
seen_paragraphs = set()
for line in lines:
clean_line = line.strip()
# Ne traiter que les lignes non vides et assez longues pour être significatives
if clean_line and len(clean_line) > 10:
if clean_line in seen_paragraphs:
# On a déjà vu cette ligne, c'est probablement une duplication
# Arrêter le traitement ici
break
seen_paragraphs.add(clean_line)
unique_lines.append(line)
content = '\n'.join(unique_lines)
# Résultat final
if not content or len(content.strip()) < 10:
return "*Contenu non extractible*"
return content
except Exception as e:
print(f"Erreur lors de l'extraction complexe: {e}")
# Dernière tentative : extraction directe avec regex
try:
# Extraire des images
image_markdowns = []
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
for match in img_matches:
src = match.group(1)
if '/web/image/' in src or 'access_token' in src or src.startswith('http'):
image_markdowns.append(f"![Image]({src})")
# Extraire du texte significatif
text_parts = []
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
if bonjour_match:
text_parts.append(pre_clean_html(bonjour_match.group(0)))
content_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder.*?</p>', html_content, re.DOTALL)
if content_match:
text_parts.append(pre_clean_html(content_match.group(0)))
# Combiner texte et images
if text_parts or image_markdowns:
result = ""
if text_parts:
result += "\n".join(text_parts) + "\n\n"
if image_markdowns:
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
result += "\n".join(unique_images)
return result
except Exception:
pass
return "*Contenu non extractible*"
def pre_clean_html(html_content):
"""
Effectue un nettoyage préliminaire du HTML en préservant la structure et le formatage basique.
Fonction interne pour nettoyer le HTML basique avant traitement avancé.
Args:
html_content: Contenu HTML à pré-nettoyer
Returns:
Texte avec les balises HTML basiques retirées
"""
# Remplacer les balises de paragraphe et saut de ligne par des sauts de ligne
content = re.sub(r'<br\s*/?>|<p[^>]*>|</p>|<div[^>]*>|</div>', '\n', html_content)
if not html_content:
return ""
# Remplacer les balises <br>, <p>, <div> par des sauts de ligne
content = html_content.replace('<br>', '\n').replace('<br/>', '\n').replace('<br />', '\n')
content = content.replace('</p>', '\n').replace('</div>', '\n')
# Préserver le formatage de base (gras, italique, etc.)
content = re.sub(r'<(?:b|strong)>(.*?)</(?:b|strong)>', r'**\1**', content)
content = re.sub(r'<(?:i|em)>(.*?)</(?:i|em)>', r'*\1*', content)
# Préserver les URLs des images
image_urls = []
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', content)
for match in img_matches:
if '/web/image/' in match.group(1) or match.group(1).startswith('http'):
image_urls.append(match.group(1))
# Transformer les listes
content = re.sub(r'<li>(.*?)</li>', r'- \1\n', content)
# Supprimer les balises HTML
content = re.sub(r'<[^>]*>', '', content)
# Supprimer les balises HTML avec leurs attributs mais conserver le contenu
content = re.sub(r'<[^>]+>', '', content)
# Supprimer les espaces multiples
content = re.sub(r' {2,}', ' ', content)
# Remplacer les entités HTML courantes
# Supprimer les sauts de ligne multiples
content = re.sub(r'\n{3,}', '\n\n', content)
# Décoder les entités HTML courantes
content = content.replace('&nbsp;', ' ')
content = content.replace('&lt;', '<')
content = content.replace('&gt;', '>')
content = content.replace('&amp;', '&')
content = content.replace('&quot;', '"')
# Nettoyer les espaces multiples
content = re.sub(r' {2,}', ' ', content)
# Supprimer les tabulations
content = content.replace('\t', ' ')
# Nettoyer les sauts de ligne multiples (mais pas tous, pour préserver la structure)
content = re.sub(r'\n{3,}', '\n\n', content)
# Ajouter les images préservées à la fin
if image_urls:
content += "\n\n"
for url in image_urls:
content += f"![Image]({url})\n"
return content.strip()

View File

@ -192,7 +192,7 @@ def create_markdown_from_json(json_file, output_file):
md_content.append("") # saut de ligne
if description:
cleaned_description = clean_html(description, is_description=True)
cleaned_description = clean_html(description)
if cleaned_description and cleaned_description != "*Contenu vide*":
cleaned_description = html.unescape(cleaned_description)
md_content.append(cleaned_description)
@ -256,7 +256,7 @@ def create_markdown_from_json(json_file, output_file):
if "body_original" in message and message["body_original"]:
body = message["body_original"]
# Nettoyer le corps HTML avec clean_html
cleaned_body = clean_html(body, is_description=False)
cleaned_body = clean_html(body, is_forwarded=message.get("is_forwarded", False))
else:
# Utiliser body directement (déjà en texte/markdown) sans passer par clean_html
body = message.get("body", "")

446
odoo/message_manager.bak Normal file
View File

@ -0,0 +1,446 @@
from typing import List, Dict, Any, Optional, Tuple
from .auth_manager import AuthManager
from formatters.clean_html import clean_html
from core.utils import save_json, save_text, detect_duplicate_content, normalize_filename
import os
import re
import logging
from datetime import datetime
class MessageManager:
"""
Gestionnaire de messages pour traiter les messages associés aux tickets.
"""
def __init__(self, auth: AuthManager):
"""
Initialise le gestionnaire de messages.
Args:
auth: Gestionnaire d'authentification
"""
self.auth = auth
self.model_name = "project.task"
self.cleaning_strategies = {
"simple": {"preserve_links": False, "preserve_images": False, "strategy": "strip_tags"},
"standard": {"preserve_links": True, "preserve_images": True, "strategy": "html2text"},
"advanced": {"preserve_links": True, "preserve_images": True, "strategy": "soup"},
"raw": {"preserve_links": False, "preserve_images": False, "strategy": "none"}
}
self.default_strategy = "standard"
def get_ticket_messages(self, ticket_id: int, fields: Optional[List[str]] = None) -> List[Dict[str, Any]]:
"""
Récupère tous les messages associés à un ticket.
Args:
ticket_id: ID du ticket
fields: Liste des champs à récupérer (facultatif)
Returns:
Liste des messages associés au ticket
"""
if fields is None:
fields = ["id", "body", "date", "author_id", "email_from", "message_type",
"parent_id", "subtype_id", "subject", "tracking_value_ids", "attachment_ids"]
params = {
"model": "mail.message",
"method": "search_read",
"args": [[["res_id", "=", ticket_id], ["model", "=", self.model_name]]],
"kwargs": {
"fields": fields,
"order": "date asc"
}
}
messages = self.auth._rpc_call("/web/dataset/call_kw", params)
return messages if isinstance(messages, list) else []
def is_system_message(self, message: Dict[str, Any]) -> bool:
"""
Vérifie si le message est un message système ou OdooBot.
Args:
message: Le message à vérifier
Returns:
True si c'est un message système, False sinon
"""
is_system = False
# Vérifier le nom de l'auteur
if 'author_id' in message and isinstance(message['author_id'], list) and len(message['author_id']) > 1:
author_name = message['author_id'][1].lower()
if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name or 'system' in author_name:
is_system = True
# Vérifier le type de message
if message.get('message_type') in ['notification', 'auto_comment']:
is_system = True
# Vérifier le sous-type du message
if 'subtype_id' in message and isinstance(message['subtype_id'], list) and len(message['subtype_id']) > 1:
subtype = message['subtype_id'][1].lower()
if 'notification' in subtype or 'system' in subtype or 'note' in subtype:
is_system = True
return is_system
def is_stage_change_message(self, message: Dict[str, Any]) -> bool:
"""
Vérifie si le message est un changement d'état.
Args:
message: Le message à vérifier
Returns:
True si c'est un message de changement d'état, False sinon
"""
if not isinstance(message.get('body', ''), str):
return False
body = message.get('body', '').lower()
# Patterns pour les changements d'état
stage_patterns = [
'étape changée', 'stage changed', 'modifié l\'étape',
'changed the stage', 'ticket transféré', 'ticket transferred',
'statut modifié', 'status changed', 'état du ticket'
]
# Vérifier aussi les valeurs de tracking si disponibles
if message.get('tracking_value_ids'):
try:
tracking_values = self.auth.read("mail.tracking.value", message.get('tracking_value_ids', []),
["field", "field_desc", "old_value_char", "new_value_char"])
for value in tracking_values:
if value.get("field") == "stage_id" or "stage" in value.get("field_desc", "").lower():
return True
except Exception as e:
logging.warning(f"Erreur lors de la vérification des valeurs de tracking: {e}")
return any(pattern in body for pattern in stage_patterns)
def is_forwarded_message(self, message: Dict[str, Any]) -> bool:
"""
Détecte si un message est un message transféré.
Args:
message: Le message à analyser
Returns:
True si le message est transféré, False sinon
"""
if not message.get('body'):
return False
# Indicateurs de message transféré
forwarded_indicators = [
"message transféré", "forwarded message",
"transféré de", "forwarded from",
"début du message transféré", "begin forwarded message",
"message d'origine", "original message",
"from:", "de:", "to:", "à:", "subject:", "objet:",
"envoyé:", "sent:", "date:", "cc:"
]
# Vérifier le contenu du message
body_lower = message.get('body', '').lower() if isinstance(message.get('body', ''), str) else ""
# Vérifier la présence d'indicateurs de transfert
for indicator in forwarded_indicators:
if indicator in body_lower:
return True
# Vérifier si le sujet contient des préfixes courants de transfert
subject_value = message.get('subject', '')
if not isinstance(subject_value, str):
subject_value = str(subject_value) if subject_value is not None else ""
subject_lower = subject_value.lower()
forwarded_prefixes = ["tr:", "fwd:", "fw:"]
for prefix in forwarded_prefixes:
if subject_lower.startswith(prefix):
return True
# Patterns typiques dans les messages transférés
patterns = [
r"-{3,}Original Message-{3,}",
r"_{3,}Original Message_{3,}",
r">{3,}", # Plusieurs signes > consécutifs indiquent souvent un message cité
r"Le .* a écrit :"
]
for pattern in patterns:
if re.search(pattern, body_lower):
return True
return False
def get_message_author_details(self, message: Dict[str, Any]) -> Dict[str, Any]:
"""
Récupère les détails de l'auteur d'un message.
Args:
message: Le message dont il faut récupérer l'auteur
Returns:
Dictionnaire avec les détails de l'auteur
"""
author_details = {
"name": "Inconnu",
"email": message.get('email_from', ''),
"is_system": False
}
try:
author_id_field = message.get('author_id')
if author_id_field and isinstance(author_id_field, list) and len(author_id_field) > 0:
author_id = author_id_field[0]
params = {
"model": "res.partner",
"method": "read",
"args": [[author_id]],
"kwargs": {"fields": ['name', 'email', 'phone', 'function', 'company_id']}
}
author_data = self.auth._rpc_call("/web/dataset/call_kw", params)
if author_data and isinstance(author_data, list) and len(author_data) > 0:
author_details.update(author_data[0])
# Vérifier si c'est un auteur système
if author_details.get('name'):
author_name = author_details['name'].lower()
if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name:
author_details['is_system'] = True
except Exception as e:
logging.warning(f"Erreur lors de la récupération des détails de l'auteur: {e}")
return author_details
def process_messages(self, ticket_id: int, ticket_code: str, ticket_name: str, output_dir: str,
strategy: str = "standard") -> Dict[str, Any]:
"""
Traite tous les messages d'un ticket, nettoie le contenu et génère des fichiers structurés.
Args:
ticket_id: ID du ticket
ticket_code: Code du ticket
ticket_name: Nom du ticket
output_dir: Répertoire de sortie
strategy: Stratégie de nettoyage (simple, standard, advanced, raw)
Returns:
Dictionnaire avec les chemins des fichiers créés
"""
# Validation de la stratégie
if strategy not in self.cleaning_strategies:
logging.warning(f"Stratégie de nettoyage '{strategy}' inconnue, utilisation de la stratégie par défaut '{self.default_strategy}'")
strategy = self.default_strategy
cleaning_config = self.cleaning_strategies[strategy]
# Récupérer les messages
messages = self.get_ticket_messages(ticket_id)
# Détecter les messages dupliqués
duplicate_indices = detect_duplicate_content(messages)
# Nettoyer et structurer les messages
processed_messages = []
# Créer un dictionnaire de métadonnées pour chaque message
message_metadata = {}
for index, message in enumerate(messages):
message_id = message.get('id')
# Ajouter des métadonnées au message
message_metadata[message_id] = {
"is_system": self.is_system_message(message),
"is_stage_change": self.is_stage_change_message(message),
"is_forwarded": self.is_forwarded_message(message),
"is_duplicate": index in duplicate_indices
}
# Créer une copie du message pour éviter de modifier l'original
message_copy = message.copy()
# Ajouter les métadonnées au message copié
for key, value in message_metadata[message_id].items():
message_copy[key] = value
# Nettoyer le corps du message selon la stratégie choisie
if message_copy.get('body'):
# Toujours conserver l'original
message_copy['body_original'] = message_copy.get('body', '')
# Appliquer la stratégie de nettoyage, sauf si raw
if strategy != "raw":
cleaned_body = clean_html(
message_copy.get('body', ''),
strategy=cleaning_config['strategy'],
preserve_links=cleaning_config['preserve_links'],
preserve_images=cleaning_config['preserve_images']
)
# Nettoyer davantage le code HTML qui pourrait rester
if cleaned_body:
# Supprimer les balises style et script avec leur contenu
cleaned_body = re.sub(r'<style[^>]*>.*?</style>', '', cleaned_body, flags=re.DOTALL)
cleaned_body = re.sub(r'<script[^>]*>.*?</script>', '', cleaned_body, flags=re.DOTALL)
# Supprimer les balises HTML restantes
cleaned_body = re.sub(r'<[^>]+>', '', cleaned_body)
message_copy['body'] = cleaned_body
# Récupérer les détails de l'auteur
message_copy['author_details'] = self.get_message_author_details(message_copy)
# Ne pas inclure les messages système sans intérêt
if message_copy.get('is_system') and not message_copy.get('is_stage_change'):
# Enregistrer l'exclusion dans les métadonnées
message_metadata[message_id]['excluded'] = "system_message"
continue
# Ignorer les messages dupliqués si demandé
if message_copy.get('is_duplicate'):
# Enregistrer l'exclusion dans les métadonnées
message_metadata[message_id]['excluded'] = "duplicate_content"
continue
processed_messages.append(message_copy)
# Trier les messages par date
processed_messages.sort(key=lambda x: x.get('date', ''))
# Récupérer les informations supplémentaires du ticket
try:
ticket_data = self.auth._rpc_call("/web/dataset/call_kw", {
"model": "project.task",
"method": "read",
"args": [[ticket_id]],
"kwargs": {"fields": ["project_id", "stage_id"]}
})
project_id = None
stage_id = None
project_name = None
stage_name = None
if ticket_data and isinstance(ticket_data, list) and len(ticket_data) > 0:
if "project_id" in ticket_data[0] and ticket_data[0]["project_id"]:
project_id = ticket_data[0]["project_id"][0] if isinstance(ticket_data[0]["project_id"], list) else ticket_data[0]["project_id"]
project_name = ticket_data[0]["project_id"][1] if isinstance(ticket_data[0]["project_id"], list) else None
if "stage_id" in ticket_data[0] and ticket_data[0]["stage_id"]:
stage_id = ticket_data[0]["stage_id"][0] if isinstance(ticket_data[0]["stage_id"], list) else ticket_data[0]["stage_id"]
stage_name = ticket_data[0]["stage_id"][1] if isinstance(ticket_data[0]["stage_id"], list) else None
except Exception as e:
logging.error(f"Erreur lors de la récupération des informations du ticket: {e}")
project_id = None
stage_id = None
project_name = None
stage_name = None
# Créer la structure pour le JSON
messages_with_summary = {
"ticket_summary": {
"id": ticket_id,
"code": ticket_code,
"name": ticket_name,
"project_id": project_id,
"project_name": project_name,
"stage_id": stage_id,
"stage_name": stage_name,
"date_extraction": datetime.now().isoformat()
},
"metadata": {
"message_count": {
"total": len(messages),
"processed": len(processed_messages),
"excluded": len(messages) - len(processed_messages)
},
"cleaning_strategy": strategy,
"cleaning_config": cleaning_config
},
"messages": processed_messages
}
# Sauvegarder les messages en JSON
all_messages_path = os.path.join(output_dir, "all_messages.json")
save_json(messages_with_summary, all_messages_path)
# Sauvegarder également les messages bruts
raw_messages_path = os.path.join(output_dir, "messages_raw.json")
save_json({
"ticket_id": ticket_id,
"ticket_code": ticket_code,
"message_metadata": message_metadata,
"messages": messages
}, raw_messages_path)
# Créer un fichier texte pour une lecture plus facile
messages_text_path = os.path.join(output_dir, "all_messages.txt")
try:
text_content = self._generate_messages_text(ticket_code, ticket_name, processed_messages)
save_text(text_content, messages_text_path)
except Exception as e:
logging.error(f"Erreur lors de la création du fichier texte: {e}")
return {
"all_messages_path": all_messages_path,
"raw_messages_path": raw_messages_path,
"messages_text_path": messages_text_path,
"messages_count": len(processed_messages),
"total_messages": len(messages)
}
def _generate_messages_text(self, ticket_code: str, ticket_name: str,
processed_messages: List[Dict[str, Any]]) -> str:
"""
Génère un fichier texte formaté à partir des messages traités.
Args:
ticket_code: Code du ticket
ticket_name: Nom du ticket
processed_messages: Liste des messages traités
Returns:
Contenu du fichier texte
"""
content = []
# Informations sur le ticket
content.append(f"TICKET: {ticket_code} - {ticket_name}")
content.append(f"Date d'extraction: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
content.append(f"Nombre de messages: {len(processed_messages)}")
content.append("\n" + "="*80 + "\n")
# Parcourir les messages filtrés
for msg in processed_messages:
author = msg.get('author_details', {}).get('name', msg.get('email_from', 'Inconnu'))
date = msg.get('date', '')
subject = msg.get('subject', 'Sans objet')
body = msg.get('body', '')
# Formater différemment les messages spéciaux
if msg.get('is_stage_change'):
content.append("*"*80)
content.append("*** CHANGEMENT D'ÉTAT ***")
content.append("*"*80 + "\n")
elif msg.get('is_forwarded'):
content.append("*"*80)
content.append("*** MESSAGE TRANSFÉRÉ ***")
content.append("*"*80 + "\n")
# En-tête du message
content.append(f"DATE: {date}")
content.append(f"DE: {author}")
if subject:
content.append(f"OBJET: {subject}")
content.append("")
content.append(f"{body}")
content.append("\n" + "-"*80 + "\n")
return "\n".join(content)

View File

@ -278,9 +278,7 @@ class MessageManager:
if strategy != "raw":
cleaned_body = clean_html(
message_copy.get('body', ''),
strategy=cleaning_config['strategy'],
preserve_links=cleaning_config['preserve_links'],
preserve_images=cleaning_config['preserve_images']
is_forwarded=message_copy.get('is_forwarded', False)
)
# Nettoyer davantage le code HTML qui pourrait rester
@ -296,12 +294,51 @@ class MessageManager:
# Récupérer les détails de l'auteur
message_copy['author_details'] = self.get_message_author_details(message_copy)
# Ne pas inclure les messages système sans intérêt
if message_copy.get('is_system') and not message_copy.get('is_stage_change'):
# Vérifier si le message contient des éléments importants
has_attachments = bool(message_copy.get('attachment_ids'))
has_images = False
has_meaningful_content = False
# Vérifier la présence d'images dans le HTML
if message_copy.get('body_original'):
# Rechercher les balises img dans le HTML
has_images = '<img' in message_copy.get('body_original', '').lower() or '/web/image/' in message_copy.get('body_original', '').lower()
# Rechercher des éléments d'images Odoo spécifiques
if '/web/image/' in message_copy.get('body_original', ''):
has_images = True
# Vérifier si le corps du message contient du texte significatif
body_text = message_copy.get('body', '')
if body_text and len(body_text.strip()) > 30: # Texte non vide et d'une certaine longueur
has_meaningful_content = True
# Déterminer si le message doit être conservé malgré son statut système
is_important = (
has_attachments or
has_images or
message_copy.get('is_forwarded') or
has_meaningful_content or
message_copy.get('is_stage_change')
)
# Ne pas inclure les messages système UNIQUEMENT s'ils n'ont rien d'important
if message_copy.get('is_system') and not is_important:
# Enregistrer l'exclusion dans les métadonnées
message_metadata[message_id]['excluded'] = "system_message"
continue
# Si le message est marqué comme exclu dans les métadonnées mais qu'il est transféré, le réintégrer
if message_metadata.get(message_id, {}).get('excluded') == "system_message" and message_copy.get('is_forwarded'):
# Supprimer l'exclusion des métadonnées
del message_metadata[message_id]['excluded']
# Vérifier aussi les messages qui sont déjà exclus dans les métadonnées d'entrée
# et les réintégrer s'ils sont transférés
if 'excluded' in message_metadata.get(message_id, {}) and message_copy.get('is_forwarded'):
# Supprimer l'exclusion des métadonnées
del message_metadata[message_id]['excluded']
# Ignorer les messages dupliqués si demandé
if message_copy.get('is_duplicate'):
# Enregistrer l'exclusion dans les métadonnées
@ -313,6 +350,46 @@ class MessageManager:
# Trier les messages par date
processed_messages.sort(key=lambda x: x.get('date', ''))
# Étape supplémentaire: Vérifier si des messages transférés ont été exclus et les réintégrer
processed_ids = {msg['id'] for msg in processed_messages if 'id' in msg}
for message in messages:
message_id = message.get('id')
if (message_id not in processed_ids and
message_metadata.get(message_id, {}).get('is_forwarded') and
'excluded' in message_metadata.get(message_id, {})):
# Créer une copie du message
message_copy = message.copy()
# Ajouter les métadonnées au message
for key, value in message_metadata[message_id].items():
if key != 'excluded': # Ne pas ajouter le tag d'exclusion
message_copy[key] = value
# Si le message a un corps, on applique le même traitement de nettoyage
if message_copy.get('body'):
# Toujours conserver l'original
message_copy['body_original'] = message_copy.get('body', '')
# Appliquer la stratégie de nettoyage, sauf si raw
if strategy != "raw":
cleaned_body = clean_html(
message_copy.get('body', ''),
is_forwarded=message_copy.get('is_forwarded', False)
)
# Nettoyage supplémentaire
if cleaned_body:
cleaned_body = re.sub(r'<style[^>]*>.*?</style>', '', cleaned_body, flags=re.DOTALL)
cleaned_body = re.sub(r'<script[^>]*>.*?</script>', '', cleaned_body, flags=re.DOTALL)
cleaned_body = re.sub(r'<[^>]+>', '', cleaned_body)
message_copy['body'] = cleaned_body
# Récupérer les détails de l'auteur
message_copy['author_details'] = self.get_message_author_details(message_copy)
# Supprimer l'exclusion des métadonnées
if 'excluded' in message_metadata[message_id]:
del message_metadata[message_id]['excluded']
# Ajouter le message aux messages traités
processed_messages.append(message_copy)
# Trier à nouveau les messages par date après la réintégration
processed_messages.sort(key=lambda x: x.get('date', ''))
# Récupérer les informations supplémentaires du ticket
try:
ticket_data = self.auth._rpc_call("/web/dataset/call_kw", {

View File

@ -1,33 +0,0 @@
{
"id": "11122",
"code": "T11143",
"name": "BRGLAB - Essai inaccessible",
"description": "*Aucune description fournie*",
"project_name": "Demandes",
"stage_name": "Clôturé",
"user_id": "",
"partner_id_email_from": "GIRAUD TP (JCG), Victor BOLLÉE, v.bollee@labojcg.fr",
"create_date": "03/04/2025 08:34:43",
"write_date_last_modification": "03/04/2025 12:23:31",
"date_deadline": "18/04/2025 00:00:00",
"messages": [
{
"author_id": "Fabien LAFAY",
"date": "03/04/2025 12:17:41",
"message_type": "E-mail",
"subject": "Re: [T11143] - BRGLAB - Essai inaccessible",
"id": "228968",
"content": "Bonjour,\nPouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :\nhttps://zk1.brg-lab.com/\nVoici ce que vous devriez voir affiché :\nSi ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?\nJe reste à votre entière disposition pour toute information complémentaire.\nCordialement,\n---\nSupport technique\nL'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.\n*Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci den avertir immédiatement lexpé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.*\n\n- image.png (image/png) [ID: 145453]\n\n---\n\n"
},
{
"author_id": "Victor BOLLÉE",
"date": "03/04/2025 12:21:13",
"message_type": "E-mail",
"subject": "TR: [T11143] - BRGLAB - Essai inaccessible",
"id": "228971",
"content": "Bonjour,\nLe problème sest résolu seul par la suite.\nJe vous remercie pour votre retour.\nBonne journée\nPS : ladresse fonctionne\nsupport@cbao.fr <support@cbao.fr>\nVoir\nTâche\nBonjour,\nPouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :\nhttps://zk1.brg-lab.com/\nVoici ce que vous devriez voir affiché :\nSi ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?\nJe reste à votre entière disposition pour toute information complémentaire.\nCordialement,\n---\ntechnique à **support@cbao.fr**\nL'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\nnos 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.\nConfidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous\ndeviez recevoir cet e-mail par erreur, merci den avertir immédiatement lexpé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\navec la communication du contenu des informations est strictement interdit.\nEnvoyé par\nCBAO S.A.R.L. .\n\n---\n"
}
],
"date_d'extraction": "15/04/2025 15:12:33",
"répertoire": "output/ticket_T11143/T11143_20250415_151222"
}

View File

@ -1,86 +0,0 @@
# Ticket T11143: BRGLAB - Essai inaccessible
## Informations du ticket
- **id**: 11122
- **code**: T11143
- **name**: BRGLAB - Essai inaccessible
- **project_name**: Demandes
- **stage_name**: Clôturé
- **user_id**:
- **partner_id/email_from**: GIRAUD TP (JCG), Victor BOLLÉE, v.bollee@labojcg.fr
- **create_date**: 03/04/2025 08:34:43
- **write_date/last modification**: 03/04/2025 12:23:31
- **date_deadline**: 18/04/2025 00:00:00
- **description**:
*Aucune description fournie*
## Messages
### Message 1
**author_id**: Fabien LAFAY
**date**: 03/04/2025 12:17:41
**message_type**: E-mail
**subject**: Re: [T11143] - BRGLAB - Essai inaccessible
**id**: 228968
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é :
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
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 den avertir immédiatement lexpé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.*
**attachment_ids**:
- image.png (image/png) [ID: 145453]
---
### Message 2
**author_id**: Victor BOLLÉE
**date**: 03/04/2025 12:21:13
**message_type**: E-mail
**subject**: TR: [T11143] - BRGLAB - Essai inaccessible
**id**: 228971
Bonjour,
Le problème sest résolu seul par la suite.
Je vous remercie pour votre retour.
Bonne journée
PS : ladresse fonctionne
**De :**
support@cbao.fr <support@cbao.fr>
**Envoyé :** jeudi 3 avril 2025 14:18
**À :** victor Bollée <v.bollee@labojcg.fr>
**Objet :** Re: [T11143] - BRGLAB - Essai inaccessible
Voir
Tâche
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é :
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**
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 den avertir immédiatement lexpé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.
Envoyé par
CBAO S.A.R.L. .
---
## Informations sur l'extraction
- **Date d'extraction**: 15/04/2025 15:12:33
- **Répertoire**: output/ticket_T11143/T11143_20250415_151222

View File

@ -1,20 +0,0 @@
[
{
"id": 145453,
"name": "image.png",
"mimetype": "image/png",
"file_size": 76543,
"create_date": "2025-04-03 12:17:41",
"create_uid": [
22,
"Fabien LAFAY"
],
"description": false,
"res_name": "[T11143] BRGLAB - Essai inaccessible",
"creator_name": "Fabien LAFAY",
"creator_id": 22,
"download_status": "success",
"local_path": "output/ticket_T11143/T11143_20250415_151222/attachments/image.png",
"error": ""
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
TICKET: T11143 - BRGLAB - Essai inaccessible
Date d'extraction: 2025-04-15 15:12:23
Nombre de messages: 6
Date d'extraction: 2025-04-15 17:18:34
Nombre de messages: 7
================================================================================
@ -13,6 +13,18 @@ DE: Fabien LAFAY
--------------------------------------------------------------------------------
********************************************************************************
*** MESSAGE TRANSFÉRÉ ***
********************************************************************************
DATE: 2025-04-03 08:35:20
DE: Fabien LAFAY
OBJET: Re: [T11143] BRGLAB - Essai inaccessible
*Contenu non extractible*
--------------------------------------------------------------------------------
********************************************************************************
@ -35,29 +47,21 @@ DE: Fabien LAFAY
OBJET: Re: [T11143] - BRGLAB - Essai inaccessible
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é : 
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
 
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 den avertir immédiatement lexpé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.*
Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci den avertir immédiatement lexpé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.
--------------------------------------------------------------------------------
@ -83,54 +87,43 @@ OBJET: TR: [T11143] - BRGLAB - Essai inaccessible
Bonjour,
 
Le problème sest résolu seul par la suite.
 
Je vous remercie pour votre retour.
 
Bonne journée
 
PS : ladresse fonctionne
 
**De :**
De :
support@cbao.fr
**Envoyé :** jeudi 3 avril 2025 14:18
Envoyé : jeudi 3 avril 2025 14:18
**À :** victor Bollée
À : victor Bollée
**Objet :** Re: [T11143] - BRGLAB - Essai inaccessible
Objet : Re: [T11143] - BRGLAB - Essai inaccessible
@ -140,15 +133,12 @@ support@cbao.fr
Voir
Tâche
@ -161,9 +151,7 @@ Voir
@ -176,58 +164,46 @@ Voir
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é : 
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**
Support technique
 
technique à **support@cbao.fr**
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 den avertir immédiatement lexpé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.
@ -235,7 +211,6 @@ Confidentialité : Ce courriel contient des informations confidentielles exclusi
Envoyé par
CBAO S.A.R.L. .

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,34 @@
[
{
"id": 145453,
"name": "image.png",
"mimetype": "image/png",
"file_size": 76543,
"create_date": "2025-04-03 12:17:41",
"create_uid": [
22,
"Fabien LAFAY"
],
"description": false,
"res_name": "[T11143] BRGLAB - Essai inaccessible",
"creator_name": "Fabien LAFAY",
"creator_id": 22,
"download_status": "success",
"local_path": "output/ticket_T11143/T11143_20250415_171834/attachments/image.png",
"error": ""
},
{
"id": 145435,
"name": "image_145435.png",
"mimetype": "image/png",
"file_size": 25267,
"create_date": "2025-04-15 17:37:30",
"creator_name": "Fabien LAFAY",
"download_status": "success",
"local_path": "output/ticket_T11143/T11143_20250415_171834/attachments/image_145435.png",
"error": "",
"was_missing": true,
"message_id": 228942,
"access_token": "608ac9e7-3627-4a13-a8ec-06ff5046ebf3"
}
]

View File

@ -0,0 +1,13 @@
{
"timestamp": "20250415_171834",
"ticket_code": "T11143",
"output_directory": "output/ticket_T11143/T11143_20250415_171834",
"message_count": 7,
"attachment_count": 1,
"files_created": [
"ticket_info.json",
"ticket_summary.json",
"all_messages.json",
"structure.json"
]
}

View File

@ -12,8 +12,7 @@
"is_system": true,
"is_stage_change": false,
"is_forwarded": true,
"is_duplicate": false,
"excluded": "system_message"
"is_duplicate": false
},
"228947": {
"is_system": true,

View File

@ -1,9 +1,9 @@
{
"date_extraction": "2025-04-15T15:12:23.158435",
"date_extraction": "2025-04-15T17:18:34.902937",
"ticket_id": 11122,
"ticket_code": "T11143",
"ticket_name": "BRGLAB - Essai inaccessible",
"output_dir": "output/ticket_T11143/T11143_20250415_151222",
"output_dir": "output/ticket_T11143/T11143_20250415_171834",
"files": {
"ticket_info": "ticket_info.json",
"ticket_summary": "ticket_summary.json",
@ -14,7 +14,7 @@
"followers": "followers.json"
},
"stats": {
"messages_count": 6,
"messages_count": 7,
"attachments_count": 1
}
}

View File

@ -145,3 +145,25 @@
2025-04-15 15:01:57 - root - INFO - Messages traités: 5
2025-04-15 15:01:57 - root - INFO - Pièces jointes: 3
2025-04-15 15:01:57 - root - INFO - ------------------------------------------------------------
2025-04-15 16:52:51 - root - INFO - Extraction du ticket T11143
2025-04-15 16:52:51 - root - INFO - ------------------------------------------------------------
2025-04-15 16:52:52 - root - INFO - Traitement de 1 pièces jointes pour le ticket 11122
2025-04-15 16:52:52 - root - INFO - Pièce jointe téléchargée: image.png (1/1)
2025-04-15 16:52:52 - root - INFO - ------------------------------------------------------------
2025-04-15 16:52:52 - root - INFO - Extraction terminée avec succès
2025-04-15 16:52:52 - root - INFO - Ticket: T11143
2025-04-15 16:52:52 - root - INFO - Répertoire: output/ticket_T11143/T11143_20250415_165251
2025-04-15 16:52:52 - root - INFO - Messages traités: 7
2025-04-15 16:52:52 - root - INFO - Pièces jointes: 1
2025-04-15 16:52:52 - root - INFO - ------------------------------------------------------------
2025-04-15 17:18:34 - root - INFO - Extraction du ticket T11143
2025-04-15 17:18:34 - root - INFO - ------------------------------------------------------------
2025-04-15 17:18:34 - root - INFO - Traitement de 1 pièces jointes pour le ticket 11122
2025-04-15 17:18:34 - root - INFO - Pièce jointe téléchargée: image.png (1/1)
2025-04-15 17:18:34 - root - INFO - ------------------------------------------------------------
2025-04-15 17:18:34 - root - INFO - Extraction terminée avec succès
2025-04-15 17:18:34 - root - INFO - Ticket: T11143
2025-04-15 17:18:34 - root - INFO - Répertoire: output/ticket_T11143/T11143_20250415_171834
2025-04-15 17:18:34 - root - INFO - Messages traités: 7
2025-04-15 17:18:34 - root - INFO - Pièces jointes: 1
2025-04-15 17:18:34 - root - INFO - ------------------------------------------------------------