gmao/app_new/interventions/crud.py

591 lines
26 KiB
Python
Raw Normal View History

2026-08-14 18:02:25 +02:00
"""Interventions Crud Routes - GMAO Collège"""
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from flask_login import login_required, current_user
from app_new.extensions import db
from ..core.models.maintenance import Intervention, InterventionComment
from ..core.models.equipment import Equipment
from ..core.models.user import User
from datetime import datetime
interventions_bp = Blueprint("interventions", __name__, url_prefix="/interventions", template_folder="templates")
"""
Interventions Routes - GMAO Collège
Gestion des interventions de maintenance
"""
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_required, current_user
from datetime import datetime, timezone
from app_new.extensions import db
from ..core.models.maintenance import Intervention, StatusChange, InterventionComment, Lot
from ..core.models.planning import ScheduledTask
from ..core.models.equipment import Equipment
from ..core.models.college import Room
from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES
2026-08-14 18:02:25 +02:00
interventions_bp = Blueprint('interventions', __name__)
@interventions_bp.route('/')
@login_required
def index():
"""Liste des interventions avec filtres avances et tri par colonne."""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', '')
priority = request.args.get('priority', '')
intervention_type = request.args.get('type', '')
lot_id = request.args.get('lot', type=int)
assigned_to_id = request.args.get('assigned_to_id', type=int)
frequency = request.args.get('frequency', '')
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
sort_col = request.args.get('sort', 'created_at')
sort_dir = request.args.get('dir', 'desc')
show_trashed = request.args.get('trashed', '0') == '1'
query = Intervention.query
if show_trashed:
query = query.filter_by(is_deleted=True)
else:
query = query.filter_by(is_deleted=False)
if status:
query = query.filter_by(status=status)
if priority:
query = query.filter_by(priority=priority)
if intervention_type:
query = query.filter(Intervention.type == intervention_type)
if lot_id:
query = query.filter_by(lot_id=lot_id)
if assigned_to_id:
query = query.filter_by(assigned_to_id=assigned_to_id)
if date_from:
try:
d = datetime.strptime(date_from, '%Y-%m-%d').date()
query = query.filter(Intervention.scheduled_date >= d)
except ValueError:
pass
if date_to:
try:
d = datetime.strptime(date_to, '%Y-%m-%d').date()
query = query.filter(Intervention.scheduled_date <= d)
except ValueError:
pass
if frequency:
if frequency == 'recurring':
query = query.filter(Intervention.is_recurring == True)
elif frequency == 'ponctuel':
query = query.filter(Intervention.is_recurring == False)
elif frequency.isdigit():
query = query.filter(Intervention.frequency_days == int(frequency))
# Tri par colonne (avec validation pour eviter injection)
valid_sort_cols = {'created_at', 'scheduled_date', 'priority', 'status', 'title'}
if sort_col not in valid_sort_cols:
sort_col = 'created_at'
sort_dir = 'asc' if sort_dir == 'asc' else 'desc'
order_clause = getattr(Intervention, sort_col).asc() if sort_dir == 'asc' else getattr(Intervention, sort_col).desc()
query = query.order_by(order_clause)
interventions = query.paginate(page=page, per_page=20)
lots = Lot.query.order_by(Lot.name).all()
users = User.query.filter_by(is_active=True).order_by(User.username).all()
# Compter les interventions par statut
status_counts = {}
for status_key in INTERVENTION_STATUSES.keys():
status_counts[status_key] = Intervention.query.filter_by(status=status_key, is_deleted=False).count()
# Compter les interventions à jeter
trashed_count = Intervention.query.filter_by(is_deleted=True).count()
return render_template('interventions/index.html',
interventions=interventions,
lots=lots,
users=users,
statuses=INTERVENTION_STATUSES,
priorities=PRIORITIES,
status_counts=status_counts,
trashed_count=trashed_count,
show_trashed=show_trashed,
sort_col=sort_col,
sort_dir=sort_dir)
@interventions_bp.route('/new', methods=['GET', 'POST'])
@login_required
def create():
"""Créer une intervention."""
from ..core.models.user import User
from ..outlook.models import OutlookMailInterpretation
if request.method == 'POST':
# Une intervention générale peut cibler une salle sans équipement.
equipment_value = request.form.get('equipment_id')
equipment_id = int(equipment_value) if equipment_value and equipment_value.isdigit() else None
2026-08-14 18:02:25 +02:00
# Déterminer le demandeur
requester_name = request.form.get('requester_name', '').strip()
if not requester_name:
# Si pas de demandeur fourni, utiliser le nom de l'utilisateur connecté
requester_name = current_user.username if hasattr(current_user, 'username') else 'Inconnu'
intervention = Intervention(
title=request.form.get('title'),
description=request.form.get('description') or '',
lot_id=request.form.get('lot_id') or None,
equipment_id=equipment_id,
room_id=request.form.get('room_id') or None,
assigned_to_id=request.form.get('assigned_to_id') or None,
author_id=current_user.id,
status=request.form.get('status', 'brouillon'),
priority=request.form.get('priority', 'normale'),
type=request.form.get('intervention_type', 'curatif'),
notes=request.form.get('notes') or '',
requester_name=requester_name,
scheduled_date=datetime.strptime(request.form.get('intervention_date'), '%Y-%m-%d').date() if request.form.get('intervention_date') else None,
scheduled_start=datetime.strptime(request.form.get('scheduled_start'), '%H:%M').time() if request.form.get('scheduled_start') else None,
scheduled_end=datetime.strptime(request.form.get('scheduled_end'), '%H:%M').time() if request.form.get('scheduled_end') else None
)
db.session.add(intervention)
db.session.commit()
# Marquer l'interprétation comme acceptée si fournie
from_interpretation_id = request.form.get('from_interpretation')
source = request.form.get('source', 'outlook') # 'outlook' ou 'ent'
if from_interpretation_id:
if source == 'ent':
from app_new.ent.interpretation_models import EntMessageInterpretation
from app_new.lib_ext.ent_service import move_ent_message_to_processed
interpretation = EntMessageInterpretation.query.get(from_interpretation_id)
if interpretation:
message = interpretation.message
# Déplacer le message ENT vers "Traités"
if message:
move_result = move_ent_message_to_processed(interpretation.message_id)
if move_result.get('success'):
flash('Intervention créée. Message ENT déplacé vers Traités.', 'success')
else:
flash(f'Intervention créée. Erreur déplacement ENT: {move_result.get("error")}', 'warning')
interpretation.status = 'accepted'
2026-08-14 18:02:25 +02:00
if message:
message.intervention_id = intervention.id
2026-08-14 18:02:25 +02:00
db.session.commit()
else:
from app_new.outlook.models import OutlookMail
from app_new.outlook.routes import move_mail_to_processed
interpretation = OutlookMailInterpretation.query.get(from_interpretation_id)
if interpretation:
mail = interpretation.mail
# Déplacer le mail Outlook vers "Traités"
if mail:
move_result = move_mail_to_processed(mail)
if move_result.get('success'):
flash('Intervention créée. Mail déplacé vers Traités.', 'success')
else:
flash(f'Intervention créée. Erreur déplacement mail: {move_result.get("error")}', 'warning')
interpretation.status = 'accepted'
2026-08-14 18:02:25 +02:00
if mail:
mail.intervention_id = intervention.id
2026-08-14 18:02:25 +02:00
db.session.commit()
# Flash déjà envoyé ci-dessus
else:
flash('Intervention créée avec succès.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
# GET - Pré-remplir depuis interprétation ou tâche planifiée
from_interpretation = request.args.get('from_interpretation')
scheduled_task_id = request.args.get('scheduled_task_id', type=int)
# Valeurs par défaut
prefilled = {
'title': '',
'description': '',
'priority': 'normale',
'type': 'curatif',
'location': '',
'equipment': '',
'assignee': '',
'date': None,
'requester_name': ''
}
interpretation = None
json_suggestions = None
if from_interpretation:
source = request.args.get('source', 'outlook')
if source == 'ent':
from app_new.ent.interpretation_models import EntMessageInterpretation
interpretation = EntMessageInterpretation.query.get(from_interpretation)
else:
interpretation = OutlookMailInterpretation.query.get(from_interpretation)
if interpretation:
# Adapter selon la source
if source == 'ent':
json_suggestions = {
'title': interpretation.message.subject if interpretation.message else '',
'description': interpretation.suggested_description,
'priority': interpretation.analysis_priority,
'type': interpretation.analysis_type,
'location': '',
'equipment': interpretation.suggested_equipment,
'assignee': '',
'date': None
}
prefilled['title'] = interpretation.message.subject if interpretation.message else ''
prefilled['description'] = interpretation.suggested_description or ''
prefilled['priority'] = interpretation.analysis_priority or 'normale'
# Demandeur = sender du message ENT
prefilled['requester_name'] = interpretation.message.sender_name if interpretation.message else ''
type_map = {
'curative': 'curatif',
'preventive': 'preventif',
'administrative': 'administratif',
'formation': 'formation',
'information': 'curatif'
}
prefilled['type'] = type_map.get(interpretation.analysis_type, 'curatif')
prefilled['equipment'] = interpretation.suggested_equipment or ''
else:
json_suggestions = {
'title': interpretation.suggested_title,
'description': interpretation.suggested_description,
'priority': interpretation.suggested_urgency,
'type': interpretation.analysis_type,
'location': interpretation.suggested_location,
'equipment': interpretation.suggested_equipment,
'assignee': interpretation.suggested_assignee,
'date': interpretation.suggested_date.strftime('%d/%m/%Y') if interpretation.suggested_date else None
}
# Pré-remplir avec les valeurs de l'interprétation
prefilled['title'] = interpretation.suggested_title or ''
prefilled['description'] = interpretation.suggested_description or ''
prefilled['priority'] = interpretation.suggested_urgency or 'normale'
# Demandeur = sender du mail Outlook
prefilled['requester_name'] = interpretation.mail.from_name if interpretation.mail else ''
# Mapper le type
type_map = {
'curative': 'curatif',
'preventive': 'preventif',
'administrative': 'administratif',
'formation': 'formation',
'information': 'curatif'
}
prefilled['type'] = type_map.get(interpretation.analysis_type, 'curatif')
prefilled['location'] = interpretation.suggested_location or ''
prefilled['equipment'] = interpretation.suggested_equipment or ''
prefilled['assignee'] = interpretation.suggested_assignee or ''
prefilled['date'] = interpretation.suggested_date
# Chercher l'équipement correspondant si spécifié
equipment_id = None
equipment_match = None
if prefilled['equipment']:
eq = Equipment.query.filter(Equipment.name.ilike(f"%{prefilled['equipment']}%")).first()
if eq:
equipment_id = eq.id
equipment_match = eq
# Pré-remplir depuis tâche planifiée si fourni
if scheduled_task_id and not from_interpretation:
task = ScheduledTask.query.get(scheduled_task_id)
if task:
prefilled['title'] = f"Intervention: {task.preventive_task.name if task.preventive_task else task.lot_task.name if task.lot_task else 'Tâche planifiée'}"
prefilled['description'] = task.notes or ''
prefilled['date'] = task.scheduled_date.strftime('%Y-%m-%d') if task.scheduled_date else None
prefilled['scheduled_start'] = task.scheduled_start.strftime('%H:%M') if task.scheduled_start else None
prefilled['scheduled_end'] = task.scheduled_end.strftime('%H:%M') if task.scheduled_end else None
# Chercher la salle correspondante si spécifiée
room_id = None
room_match = None
if prefilled['location']:
room = Room.query.filter(Room.name.ilike(f"%{prefilled['location']}%")).first()
if room:
room_id = room.id
room_match = room
lots = Lot.query.order_by(Lot.name).all()
equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all()
rooms = Room.query.order_by(Room.name).all()
users = User.query.filter_by(is_active=True).order_by(User.username).all()
return render_template('interventions/new.html',
lots=lots,
equipments=equipments,
rooms=rooms,
users=users,
statuses=INTERVENTION_STATUSES,
priorities=PRIORITIES,
prefilled=prefilled,
equipment_id=equipment_id,
equipment_match=equipment_match,
room_id=room_id,
room_match=room_match,
from_interpretation=from_interpretation,
interpretation=interpretation,
json_suggestions=json_suggestions,
source=source if from_interpretation else 'outlook')
@interventions_bp.route('/create-for-group', methods=['GET'])
@login_required
def create_for_group_select():
"""Sélection d'équipements pour création groupée."""
equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all()
return render_template('interventions/create_for_group_select.html', equipments=equipments)
@interventions_bp.route('/create-for-group', methods=['POST'])
@login_required
def create_for_group():
"""Créer des interventions pour plusieurs équipements."""
equipment_ids = request.form.getlist('equipment_ids')
title = request.form.get('title')
description = request.form.get('description', '')
priority = request.form.get('priority', 'normale')
lot_id = request.form.get('lot_id')
if not equipment_ids:
flash('Aucun équipement sélectionné.', 'error')
return redirect(url_for('interventions.create_for_group_select'))
created_count = 0
for eq_id in equipment_ids:
equipment = Equipment.query.get(eq_id)
if equipment:
intervention = Intervention(
title=title,
description=description,
priority=priority,
lot_id=lot_id if lot_id else None,
equipment_id=equipment.id,
room_id=equipment.room_id,
status='brouillon',
intervention_type='corrective'
)
db.session.add(intervention)
created_count += 1
db.session.commit()
flash(f'{created_count} intervention(s) créée(s) avec succès.', 'success')
return redirect(url_for('interventions.index'))
@interventions_bp.route('/<int:id>')
@login_required
def detail(id):
"""Détail d'une intervention."""
intervention = Intervention.query.get_or_404(id)
return render_template('interventions/detail.html',
intervention=intervention,
statuses=INTERVENTION_STATUSES)
@interventions_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
"""Modifier une intervention."""
from ..core.models.user import User
intervention = Intervention.query.get_or_404(id)
if request.method == 'POST':
old_status = intervention.status
intervention.title = request.form.get('title')
intervention.description = request.form.get('description')
intervention.lot_id = request.form.get('lot_id') or None
intervention.equipment_id = request.form.get('equipment_id') or None
intervention.room_id = request.form.get('room_id') or None
intervention.assigned_to_id = request.form.get('assigned_to_id') or None
# Ne pas changer le statut si le formulaire ne le contient pas
new_status = request.form.get('status')
if new_status:
intervention.status = new_status
intervention.priority = request.form.get('priority')
intervention.type = request.form.get('intervention_type', intervention.type)
intervention.scheduled_date = request.form.get('scheduled_date') or None
intervention.notes = request.form.get('notes')
intervention.requester_name = request.form.get('requester_name') or None
# Enregistrer le changement de statut seulement si le statut a changé
if new_status and old_status != intervention.status:
status_change = StatusChange(
intervention_id=intervention.id,
from_status=old_status,
to_status=intervention.status,
changed_by_id=current_user.id
)
db.session.add(status_change)
db.session.commit()
flash('Intervention modifiée avec succès.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
lots = Lot.query.order_by(Lot.name).all()
equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all()
rooms = Room.query.order_by(Room.name).all()
users = User.query.filter_by(is_active=True).order_by(User.username).all()
# Pré-remplir avec les valeurs existantes pour l'édition
prefilled = {
'title': intervention.title,
'description': intervention.description or '',
'priority': intervention.priority,
'type': intervention.type,
'location': intervention.equipment.room.name if intervention.equipment and intervention.equipment.room else '',
'equipment': intervention.equipment.name if intervention.equipment else '',
'assignee': intervention.assigned_to.username if intervention.assigned_to else '',
'date': intervention.scheduled_date,
'requester_name': intervention.requester_name or ''
}
return render_template('interventions/new.html',
intervention=intervention,
lots=lots,
equipments=equipments,
rooms=rooms,
users=users,
statuses=INTERVENTION_STATUSES,
priorities=PRIORITIES,
prefilled=prefilled,
title="Modifier l'intervention")
@interventions_bp.route('/<int:id>/status', methods=['POST'])
@login_required
def change_status(id):
"""Changer le statut d'une intervention."""
intervention = Intervention.query.get_or_404(id)
old_status = intervention.status
new_status = request.form.get('status')
comment = request.form.get('comment', '')
allowed = INTERVENTION_TRANSITIONS.get(old_status, set())
if new_status != old_status and new_status not in allowed:
flash(f'Transition interdite : {old_status}{new_status}.', 'danger')
return redirect(url_for('interventions.detail', id=intervention.id))
if new_status == 'en_cours':
from ..core.services.planning_service import PlanningService
reason = PlanningService.maintenance_unavailability(
datetime.now(timezone.utc).date(), user_id=current_user.id
)
if reason:
flash(f'Impossible de démarrer une maintenance aujourdhui : {reason}.', 'danger')
return redirect(url_for('interventions.detail', id=intervention.id))
2026-08-14 18:02:25 +02:00
if new_status != old_status:
intervention.status = new_status
status_change = StatusChange(
intervention_id=intervention.id,
from_status=old_status,
to_status=new_status,
changed_by_id=current_user.id,
comment=comment
)
db.session.add(status_change)
if new_status == 'terminee':
now = datetime.now(timezone.utc)
intervention.completed_date = now.date()
intervention.completed_at = now
2026-08-14 18:02:25 +02:00
db.session.commit()
flash('Statut modifié.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
@interventions_bp.route('/<int:id>/comment', methods=['POST'])
@login_required
def add_comment(id):
"""Ajouter un commentaire à une intervention."""
intervention = Intervention.query.get_or_404(id)
content = request.form.get('content')
if content:
comment = InterventionComment(
intervention_id=intervention.id,
author_id=current_user.id,
content=content
)
db.session.add(comment)
db.session.commit()
flash('Commentaire ajouté.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
@interventions_bp.route('/<int:id>/soft-delete', methods=['POST'])
@login_required
def soft_delete(id):
"""Mettre une intervention en corbeille (soft delete)."""
intervention = Intervention.query.get_or_404(id)
intervention.is_deleted = True
intervention.deleted_at = datetime.now(timezone.utc)
intervention.deleted_by_id = current_user.id
db.session.commit()
flash('Intervention mise en corbeille.', 'success')
return redirect(url_for('interventions.index'))
@interventions_bp.route('/<int:id>/postpone', methods=['POST'])
@login_required
def postpone(id):
"""Reporter une intervention : motif, compteur, conservation date originale."""
intervention = Intervention.query.get_or_404(id)
new_date_str = request.form.get('new_scheduled_date')
reason = request.form.get('reason', '').strip()
if not new_date_str:
flash('Nouvelle date requise.', 'danger')
return redirect(url_for('interventions.detail', id=id))
try:
new_date = datetime.strptime(new_date_str, '%Y-%m-%d').date()
except ValueError:
flash('Format de date invalide.', 'danger')
return redirect(url_for('interventions.detail', id=id))
# Conserver la date originale à la première reprogrammation
if intervention.postpone_count == 0 and intervention.scheduled_date:
intervention.original_scheduled_date = intervention.scheduled_date
intervention.scheduled_date = new_date
intervention.postpone_count += 1
intervention.last_postpone_reason = reason
intervention.last_postpone_at = datetime.now(timezone.utc)
# Commentaire d'historique
comment_text = f"Report #{intervention.postpone_count}"
if intervention.original_scheduled_date:
comment_text += f" (date originale {intervention.original_scheduled_date.strftime('%d/%m/%Y')})"
comment_text += f"{new_date.strftime('%d/%m/%Y')}"
if reason:
comment_text += f"{reason}"
status_change = StatusChange(
intervention_id=intervention.id,
from_status=intervention.status,
to_status='reportee',
changed_by_id=current_user.id,
comment=comment_text
)
db.session.add(status_change)
db.session.commit()
flash(f"Intervention reportée au {new_date.strftime('%d/%m/%Y')}.", 'success')
return redirect(url_for('interventions.detail', id=id))