"""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, EquipmentCategory 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 Building, Zone, Room from ..core.models.company import Service, Company from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES, WORKFLOW_TYPES, WORKFLOW_STATUS_ORDERS from ..core.authorization import has_permission, permission_required interventions_bp = Blueprint('interventions', __name__, template_folder='templates') @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', '') 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) 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_types=WORKFLOW_TYPES, 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 @permission_required('intervention.create') 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 # 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') 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', 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' if message: message.intervention_id = intervention.id 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' if mail: mail.intervention_id = intervention.id 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') } 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') 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, 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('/') @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() # Utiliser explicitement le template du module. Une ancienne copie globale # de ``interventions/detail.html`` est conservée pour compatibilité. return render_template('interventions_module/detail.html', intervention=intervention, statuses=INTERVENTION_STATUSES, transitions=transitions, history=history, workflow_status_order=WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective']), buildings=Building.query.order_by(Building.name).all(), zones=Zone.query.order_by(Zone.name).all(), 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(), 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('//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)) @interventions_bp.route('//location', methods=['POST']) @login_required def update_location(id): """Modifie la localisation en cascade bâtiment -> zone -> salle -> équipement.""" intervention = Intervention.query.get_or_404(id) building_id = request.form.get('building_id', type=int) zone_id = request.form.get('zone_id', type=int) 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)) 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)) 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)) @interventions_bp.route('//edit', methods=['GET', 'POST']) @login_required @permission_required('intervention.edit') 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 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 } 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, prefilled=prefilled, title="Modifier l'intervention") @interventions_bp.route('//status', methods=['POST']) @login_required @permission_required('intervention.change_status') 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') 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 in {'terminee', 'cloturee'} and not has_permission('intervention.close', current_user): flash('Vous ne disposez pas du droit de clôturer cette intervention.', '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 aujourd’hui : {reason}.', 'danger') return redirect(url_for('interventions.detail', id=intervention.id)) 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 db.session.commit() flash('Statut modifié.', 'success') return redirect(url_for('interventions.detail', id=intervention.id)) @interventions_bp.route('//refuse', methods=['POST']) @login_required @permission_required('intervention.reject') 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)) @interventions_bp.route('//comment', methods=['POST']) @login_required @permission_required('intervention.comment') 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('//soft-delete', methods=['POST']) @login_required @permission_required('intervention.delete') 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('//postpone', methods=['POST']) @login_required @permission_required('intervention.postpone') 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))