From 5238b18e396b36b46548b38cce685faf608f9909 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 22 Aug 2026 13:10:39 +0000 Subject: [PATCH] fix(ui): corriger les erreurs P1 de l audit --- app_new/cleaning/routes.py | 36 +++++--- app_new/companies/routes.py | 15 +++- app_new/equipments/rooms.py | 90 +++++++++++++++---- app_new/interventions/crud.py | 69 +++++++++------ app_new/interventions/templates/detail.html | 11 +-- app_new/interventions/templates/index.html | 2 +- app_new/templates/base.html | 46 +++++----- app_new/templates/companies/new.html | 10 +-- app_new/templates/dashboard/index.html | 4 +- app_new/templates/equipments/room_form.html | 26 +++--- app_new/templates/interventions/detail.html | 5 +- tests/integration/test_ui_p1_regressions.py | 98 +++++++++++++++++++++ 12 files changed, 307 insertions(+), 105 deletions(-) create mode 100644 tests/integration/test_ui_p1_regressions.py diff --git a/app_new/cleaning/routes.py b/app_new/cleaning/routes.py index a1de643..39e35b4 100644 --- a/app_new/cleaning/routes.py +++ b/app_new/cleaning/routes.py @@ -12,7 +12,7 @@ from ..core.models import ( StockLocation, StockLot, StockLotBalance, StockMovement, Staff, RequestProfile, StockInventory, StockInventoryLine, Equipment, MaterialAssignment, - ProductDocument, + ProductDocument, ReusableContainer, CleaningForecastConfig, ) from .services.stock import StockError, receive_stock, issue_stock, transfer_stock, transvasement, dilute from .services.forecast import forecast_product @@ -68,13 +68,21 @@ def product_new(): def product_edit(id): product = ProductGeneric.query.get_or_404(id) if request.method == "POST": + try: + minimum = Decimal(request.form.get("stock_minimum") or 0) + security = Decimal(request.form.get("stock_security") or 0) + daily = Decimal(request.form.get("forecast_daily_quantity") or 0) + except (ArithmeticError, ValueError): + flash("Les quantités doivent être numériques (ex. 12,50).", "danger") + return render_template("cleaning/product_form.html", product=product, + categories=ProductCategory.query.order_by(ProductCategory.name).all()), 400 product.name = (request.form.get("name") or product.name).strip() product.description = request.form.get("description") product.category_id = request.form.get("category_id") or None product.reference_unit = request.form.get("reference_unit") or product.reference_unit - product.stock_minimum = Decimal(request.form.get("stock_minimum") or 0) - product.stock_security = Decimal(request.form.get("stock_security") or 0) - product.forecast_daily_quantity = Decimal(request.form.get("forecast_daily_quantity") or 0) + product.stock_minimum = minimum + product.stock_security = security + product.forecast_daily_quantity = daily product.is_active = request.form.get("is_active") == "on" db.session.commit() flash("Produit générique mis à jour.", "success") @@ -140,7 +148,9 @@ def packaging_new(): @login_required def stock(): balances = StockLotBalance.query.join(StockLot).join(ProductGeneric).join(StockLocation).order_by(ProductGeneric.name, StockLot.expiry_date).all() - return render_template("cleaning/stock.html", balances=balances, locations=StockLocation.query.filter_by(is_active=True).order_by(StockLocation.name).all()) + return render_template("cleaning/stock.html", balances=balances, + locations=StockLocation.query.filter_by(is_active=True).order_by(StockLocation.name).all(), + current_date=date.today()) @cleaning_bp.route("/locations", methods=["GET", "POST"]) @login_required @@ -227,12 +237,18 @@ def forecast_config(): config = CleaningForecastConfig.query.filter_by(is_active=True).first() if request.method == "POST": if not config: config = CleaningForecastConfig(); db.session.add(config) - config.normal_coefficient = Decimal(request.form.get("normal_coefficient") or 1) - config.permanence_coefficient = Decimal(request.form.get("permanence_coefficient") or 0) - config.closed_coefficient = Decimal(request.form.get("closed_coefficient") or 0) - config.exceptional_coefficient = Decimal(request.form.get("exceptional_coefficient") or 1) + try: + config.normal_coefficient = Decimal(request.form.get("normal_coefficient") or 1) + config.permanence_coefficient = Decimal(request.form.get("permanence_coefficient") or 0) + config.closed_coefficient = Decimal(request.form.get("closed_coefficient") or 0) + config.exceptional_coefficient = Decimal(request.form.get("exceptional_coefficient") or 1) + except (ArithmeticError, ValueError): + db.session.rollback() + flash("Les coefficients doivent être numériques (entre 0 et 1).", "danger") + return render_template("cleaning/forecast_config.html", forecast_config=config, + values=request.form), 400 db.session.commit(); flash("Coefficients de prévision enregistrés.", "success"); return redirect(url_for("cleaning.forecast_config")) - return render_template("cleaning/forecast_config.html", config=config, + return render_template("cleaning/forecast_config.html", forecast_config=config, values={"normal_coefficient": config.normal_coefficient if config else 1, "permanence_coefficient": config.permanence_coefficient if config else Decimal("0.4"), "closed_coefficient": config.closed_coefficient if config else 0, diff --git a/app_new/companies/routes.py b/app_new/companies/routes.py index 1595859..8590c55 100644 --- a/app_new/companies/routes.py +++ b/app_new/companies/routes.py @@ -31,8 +31,12 @@ def detail(id): def create(): """Créer une entreprise.""" if request.method == 'POST': + name = (request.form.get('name') or '').strip() + if not name: + flash('Le nom de l’entreprise est obligatoire.', 'danger') + return render_template('companies/new.html', form_data=request.form), 400 company = Company( - name=request.form.get('name'), + name=name, address=request.form.get('address'), contact_phone=request.form.get('contact_phone') or request.form.get('phone'), contact_email=request.form.get('contact_email') or request.form.get('email'), @@ -40,8 +44,13 @@ def create(): specialty=request.form.get('specialty'), ) db.session.add(company) - db.session.commit() + try: + db.session.commit() + except Exception: + db.session.rollback() + flash('Impossible d’enregistrer cette entreprise. Vérifiez les données saisies.', 'danger') + return render_template('companies/new.html', form_data=request.form), 400 flash('Entreprise créée avec succès.', 'success') return redirect(url_for('companies.index')) - return render_template('companies/new.html') \ No newline at end of file + return render_template('companies/new.html') diff --git a/app_new/equipments/rooms.py b/app_new/equipments/rooms.py index ce0107e..f107cf2 100644 --- a/app_new/equipments/rooms.py +++ b/app_new/equipments/rooms.py @@ -10,6 +10,21 @@ from app_new.core.models.equipment import Equipment rooms_bp = Blueprint('rooms', __name__, template_folder='templates') +def _room_form_context(room=None): + """Return all reference data used by the room form. + + The building list is deliberately loaded independently from the zone + list. Previously the GET route only supplied ``zones`` which left the + required building select empty and made every room creation impossible. + """ + return { + 'room': room, + 'buildings': Building.query.order_by(Building.name).all(), + 'zones': Zone.query.join(Building).order_by(Building.name, Zone.name).all(), + 'room_types': RoomType.query.order_by(RoomType.name).all(), + } + + @rooms_bp.route('/') @login_required def index(): @@ -88,22 +103,48 @@ def detail(id): def create(): """Créer une salle.""" if request.method == 'POST': + name = (request.form.get('name') or '').strip() + building_id = request.form.get('building_id', type=int) + zone_id = request.form.get('zone_id', type=int) + building = db.session.get(Building, building_id) if building_id else None + zone = db.session.get(Zone, zone_id) if zone_id else None + errors = [] + if not name: + errors.append('Le nom de la salle est obligatoire.') + if not building: + errors.append('Sélectionnez un bâtiment valide.') + if zone and building and zone.building_id != building.id: + errors.append('La zone sélectionnée n’appartient pas au bâtiment choisi.') + if errors: + for error in errors: + flash(error, 'danger') + context = _room_form_context() + context['form_data'] = request.form + return render_template('equipments/room_form.html', **context), 400 room = Room( - name=request.form.get('name'), + name=name, code=request.form.get('code') or None, - zone_id=request.form.get('zone_id', type=int), - building_id=request.form.get('building_id', type=int), + zone_id=zone_id, + building_id=building.id, room_type_id=request.form.get('room_type_id', type=int), floor=request.form.get('floor', 0, type=int) ) db.session.add(room) - db.session.commit() + try: + db.session.commit() + except Exception: + db.session.rollback() + flash('Impossible d’enregistrer cette salle. Vérifiez les valeurs saisies.', 'danger') + context = _room_form_context() + context['form_data'] = request.form + return render_template('equipments/room_form.html', **context), 400 flash(f"Salle '{room.name}' créée.", 'success') return redirect(url_for('rooms.index')) - zones = Zone.query.join(Building).order_by(Building.name, Zone.name).all() - room_types = RoomType.query.order_by(RoomType.name).all() - return render_template('equipments/room_form.html', zones=zones, room_types=room_types) + context = _room_form_context() + context['form_data'] = {} + context['title'] = 'Nouvelle salle' + return render_template('equipments/room_form.html', **context) @rooms_bp.route('//edit', methods=['GET', 'POST']) @@ -113,20 +154,39 @@ def edit(id): room = Room.query.get_or_404(id) if request.method == 'POST': - room.name = request.form.get('name') + name = (request.form.get('name') or '').strip() + building_id = request.form.get('building_id', type=int) + zone_id = request.form.get('zone_id', type=int) + building = db.session.get(Building, building_id) if building_id else None + zone = db.session.get(Zone, zone_id) if zone_id else None + if not name or not building or (zone and zone.building_id != building.id): + flash('Le nom, le bâtiment et une zone cohérente sont obligatoires.', 'danger') + context = _room_form_context(room) + context['form_data'] = request.form + context['title'] = 'Modifier la salle' + return render_template('equipments/room_form.html', **context), 400 + room.name = name room.code = request.form.get('code') or None - room.zone_id = request.form.get('zone_id', type=int) - room.building_id = request.form.get('building_id', type=int) + room.zone_id = zone_id + room.building_id = building.id room.room_type_id = request.form.get('room_type_id', type=int) room.floor = request.form.get('floor', 0, type=int) - db.session.commit() + try: + db.session.commit() + except Exception: + db.session.rollback() + flash('Impossible d’enregistrer cette salle. Vérifiez les valeurs saisies.', 'danger') + context = _room_form_context(room) + context['form_data'] = request.form + context['title'] = 'Modifier la salle' + return render_template('equipments/room_form.html', **context), 400 flash(f"Salle '{room.name}' mise à jour.", 'success') return redirect(url_for('rooms.index')) - zones = Zone.query.join(Building).order_by(Building.name, Zone.name).all() - room_types = RoomType.query.order_by(RoomType.name).all() - return render_template('equipments/room_form.html', - room=room, zones=zones, room_types=room_types) + context = _room_form_context(room) + context['form_data'] = {} + context['title'] = 'Modifier la salle' + return render_template('equipments/room_form.html', **context) @rooms_bp.route('//delete', methods=['POST']) diff --git a/app_new/interventions/crud.py b/app_new/interventions/crud.py index 6773248..e8ce342 100644 --- a/app_new/interventions/crud.py +++ b/app_new/interventions/crud.py @@ -119,6 +119,24 @@ def index(): sort_dir=sort_dir) +def _render_intervention_form(prefilled=None, form_data=None, **extra): + """Render the intervention form consistently, including validation errors.""" + from ..core.models.user import User + return render_template( + 'interventions/new.html', + 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(), + statuses=INTERVENTION_STATUSES, + priorities=PRIORITIES, + workflow_types=WORKFLOW_TYPES, + prefilled=prefilled or {}, + form_data=form_data or {}, + **extra, + ) + + @interventions_bp.route('/new', methods=['GET', 'POST']) @login_required @permission_required('intervention.create') @@ -128,6 +146,10 @@ def create(): from ..outlook.models import OutlookMailInterpretation if request.method == 'POST': + title = (request.form.get('title') or '').strip() + if not title: + flash('Le titre de l’intervention est obligatoire.', 'danger') + return _render_intervention_form(form_data=request.form), 400 # 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 @@ -143,8 +165,15 @@ def create(): 'preventif': 'preventive', 'amelioratif': 'travaux', 'prevention': 'prevention', 'administratif': 'prevention', }.get(selected_type, 'corrective') + try: + 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 + except ValueError: + flash('La date ou l’heure saisie est invalide.', 'danger') + return _render_intervention_form(form_data=request.form), 400 intervention = Intervention( - title=request.form.get('title'), + title=title, description=request.form.get('description') or '', lot_id=request.form.get('lot_id') or None, equipment_id=equipment_id, @@ -157,9 +186,9 @@ def create(): 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 + scheduled_date=scheduled_date, + scheduled_start=scheduled_start, + scheduled_end=scheduled_end ) db.session.add(intervention) db.session.commit() @@ -329,28 +358,15 @@ def create(): 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') + return _render_intervention_form(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']) @@ -410,6 +426,7 @@ def detail(id): # de ``interventions/detail.html`` est conservée pour compatibilité. return render_template('interventions_module/detail.html', intervention=intervention, + documents=intervention.documents.all(), statuses=INTERVENTION_STATUSES, transitions=transitions, history=history, diff --git a/app_new/interventions/templates/detail.html b/app_new/interventions/templates/detail.html index f867cdc..f338ad7 100644 --- a/app_new/interventions/templates/detail.html +++ b/app_new/interventions/templates/detail.html @@ -570,9 +570,10 @@
- {% if intervention.documents.all() %} + {% set intervention_documents = documents|default([]) %} + {% if intervention_documents %}
- {% for doc in intervention.documents %} + {% for doc in intervention_documents %}
{% set ext = doc.filename|lower %} @@ -584,13 +585,13 @@
{{ doc.uploaded_at.strftime('%d/%m/%Y') if doc.uploaded_at else '—' }}
- + -
+ @@ -710,7 +711,7 @@
-{% for doc in intervention.documents %} +{% for doc in intervention_documents|default([]) %}