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
|
2026-08-21 12:32:00 +02:00
|
|
|
|
from ..core.models.equipment import Equipment, EquipmentCategory
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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
|
2026-08-21 13:08:30 +02:00
|
|
|
|
from ..core.models.college import Building, Zone, Room
|
2026-08-21 00:43:46 +02:00
|
|
|
|
from ..core.models.company import Service, Company
|
2026-08-21 12:12:23 +02:00
|
|
|
|
from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES, WORKFLOW_TYPES, WORKFLOW_STATUS_ORDERS
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
2026-08-21 12:38:37 +02:00
|
|
|
|
interventions_bp = Blueprint('interventions', __name__, template_folder='templates')
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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', '')
|
2026-08-21 12:12:23 +02:00
|
|
|
|
workflow_type = request.args.get('workflow_type', '')
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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)
|
2026-08-21 12:12:23 +02:00
|
|
|
|
if workflow_type in WORKFLOW_TYPES:
|
|
|
|
|
|
query = query.filter_by(workflow_type=workflow_type)
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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,
|
2026-08-21 12:12:23 +02:00
|
|
|
|
workflow_types=WORKFLOW_TYPES,
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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':
|
2026-08-15 00:47:55 +02:00
|
|
|
|
# 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'
|
|
|
|
|
|
|
2026-08-21 12:12:23 +02:00
|
|
|
|
selected_type = request.form.get('intervention_type', 'curatif')
|
|
|
|
|
|
workflow_type = request.form.get('workflow_type') or {
|
|
|
|
|
|
'preventif': 'preventive', 'amelioratif': 'travaux',
|
|
|
|
|
|
'prevention': 'prevention', 'administratif': 'prevention',
|
|
|
|
|
|
}.get(selected_type, 'corrective')
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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'),
|
2026-08-21 12:12:23 +02:00
|
|
|
|
type=selected_type,
|
|
|
|
|
|
workflow_type=workflow_type if workflow_type in WORKFLOW_TYPES else 'corrective',
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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')
|
2026-08-15 00:47:55 +02:00
|
|
|
|
interpretation.status = 'accepted'
|
2026-08-14 18:02:25 +02:00
|
|
|
|
if message:
|
2026-08-15 00:47:55 +02:00
|
|
|
|
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')
|
2026-08-15 00:47:55 +02:00
|
|
|
|
interpretation.status = 'accepted'
|
2026-08-14 18:02:25 +02:00
|
|
|
|
if mail:
|
2026-08-15 00:47:55 +02:00
|
|
|
|
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': ''
|
2026-08-21 12:12:23 +02:00
|
|
|
|
, 'workflow_type': request.args.get('workflow', 'corrective')
|
2026-08-14 18:02:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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')
|
2026-08-21 12:12:23 +02:00
|
|
|
|
prefilled['workflow_type'] = 'preventive' if interpretation.analysis_type == 'preventive' else prefilled.get('workflow_type', 'corrective')
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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,
|
2026-08-21 12:12:23 +02:00
|
|
|
|
workflow_types=WORKFLOW_TYPES,
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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)
|
2026-08-21 12:14:07 +02:00
|
|
|
|
allowed_workflow_statuses = set(WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective'])) | {'refusee', 'annulee', 'reportee'}
|
2026-08-21 00:43:46 +02:00
|
|
|
|
transitions = [(key, value['label']) for key, value in INTERVENTION_STATUSES.items()
|
2026-08-21 12:14:07 +02:00
|
|
|
|
if key in INTERVENTION_TRANSITIONS.get(intervention.status, set()) and key in allowed_workflow_statuses]
|
2026-08-21 00:43:46 +02:00
|
|
|
|
history = intervention.status_history.order_by(StatusChange.created_at.desc()).all()
|
2026-08-21 12:38:37 +02:00
|
|
|
|
# Utiliser explicitement le template du module. Une ancienne copie globale
|
|
|
|
|
|
# de ``interventions/detail.html`` est conservée pour compatibilité.
|
2026-08-21 12:40:58 +02:00
|
|
|
|
return render_template('interventions_module/detail.html',
|
2026-08-14 18:02:25 +02:00
|
|
|
|
intervention=intervention,
|
2026-08-21 00:43:46 +02:00
|
|
|
|
statuses=INTERVENTION_STATUSES,
|
|
|
|
|
|
transitions=transitions,
|
|
|
|
|
|
history=history,
|
2026-08-21 12:32:00 +02:00
|
|
|
|
workflow_status_order=WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective']),
|
2026-08-21 13:08:30 +02:00
|
|
|
|
buildings=Building.query.order_by(Building.name).all(),
|
|
|
|
|
|
zones=Zone.query.order_by(Zone.name).all(),
|
2026-08-21 12:32:00 +02:00
|
|
|
|
rooms=Room.query.order_by(Room.name).all(),
|
|
|
|
|
|
categories=EquipmentCategory.query.order_by(EquipmentCategory.name).all(),
|
|
|
|
|
|
equipment_choices=Equipment.query.filter_by(is_deleted=False).order_by(Equipment.name).all(),
|
2026-08-21 00:43:46 +02:00
|
|
|
|
services=Service.query.filter_by(is_active=True).order_by(Service.name).all(),
|
|
|
|
|
|
companies=Company.query.filter_by(is_active=True).order_by(Company.name).all())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@interventions_bp.route('/<int:id>/work-details', methods=['POST'])
|
|
|
|
|
|
@login_required
|
|
|
|
|
|
def save_work_details(id):
|
|
|
|
|
|
"""Enregistre les informations de travaux depuis la fiche intervention."""
|
|
|
|
|
|
intervention = Intervention.query.get_or_404(id)
|
2026-08-21 00:53:04 +02:00
|
|
|
|
if 'execution_mode' in request.form:
|
|
|
|
|
|
intervention.execution_mode = request.form.get('execution_mode') or 'interne'
|
|
|
|
|
|
if 'department_service_id' in request.form:
|
|
|
|
|
|
service_id = request.form.get('department_service_id')
|
|
|
|
|
|
intervention.department_service_id = int(service_id) if service_id and service_id.isdigit() else None
|
|
|
|
|
|
if 'company_id' in request.form:
|
|
|
|
|
|
company_id = request.form.get('company_id')
|
|
|
|
|
|
intervention.company_id = int(company_id) if company_id and company_id.isdigit() else None
|
|
|
|
|
|
if 'quote_status' in request.form:
|
|
|
|
|
|
intervention.quote_status = request.form.get('quote_status') or 'non_requis'
|
|
|
|
|
|
if 'quote_reference' in request.form:
|
|
|
|
|
|
intervention.quote_reference = request.form.get('quote_reference') or None
|
|
|
|
|
|
for field in ('quote_amount_ht', 'quote_amount_ttc'):
|
|
|
|
|
|
if field in request.form:
|
|
|
|
|
|
setattr(intervention, field, float(request.form[field]) if request.form.get(field) else None)
|
2026-08-21 00:43:46 +02:00
|
|
|
|
for field in ('department_request_date', 'chief_validation_date', 'department_validation_date', 'quote_date', 'quote_valid_until'):
|
2026-08-21 00:53:04 +02:00
|
|
|
|
if field in request.form:
|
|
|
|
|
|
value = request.form.get(field)
|
|
|
|
|
|
setattr(intervention, field, datetime.strptime(value, '%Y-%m-%d').date() if value else None)
|
|
|
|
|
|
if 'work_notes' in request.form:
|
|
|
|
|
|
intervention.work_notes = request.form.get('work_notes') or None
|
2026-08-21 00:43:46 +02:00
|
|
|
|
db.session.commit()
|
|
|
|
|
|
flash('Workflow travaux enregistré.', 'success')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 12:32:00 +02:00
|
|
|
|
@interventions_bp.route('/<int:id>/location', methods=['POST'])
|
|
|
|
|
|
@login_required
|
|
|
|
|
|
def update_location(id):
|
2026-08-21 13:08:30 +02:00
|
|
|
|
"""Modifie la localisation en cascade bâtiment -> zone -> salle -> équipement."""
|
2026-08-21 12:32:00 +02:00
|
|
|
|
intervention = Intervention.query.get_or_404(id)
|
2026-08-21 13:08:30 +02:00
|
|
|
|
building_id = request.form.get('building_id', type=int)
|
|
|
|
|
|
zone_id = request.form.get('zone_id', type=int)
|
2026-08-21 12:32:00 +02:00
|
|
|
|
room_id = request.form.get('room_id', type=int)
|
|
|
|
|
|
category_id = request.form.get('category_id', type=int)
|
|
|
|
|
|
equipment_id = request.form.get('equipment_id', type=int)
|
|
|
|
|
|
room = Room.query.get(room_id) if room_id else None
|
|
|
|
|
|
category = EquipmentCategory.query.get(category_id) if category_id else None
|
|
|
|
|
|
equipment = Equipment.query.get(equipment_id) if equipment_id else None
|
|
|
|
|
|
if room_id and not room:
|
|
|
|
|
|
flash('Salle invalide.', 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
2026-08-21 13:08:30 +02:00
|
|
|
|
if room and building_id and room.building_id != building_id:
|
|
|
|
|
|
flash("La salle ne correspond pas au bâtiment sélectionné.", 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
|
|
|
|
|
if room and zone_id and room.zone_id != zone_id:
|
|
|
|
|
|
flash("La salle ne correspond pas à la zone sélectionnée.", 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
2026-08-21 12:32:00 +02:00
|
|
|
|
if equipment:
|
|
|
|
|
|
if room_id and equipment.room_id != room_id:
|
|
|
|
|
|
flash("L'équipement choisi n'est pas dans la salle sélectionnée.", 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
|
|
|
|
|
if category_id and equipment.effective_category_id != category_id:
|
|
|
|
|
|
flash("L'équipement choisi ne correspond pas à la catégorie sélectionnée.", 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
|
|
|
|
|
intervention.room_id = room.id if room else None
|
|
|
|
|
|
intervention.equipment_id = equipment.id if equipment else None
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
flash('Salle et équipement concernés mis à jour.', 'success')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
|
@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)
|
2026-08-21 12:12:23 +02:00
|
|
|
|
workflow_type = request.form.get('workflow_type')
|
|
|
|
|
|
if workflow_type in WORKFLOW_TYPES:
|
|
|
|
|
|
intervention.workflow_type = workflow_type
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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 ''
|
2026-08-21 12:12:23 +02:00
|
|
|
|
, 'workflow_type': intervention.workflow_type
|
2026-08-14 18:02:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return render_template('interventions/new.html',
|
|
|
|
|
|
intervention=intervention,
|
|
|
|
|
|
lots=lots,
|
|
|
|
|
|
equipments=equipments,
|
|
|
|
|
|
rooms=rooms,
|
|
|
|
|
|
users=users,
|
|
|
|
|
|
statuses=INTERVENTION_STATUSES,
|
|
|
|
|
|
priorities=PRIORITIES,
|
2026-08-21 12:12:23 +02:00
|
|
|
|
workflow_types=WORKFLOW_TYPES,
|
2026-08-14 18:02:25 +02:00
|
|
|
|
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
|
2026-08-21 00:53:04 +02:00
|
|
|
|
# Les anciennes fiches utilisent ``to_status`` ; accepter aussi ``status``
|
|
|
|
|
|
# pour les formulaires d'édition et conserver une transition réellement modifiable.
|
|
|
|
|
|
new_status = request.form.get('to_status') or request.form.get('status')
|
2026-08-14 18:02:25 +02:00
|
|
|
|
comment = request.form.get('comment', '')
|
2026-08-15 00:56:36 +02:00
|
|
|
|
|
2026-08-15 01:12:51 +02:00
|
|
|
|
allowed = INTERVENTION_TRANSITIONS.get(old_status, set())
|
2026-08-21 12:14:07 +02:00
|
|
|
|
workflow_statuses = set(WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective'])) | {'refusee', 'annulee', 'reportee'}
|
|
|
|
|
|
if new_status not in workflow_statuses:
|
|
|
|
|
|
flash(f'Le statut « {new_status} » ne correspond pas au workflow {intervention.workflow_label}.', 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=intervention.id))
|
2026-08-15 01:12:51 +02:00
|
|
|
|
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))
|
|
|
|
|
|
|
2026-08-15 00:56:36 +02:00
|
|
|
|
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 aujourd’hui : {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':
|
2026-08-15 00:56:36 +02:00
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 12:08:21 +02:00
|
|
|
|
@interventions_bp.route('/<int:id>/refuse', methods=['POST'])
|
|
|
|
|
|
@login_required
|
|
|
|
|
|
def refuse(id):
|
|
|
|
|
|
"""Refuse une demande avec un motif obligatoire."""
|
|
|
|
|
|
intervention = Intervention.query.get_or_404(id)
|
|
|
|
|
|
reason = (request.form.get('refusal_reason') or '').strip()
|
|
|
|
|
|
if not reason:
|
|
|
|
|
|
flash('Le motif du refus est obligatoire.', 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
|
|
|
|
|
if intervention.status in {'terminee', 'cloturee', 'refusee'}:
|
|
|
|
|
|
flash('Cette intervention ne peut plus être refusée depuis son statut actuel.', 'danger')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
|
|
|
|
|
old_status = intervention.status
|
|
|
|
|
|
intervention.status = 'refusee'
|
|
|
|
|
|
intervention.refusal_reason = reason
|
|
|
|
|
|
intervention.refused_at = datetime.now(timezone.utc)
|
|
|
|
|
|
intervention.refused_by_id = current_user.id
|
|
|
|
|
|
db.session.add(StatusChange(intervention_id=id, from_status=old_status, to_status='refusee', changed_by_id=current_user.id, comment=reason))
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
flash('Intervention refusée et motif enregistré.', 'success')
|
|
|
|
|
|
return redirect(url_for('interventions.detail', id=id))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
|
@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))
|