gmao/app_new/interventions/crud.py

679 lines
31 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 ..core.models.company import Service, Company
from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES, WORKFLOW_TYPES, WORKFLOW_STATUS_ORDERS
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', '')
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)
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,
workflow_status_order=WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective']),
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':
# 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'
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'),
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')
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': ''
, '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')
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,
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)
allowed_workflow_statuses = set(WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective'])) | {'refusee', 'annulee', 'reportee'}
transitions = [(key, value['label']) for key, value in INTERVENTION_STATUSES.items()
if key in INTERVENTION_TRANSITIONS.get(intervention.status, set()) and key in allowed_workflow_statuses]
history = intervention.status_history.order_by(StatusChange.created_at.desc()).all()
2026-08-14 18:02:25 +02:00
return render_template('interventions/detail.html',
intervention=intervention,
statuses=INTERVENTION_STATUSES,
transitions=transitions,
history=history,
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)
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)
for field in ('department_request_date', 'chief_validation_date', 'department_validation_date', 'quote_date', 'quote_valid_until'):
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
db.session.commit()
flash('Workflow travaux enregistré.', '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)
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 ''
, '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,
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
# 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', '')
allowed = INTERVENTION_TRANSITIONS.get(old_status, set())
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))
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>/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))