gmao/app_new/outlook/models.py
2026-08-14 16:02:25 +00:00

319 lines
No EOL
14 KiB
Python

"""
Modèle Outlook - GMAO Collège
Intégration avec Outlook/Microsoft
"""
from datetime import datetime, timezone
from app_new.extensions import db
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)
body_content = db.Column(db.Text, 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"
id = db.Column(db.String(100), primary_key=True) # Attachment ID from Microsoft
mail_id = db.Column(db.String(100), 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
content = db.Column(db.LargeBinary, 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(100), 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/