323 lines
15 KiB
Python
323 lines
15 KiB
Python
"""
|
|
Modèle Outlook - GMAO Collège
|
|
Intégration avec Outlook/Microsoft
|
|
"""
|
|
from datetime import datetime, timezone
|
|
from app_new.extensions import db
|
|
from sqlalchemy.dialects.mysql import LONGTEXT, LONGBLOB
|
|
|
|
|
|
class OutlookAccount(db.Model):
|
|
"""Compte Outlook connecté."""
|
|
__tablename__ = "outlook_accounts"
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
|
email = db.Column(db.String(255), nullable=False)
|
|
tenant_id = db.Column(db.String(100), nullable=False)
|
|
refresh_token = db.Column(db.Text, nullable=True)
|
|
client_id = db.Column(db.String(100), nullable=True)
|
|
is_active = db.Column(db.Boolean, default=True)
|
|
last_connected_at = db.Column(db.DateTime, nullable=True)
|
|
connection_status = db.Column(db.String(20), nullable=True)
|
|
scopes = db.Column(db.Text, nullable=True)
|
|
device_code = db.Column(db.Text, nullable=True)
|
|
device_code_expires_at = db.Column(db.DateTime, nullable=True)
|
|
user_code = db.Column(db.String(20), nullable=True)
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
|
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
|
|
|
# Relations
|
|
user = db.relationship("User", backref="outlook_accounts")
|
|
folders = db.relationship("OutlookFolder", backref="account", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self):
|
|
return f"<OutlookAccount {self.email}>"
|
|
|
|
|
|
class OutlookFolder(db.Model):
|
|
"""Dossier Outlook (Boîte de réception, Éléments envoyés, etc.)."""
|
|
__tablename__ = "outlook_folders"
|
|
|
|
id = db.Column(db.String(500), primary_key=True) # Folder ID from Microsoft
|
|
account_id = db.Column(db.Integer, db.ForeignKey("outlook_accounts.id"), nullable=False)
|
|
name = db.Column(db.String(255), nullable=False)
|
|
display_name = db.Column(db.String(255), nullable=True)
|
|
parent_id = db.Column(db.String(500), db.ForeignKey('outlook_folders.id'), nullable=True) # Pour les sous-dossiers
|
|
total_items = db.Column(db.Integer, default=0)
|
|
unread_items = db.Column(db.Integer, default=0)
|
|
folder_type = db.Column(db.String(50), nullable=True) # inbox, sentitems, drafts, etc.
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
# Relations
|
|
mails = db.relationship("OutlookMail", backref="folder", cascade="all, delete-orphan")
|
|
children = db.relationship("OutlookFolder", backref="parent", remote_side=[id])
|
|
|
|
def __repr__(self):
|
|
return f"<OutlookFolder {self.display_name or self.name}>"
|
|
|
|
|
|
class OutlookMail(db.Model):
|
|
"""Email Outlook synchronisé."""
|
|
__tablename__ = "outlook_mails"
|
|
|
|
id = db.Column(db.String(500), primary_key=True) # Message ID from Microsoft
|
|
account_id = db.Column(db.Integer, db.ForeignKey("outlook_accounts.id"), nullable=False)
|
|
folder_id = db.Column(db.String(500), db.ForeignKey("outlook_folders.id"), nullable=True)
|
|
subject = db.Column(db.Text, nullable=True)
|
|
sender = db.Column(db.String(255), nullable=True)
|
|
sender_email = db.Column(db.String(255), nullable=True)
|
|
body_preview = db.Column(db.Text, nullable=True)
|
|
# Un corps HTML Outlook dépasse fréquemment la limite de 64 Kio de TEXT.
|
|
body_content = db.Column(LONGTEXT, nullable=True)
|
|
body_type = db.Column(db.String(20), nullable=True) # html ou text
|
|
received_at = db.Column(db.DateTime, nullable=True)
|
|
is_read = db.Column(db.Boolean, default=False)
|
|
importance = db.Column(db.String(20), nullable=True)
|
|
has_attachments = db.Column(db.Boolean, default=False)
|
|
attachment_count = db.Column(db.Integer, nullable=True)
|
|
from_name = db.Column(db.String(255), nullable=True)
|
|
from_email = db.Column(db.String(255), nullable=True)
|
|
to_recipients = db.Column(db.Text, nullable=True) # JSON
|
|
cc_recipients = db.Column(db.Text, nullable=True) # JSON
|
|
bcc_recipients = db.Column(db.Text, nullable=True) # JSON
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
# Relations
|
|
account = db.relationship("OutlookAccount", backref="mails")
|
|
attachments = db.relationship("OutlookAttachment", backref="mail", cascade="all, delete-orphan")
|
|
interpretations = db.relationship("OutlookMailInterpretation", backref="mail", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self):
|
|
return f"<OutlookMail {self.subject}>"
|
|
|
|
|
|
class OutlookAttachment(db.Model):
|
|
"""Pièce jointe d'un email Outlook."""
|
|
__tablename__ = "outlook_attachments"
|
|
|
|
# Les identifiants Microsoft Graph peuvent dépasser 100 caractères.
|
|
id = db.Column(db.String(500), primary_key=True) # Attachment ID from Microsoft
|
|
mail_id = db.Column(db.String(500), db.ForeignKey("outlook_mails.id"), nullable=False)
|
|
name = db.Column(db.String(500), nullable=False)
|
|
content_type = db.Column(db.String(100), nullable=True)
|
|
size = db.Column(db.Integer, default=0)
|
|
is_inline = db.Column(db.Boolean, default=False)
|
|
content_id = db.Column(db.String(200), nullable=True) # Pour les images inline
|
|
# Les PDF et documents doivent pouvoir dépasser 64 Kio (BLOB).
|
|
content = db.Column(LONGBLOB, nullable=True) # Contenu binaire de la pièce jointe
|
|
|
|
def __repr__(self):
|
|
return f"<OutlookAttachment {self.name}>"
|
|
|
|
|
|
class OutlookMailInterpretation(db.Model):
|
|
"""Interprétation Hermes d'un email avec analyse structurée."""
|
|
__tablename__ = "outlook_mail_interpretations"
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
mail_id = db.Column(db.String(500), db.ForeignKey("outlook_mails.id"), nullable=False)
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
|
|
|
# Analyse brute (texte de l'IA)
|
|
interpretation = db.Column(db.Text, nullable=False)
|
|
|
|
# Analyse structurée (JSON parsé)
|
|
analysis_type = db.Column(db.String(20), nullable=True) # curative, preventive, information
|
|
analysis_action = db.Column(db.String(20), nullable=True) # create, update, inform
|
|
|
|
# Données de l'intervention suggérée
|
|
suggested_title = db.Column(db.String(255), nullable=True)
|
|
suggested_description = db.Column(db.Text, nullable=True)
|
|
suggested_urgency = db.Column(db.String(20), nullable=True) # basse, normale, haute, urgente
|
|
suggested_location = db.Column(db.String(255), nullable=True)
|
|
suggested_equipment = db.Column(db.String(255), nullable=True)
|
|
suggested_assignee = db.Column(db.String(255), nullable=True)
|
|
suggested_date = db.Column(db.DateTime, nullable=True) # Pour les préventives
|
|
|
|
# Lien vers intervention existante (si mise à jour)
|
|
existing_intervention_id = db.Column(db.Integer, db.ForeignKey("interventions.id"), nullable=True)
|
|
|
|
# Contexte utilisateur
|
|
user_context = db.Column(db.Text, nullable=True)
|
|
|
|
# Métadonnées
|
|
raw_json = db.Column(db.Text, nullable=True) # JSON complet stocké
|
|
confidence_score = db.Column(db.Float, nullable=True) # Score de confiance de l'analyse
|
|
status = db.Column(db.String(20), default='pending') # pending, accepted, rejected
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
# Relations
|
|
user = db.relationship("User", backref="outlook_interpretations")
|
|
|
|
def get_parsed_analysis(self):
|
|
"""Retourne l'analyse parsée depuis le JSON stocké."""
|
|
if self.raw_json:
|
|
import json
|
|
try:
|
|
return json.loads(self.raw_json)
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
def set_from_parsed(self, parsed_data):
|
|
"""Définit les champs depuis le JSON parsé."""
|
|
self.analysis_type = parsed_data.get('type')
|
|
self.analysis_action = parsed_data.get('action')
|
|
|
|
intervention = parsed_data.get('intervention', {})
|
|
self.suggested_title = intervention.get('titre')
|
|
self.suggested_description = intervention.get('description')
|
|
self.suggested_urgency = intervention.get('urgence')
|
|
self.suggested_location = intervention.get('lieu')
|
|
self.suggested_equipment = intervention.get('equipement')
|
|
self.suggested_assignee = intervention.get('intervenant_suggere')
|
|
|
|
# Extraire la date (peut être dans intervention.date_intervention ou date_suggestion)
|
|
date_str = intervention.get('date_intervention') or parsed_data.get('date_suggestion')
|
|
if date_str:
|
|
try:
|
|
from datetime import datetime, timezone
|
|
self.suggested_date = datetime.strptime(date_str, '%Y-%m-%d')
|
|
except:
|
|
pass
|
|
|
|
if parsed_data.get('intervention_existante_id'):
|
|
self.existing_intervention_id = parsed_data['intervention_existante_id']
|
|
|
|
import json
|
|
self.raw_json = json.dumps(parsed_data, ensure_ascii=False)
|
|
|
|
def __repr__(self):
|
|
return f"<OutlookMailInterpretation mail={self.mail_id} type={self.analysis_type}>"
|
|
|
|
|
|
class GmaoContext(db.Model):
|
|
"""Contexte GMAO pour l'interprétation des emails par Hermes.
|
|
|
|
Stocke les informations contextuelles sur l'établissement,
|
|
les équipements, les procédures, etc. pour aider Hermes
|
|
à mieux interpréter les demandes.
|
|
"""
|
|
__tablename__ = "gmao_context"
|
|
__table_args__ = {'extend_existing': True}
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, unique=True)
|
|
|
|
# Informations sur l'établissement
|
|
establishment_name = db.Column(db.String(200), nullable=True) # ex: "Collège Jean Jaurès"
|
|
establishment_type = db.Column(db.String(100), nullable=True) # ex: "Collège public"
|
|
address = db.Column(db.Text, nullable=True)
|
|
|
|
# Contexte technique
|
|
technical_context = db.Column(db.Text, nullable=True) # Info sur les bâtiments, surfaces, etc.
|
|
equipment_context = db.Column(db.Text, nullable=True) # Liste des équipements principaux
|
|
procedure_context = db.Column(db.Text, nullable=True) # Procédures spécifiques
|
|
|
|
# Contexte organisationnel
|
|
staff_info = db.Column(db.Text, nullable=True) # Info sur le personnel
|
|
contact_info = db.Column(db.Text, nullable=True) # Contacts utiles (entreprises, etc.)
|
|
|
|
# Instructions personnalisées pour Hermes
|
|
hermes_instructions = db.Column(db.Text, nullable=True) # Instructions spécifiques pour l'IA
|
|
|
|
# Expéditeurs à exclure de l'interprétation (newsletter, spam, etc.)
|
|
excluded_senders = db.Column(db.Text, nullable=True) # Un email par ligne
|
|
|
|
# Configuration de l'interprétation automatique
|
|
auto_interpret_enabled = db.Column(db.Boolean, default=False)
|
|
auto_interpret_interval = db.Column(db.Integer, default=10) # Minutes entre chaque vérification
|
|
auto_interpret_folder = db.Column(db.String(100), default='inbox') # Dossier à surveiller
|
|
auto_interpret_max_emails = db.Column(db.Integer, default=10) # Max emails par exécution
|
|
auto_interpret_lookback_hours = db.Column(db.Integer, default=24) # Heures en arrière pour chercher les emails
|
|
|
|
# Configuration synchronisation PRONOTE
|
|
pronote_sync_enabled = db.Column(db.Boolean, default=False)
|
|
pronote_sync_interval = db.Column(db.Integer, default=360) # Minutes entre chaque sync (6h par défaut)
|
|
pronote_sync_weeks = db.Column(db.Integer, default=4) # Nombre de semaines à synchroniser
|
|
|
|
# Configuration synchronisation Outlook
|
|
outlook_sync_enabled = db.Column(db.Boolean, default=True)
|
|
outlook_sync_interval = db.Column(db.Integer, default=10) # Minutes entre chaque vérification
|
|
outlook_sync_lookback_hours = db.Column(db.Integer, default=24) # Heures en arrière pour chercher les emails
|
|
|
|
# Configuration IA
|
|
# Provider IA : 'openrouter' (cloud) ou 'ollama' (local avec auto-identification)
|
|
ai_provider = db.Column(db.String(20), default='openrouter') # openrouter|ollama
|
|
# Modèle Ollama local (ex: gemma3:4b-it, qwen2.5:3b)
|
|
ollama_model = db.Column(db.String(100), nullable=True)
|
|
|
|
# Watchdog interne (cron local)
|
|
watchdog_enabled = db.Column(db.Boolean, default=False)
|
|
watchdog_interval = db.Column(db.Integer, default=10) # Minutes
|
|
|
|
# Métadonnées
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
|
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
|
|
|
# Relations
|
|
user = db.relationship("User", backref="gmao_context", uselist=False)
|
|
|
|
@staticmethod
|
|
def get_for_user(user_id):
|
|
"""Récupère ou crée le contexte GMAO pour un utilisateur."""
|
|
context = GmaoContext.query.filter_by(user_id=user_id).first()
|
|
if not context:
|
|
context = GmaoContext(user_id=user_id)
|
|
db.session.add(context)
|
|
db.session.commit()
|
|
return context
|
|
|
|
def get_full_context(self):
|
|
"""Retourne le contexte complet formaté pour Hermes."""
|
|
parts = []
|
|
|
|
if self.establishment_name:
|
|
parts.append(f"Établissement: {self.establishment_name}")
|
|
if self.establishment_type:
|
|
parts[-1] += f" ({self.establishment_type})"
|
|
|
|
if self.address:
|
|
parts.append(f"Adresse: {self.address}")
|
|
|
|
if self.technical_context:
|
|
parts.append(f"Contexte technique:\n{self.technical_context}")
|
|
|
|
if self.equipment_context:
|
|
parts.append(f"Équipements:\n{self.equipment_context}")
|
|
|
|
if self.procedure_context:
|
|
parts.append(f"Procédures:\n{self.procedure_context}")
|
|
|
|
if self.staff_info:
|
|
parts.append(f"Personnel:\n{self.staff_info}")
|
|
|
|
if self.contact_info:
|
|
parts.append(f"Contacts:\n{self.contact_info}")
|
|
|
|
if self.hermes_instructions:
|
|
parts.append(f"Instructions:\n{self.hermes_instructions}")
|
|
|
|
return "\n\n".join(parts) if parts else ""
|
|
|
|
def is_sender_excluded(self, sender_email):
|
|
"""Vérifie si un expéditeur est dans la liste d'exclusion."""
|
|
if not self.excluded_senders or not sender_email:
|
|
return False
|
|
excluded = [e.strip().lower() for e in self.excluded_senders.split('\n') if e.strip()]
|
|
return sender_email.lower() in excluded
|
|
|
|
def get_excluded_senders_list(self):
|
|
"""Retourne la liste des expéditeurs exclus."""
|
|
if not self.excluded_senders:
|
|
return []
|
|
return [e.strip() for e in self.excluded_senders.split('\n') if e.strip()]
|
|
|
|
def __repr__(self):
|
|
return f"<GmaoContext user={self.user_id}>"
|
|
|
|
|
|
|
|
# Note: Les routes Outlook sont dans app_new/outlook/routes.py
|
|
# Les templates sont dans app_new/outlook/templates/
|