""" 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"" 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"" 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"" 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"" 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"" 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"" # Note: Les routes Outlook sont dans app_new/outlook/routes.py # Les templates sont dans app_new/outlook/templates/