""" Wizard de création d'équipements par lot - GMAO Collège """ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app from flask_login import login_required from app_new.extensions import db from app_new.core.models.equipment import Equipment, EquipmentCategory from app_new.core.models.maintenance import Lot from app_new.core.models.college import Room, Building, Zone, RoomType wizard_bp = Blueprint('wizard', __name__, url_prefix='/wizard', template_folder='templates') @wizard_bp.route('/api/lots', methods=['POST']) @login_required def quick_create_lot(): """Crée un lot depuis un formulaire d'équipement et le lie à sa catégorie.""" data = request.get_json(silent=True) or {} name = (data.get('name') or '').strip() category_id = data.get('category_id') try: category_id = int(category_id) except (TypeError, ValueError): category_id = 0 category = db.session.get(EquipmentCategory, category_id) if not name or len(name) > 500 or not category: return jsonify({'error': 'Sélectionnez une catégorie et saisissez le nom du lot.'}), 400 existing = Lot.query.filter( Lot.category_id == category.id, db.func.lower(Lot.name) == name.lower(), ).first() if existing: return jsonify({ 'error': 'Ce lot existe déjà dans cette catégorie.', 'lot': {'id': existing.id, 'name': existing.name, 'category_id': category.id}, }), 409 lot = Lot(name=name, category_id=category.id, is_present=True) db.session.add(lot) db.session.commit() return jsonify({ 'success': True, 'lot': {'id': lot.id, 'name': lot.name, 'category_id': category.id}, }), 201 @wizard_bp.route('/equipments') @login_required def equipment_wizard(): """Page principale du wizard de création d'équipements.""" categories = EquipmentCategory.query.order_by(EquipmentCategory.name).all() buildings = Building.query.order_by(Building.name).all() room_types = RoomType.query.order_by(RoomType.name).all() # Récupérer les lots groupés par catégorie (relation directe) lots_by_category = {} for cat in categories: lots_by_category[cat.id] = [{'id': lot.id, 'name': lot.name} for lot in cat.lots] # Tous les lots pour fallback lots = Lot.query.order_by(Lot.name).all() housing_id = request.args.get('housing_id', type=int) selected_room_ids = [] if housing_id: from app_new.core.models.college import HousingUnit housing = HousingUnit.query.get_or_404(housing_id) selected_room_ids = [room.id for room in housing.rooms] return render_template('wizard/equipment_wizard.html', lots=lots, categories=categories, buildings=buildings, room_types=room_types, lots_by_category=lots_by_category, selected_room_ids=selected_room_ids) @wizard_bp.route('/equipments/bulk-create', methods=['POST']) @login_required def bulk_create_equipments(): """Crée plusieurs familles d'équipements en une transaction.""" data = request.get_json(silent=True) or {} room_ids = list(dict.fromkeys(data.get('room_ids') or [])) items = data.get('items') or [] rooms = Room.query.filter(Room.id.in_(room_ids)).all() if room_ids else [] if len(rooms) != len(room_ids): return jsonify({'error': 'Une ou plusieurs pièces sont invalides.'}), 400 if not rooms or not items: return jsonify({'error': 'Sélectionnez au moins une pièce et un équipement.'}), 400 created = [] try: for raw in items: name = (raw.get('name') or '').strip() category = db.session.get(EquipmentCategory, int(raw.get('category_id') or 0)) lot = db.session.get(Lot, int(raw.get('lot_id') or 0)) quantity = int(raw.get('quantity') or 1) mode = raw.get('mode', 'quantitatif') mobility = raw.get('mobility', 'non_precise') if not name or not category or not lot or quantity < 1 or quantity > 1000: raise ValueError('Une ligne contient des données invalides.') if lot.category_id != category.id: raise ValueError(f'Le lot « {lot.name} » ne correspond pas à la catégorie « {category.name} ».') root = Equipment( name=name, category_id=category.id, lot_id=lot.id, is_group=True, quantity=quantity * len(rooms), mobility=mobility, tracked_individually=(mode == 'individuel'), status='en_service', ) db.session.add(root) db.session.flush() created.append(root) for room in rooms: group = Equipment( name=name, parent_id=root.id, room_id=room.id, is_group=True, quantity=quantity, mobility=mobility, status='en_service', tracked_individually=(mode == 'individuel'), ) db.session.add(group) db.session.flush() created.append(group) if mode == 'individuel': for number in range(1, quantity + 1): unit = Equipment( name=f'{name} #{number}', parent_id=group.id, room_id=room.id, is_group=False, quantity=1, mobility=mobility, tracked_individually=True, individual_number=number, status='en_service', position=f'{name} n°{number}', ) db.session.add(unit) created.append(unit) # Valider d'abord le patrimoine. La génération des échéances est # volontairement découplée : une anomalie dans un lot ne doit jamais # annuler les équipements déjà créés. db.session.commit() task_targets = [eq for eq in created if (not eq.is_group or (eq.parent_id and not eq.tracked_individually))] task_errors = [] for equipment in task_targets: try: equipment.create_scheduled_tasks_from_lot(commit=True) except Exception as exc: db.session.rollback() message = f"Équipement {equipment.id} ({equipment.name}) : {str(exc)[:500]}" task_errors.append(message) current_app.logger.exception('Échec génération tâche préventive - %s', message) try: from app_new.watchdog_logger import log_watchdog log_watchdog('wizard_equipements', message, level='error') except Exception: pass return jsonify({'success': True, 'created_count': len(created), 'families': len(items), 'rooms': len(rooms), 'task_errors': task_errors}) except (TypeError, ValueError) as exc: db.session.rollback() return jsonify({'error': str(exc)}), 400 except Exception: db.session.rollback() current_app.logger.exception('Échec de la création massive du patrimoine') return jsonify({'error': 'La création a été annulée intégralement.'}), 500 @wizard_bp.route('/api/rooms/') @login_required def get_rooms(building_id): """Récupère les salles d'un bâtiment.""" rooms = Room.query.filter_by(building_id=building_id).order_by(Room.name).all() return jsonify([{ 'id': r.id, 'name': r.name, 'zone': r.zone.name if r.zone else None } for r in rooms]) @wizard_bp.route('/api/zones/') @login_required def get_zones(building_id): """Récupère les zones d'un bâtiment.""" zones = Zone.query.filter_by(building_id=building_id).order_by(Zone.name).all() return jsonify([{ 'id': z.id, 'name': z.name } for z in zones]) @wizard_bp.route('/api/lot_tasks/') @login_required def get_lot_tasks(lot_id): """Récupère les tâches préventives d'un lot.""" from app_new.core.models.maintenance import LotTask tasks = LotTask.query.filter_by(lot_id=lot_id).order_by(LotTask.num_tache).all() return jsonify([{ 'id': t.id, 'num_tache': t.num_tache, 'tache': t.tache, 'periodicite': t.periodicite, 'contrat': t.contrat, 'gestionnaire': t.gestionnaire, 'type_maintenance': t.type_maintenance } for t in tasks]) @wizard_bp.route('/equipments/create', methods=['POST']) @login_required def create_equipments(): """Crée les équipements selon les paramètres du wizard.""" try: data = request.get_json() lot_id = data.get('lot_id') category_id = data.get('category_id') equipment_name = data.get('name', '').strip() # Nom personnalisé parent_name = data.get('parent_name') create_parent = data.get('create_parent', False) building_id = data.get('building_id') zone_id = data.get('zone_id') rooms_data = data.get('rooms', []) # [{room_id, count}, ...] tracked_individually = data.get('tracked_individually', False) # Debug current_app.logger.info(f"Création équipements: tracked_individually={tracked_individually}, rooms={rooms_data}") if not lot_id or not category_id: return jsonify({'error': 'Lot et catégorie requis'}), 400 if not rooms_data: return jsonify({'error': 'Au moins une salle requise'}), 400 lot = Lot.query.get(lot_id) category = EquipmentCategory.query.get(category_id) # Nom de base: personnalisé ou catégorie base_name = equipment_name if equipment_name else category.name created_equipments = [] # Étape 1: Créer le parent global (si demandé) parent = None if create_parent and parent_name: parent = Equipment( name=parent_name, lot_id=lot_id, category_id=category_id, is_group=True, parent_id=None ) db.session.add(parent) db.session.flush() # Pour avoir l'ID # Étape 2: Pour chaque salle, créer le groupe et les individuels for room_data in rooms_data: room_id = room_data.get('room_id') count = room_data.get('count', 1) room = Room.query.get(room_id) if not room: continue if tracked_individually: # Avec suivi individuel: créer un groupe + des individus room_group = Equipment( name=f"{base_name} {room.name}", lot_id=lot_id if not parent else None, category_id=category_id if not parent else None, is_group=True, parent_id=parent.id if parent else None, room_id=room_id, quantity=1 ) db.session.add(room_group) db.session.flush() created_equipments.append({ 'type': 'group', 'name': room_group.name, 'room': room.name, 'quantity': 1 }) # Créer les équipements individuels if count > 1: for i in range(1, count + 1): individual = Equipment( name=f"{base_name} {i}", lot_id=lot_id, # Hériter du lot category_id=category_id, # Hériter de la catégorie is_group=False, tracked_individually=True, parent_id=room_group.id, room_id=room_id, position=f"{base_name} n°{i}" ) db.session.add(individual) created_equipments.append({ 'type': 'individual', 'name': individual.name, 'room': room.name }) else: # Sans suivi individuel: créer un seul équipement avec quantity equipment = Equipment( name=f"{base_name} {room.name}", lot_id=lot_id if not parent else None, category_id=category_id if not parent else None, is_group=False, parent_id=parent.id if parent else None, room_id=room_id, quantity=count ) db.session.add(equipment) db.session.flush() created_equipments.append({ 'type': 'equipment', 'name': equipment.name, 'room': room.name, 'quantity': count }) db.session.commit() # Créer les tâches planifiées pour tous les équipements créés for eq in created_equipments: # Récupérer l'équipement depuis la DB pour avoir son ID equipment = Equipment.query.filter_by(name=eq['name']).first() if equipment and equipment.effective_lot_id: equipment.create_scheduled_tasks_from_lot() return jsonify({ 'success': True, 'count': len(created_equipments), 'equipments': created_equipments }) except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @wizard_bp.route('/equipments/preview', methods=['POST']) @login_required def preview_equipments(): """Prévisualise les équipements qui seront créés.""" data = request.get_json() lot_id = data.get('lot_id') category_id = data.get('category_id') parent_name = data.get('parent_name') create_parent = data.get('create_parent', False) rooms_data = data.get('rooms', []) tracked_individually = data.get('tracked_individually', False) lot = Lot.query.get(lot_id) if lot_id else None category = EquipmentCategory.query.get(category_id) if category_id else None preview = [] # Parent global if create_parent and parent_name: preview.append({ 'level': 0, 'name': parent_name, 'type': 'Groupe parent', 'lot': lot.name if lot else '-', 'category': category.name if category else '-', 'room': '-' }) # Par salle for room_data in rooms_data: room_id = room_data.get('room_id') count = room_data.get('count', 1) room = Room.query.get(room_id) if not room: continue # Groupe salle level = 1 if create_parent else 0 preview.append({ 'level': level, 'name': f"{category.name} {room.name}", 'type': 'Groupe salle', 'lot': lot.name if lot else '-', 'category': category.name if category else '-', 'room': room.name }) # Individuels if tracked_individually: for i in range(1, count + 1): preview.append({ 'level': level + 1, 'name': f"{category.name} {i}", 'type': 'Individuel', 'lot': '(hérité)', 'category': '(hérité)', 'room': room.name }) return jsonify({ 'preview': preview, 'total': len(preview) })