""" Parts Routes - GMAO Collège Gestion des pièces détachées """ from flask import Blueprint, render_template, request, flash, redirect, url_for from flask_login import login_required, current_user from app_new.extensions import db from ..core.models.company import Part, PartStockMovement def _record_movement(part, new_quantity, movement_type, reason): before = int(part.quantity or 0) after = int(new_quantity) if after < 0: raise ValueError('Le stock ne peut pas devenir négatif.') if after == before: return None movement = PartStockMovement( part=part, user_id=current_user.id, movement_type=movement_type, quantity_delta=after - before, quantity_before=before, quantity_after=after, reason=(reason or '').strip() or 'Ajustement manuel', ) part.quantity = after db.session.add(movement) return movement parts_bp = Blueprint('parts', __name__) @parts_bp.route('/') @login_required def index(): """Liste des pièces.""" page = request.args.get('page', 1, type=int) parts = Part.query.order_by(Part.name).paginate(page=page, per_page=20) return render_template('parts/index.html', parts=parts) @parts_bp.route('/') @login_required def detail(id): """Détail d'une pièce.""" part = Part.query.get_or_404(id) movements = part.stock_movements.order_by(PartStockMovement.created_at.desc()).limit(100).all() return render_template('parts/detail.html', part=part, movements=movements) @parts_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit(id): """Modifier une pièce.""" part = Part.query.get_or_404(id) if request.method == 'POST': part.name = request.form.get('name') part.reference = request.form.get('reference') try: _record_movement(part, request.form.get('quantity', type=int, default=0), 'correction', request.form.get('stock_reason')) except ValueError as exc: flash(str(exc), 'danger') return render_template('parts/form.html', part=part), 400 part.unit = request.form.get('unit') part.unit_price = request.form.get('unit_price', type=float) part.supplier = request.form.get('supplier') db.session.commit() flash('Pièce modifiée avec succès.', 'success') return redirect(url_for('parts.detail', id=part.id)) return render_template('parts/form.html', part=part) @parts_bp.route('/new', methods=['GET', 'POST']) @login_required def create(): """Créer une pièce.""" if request.method == 'POST': quantity = max(0, request.form.get('quantity', type=int, default=0)) part = Part( name=request.form.get('name'), reference=request.form.get('reference'), quantity=0, unit=request.form.get('unit') ) part.unit_price = request.form.get('unit_price', type=float) part.supplier = request.form.get('supplier') db.session.add(part) db.session.flush() if quantity: _record_movement(part, quantity, 'entree', 'Stock initial') db.session.commit() flash('Pièce créée avec succès.', 'success') return redirect(url_for('parts.index')) return render_template('parts/new.html') @parts_bp.route('//movement', methods=['POST']) @login_required def movement(id): """Enregistre une entrée, une sortie ou un inventaire sans écraser l'historique.""" part = Part.query.get_or_404(id) movement_type = request.form.get('movement_type') amount = request.form.get('quantity', type=int) reason = (request.form.get('reason') or '').strip() if movement_type not in {'entree', 'sortie', 'inventaire'} or amount is None or amount < 0 or not reason: flash('Type, quantité positive et motif sont obligatoires.', 'danger') return redirect(url_for('parts.detail', id=id)) new_quantity = amount if movement_type == 'inventaire' else int(part.quantity or 0) + (amount if movement_type == 'entree' else -amount) try: _record_movement(part, new_quantity, movement_type, reason) db.session.commit() flash('Mouvement de stock enregistré.', 'success') except ValueError as exc: db.session.rollback() flash(str(exc), 'danger') return redirect(url_for('parts.detail', id=id))