fix(ui): corriger les erreurs P1 de l audit

This commit is contained in:
root 2026-08-22 13:10:39 +00:00
parent b116955299
commit 5238b18e39
12 changed files with 307 additions and 105 deletions

View file

@ -12,7 +12,7 @@ from ..core.models import (
StockLocation, StockLot, StockLotBalance, StockMovement, Staff, StockLocation, StockLot, StockLotBalance, StockMovement, Staff,
RequestProfile, StockInventory, StockInventoryLine, RequestProfile, StockInventory, StockInventoryLine,
Equipment, MaterialAssignment, Equipment, MaterialAssignment,
ProductDocument, ProductDocument, ReusableContainer, CleaningForecastConfig,
) )
from .services.stock import StockError, receive_stock, issue_stock, transfer_stock, transvasement, dilute from .services.stock import StockError, receive_stock, issue_stock, transfer_stock, transvasement, dilute
from .services.forecast import forecast_product from .services.forecast import forecast_product
@ -68,13 +68,21 @@ def product_new():
def product_edit(id): def product_edit(id):
product = ProductGeneric.query.get_or_404(id) product = ProductGeneric.query.get_or_404(id)
if request.method == "POST": 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.name = (request.form.get("name") or product.name).strip()
product.description = request.form.get("description") product.description = request.form.get("description")
product.category_id = request.form.get("category_id") or None product.category_id = request.form.get("category_id") or None
product.reference_unit = request.form.get("reference_unit") or product.reference_unit 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_minimum = minimum
product.stock_security = Decimal(request.form.get("stock_security") or 0) product.stock_security = security
product.forecast_daily_quantity = Decimal(request.form.get("forecast_daily_quantity") or 0) product.forecast_daily_quantity = daily
product.is_active = request.form.get("is_active") == "on" product.is_active = request.form.get("is_active") == "on"
db.session.commit() db.session.commit()
flash("Produit générique mis à jour.", "success") flash("Produit générique mis à jour.", "success")
@ -140,7 +148,9 @@ def packaging_new():
@login_required @login_required
def stock(): def stock():
balances = StockLotBalance.query.join(StockLot).join(ProductGeneric).join(StockLocation).order_by(ProductGeneric.name, StockLot.expiry_date).all() 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"]) @cleaning_bp.route("/locations", methods=["GET", "POST"])
@login_required @login_required
@ -227,12 +237,18 @@ def forecast_config():
config = CleaningForecastConfig.query.filter_by(is_active=True).first() config = CleaningForecastConfig.query.filter_by(is_active=True).first()
if request.method == "POST": if request.method == "POST":
if not config: config = CleaningForecastConfig(); db.session.add(config) if not config: config = CleaningForecastConfig(); db.session.add(config)
try:
config.normal_coefficient = Decimal(request.form.get("normal_coefficient") or 1) config.normal_coefficient = Decimal(request.form.get("normal_coefficient") or 1)
config.permanence_coefficient = Decimal(request.form.get("permanence_coefficient") or 0) config.permanence_coefficient = Decimal(request.form.get("permanence_coefficient") or 0)
config.closed_coefficient = Decimal(request.form.get("closed_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) 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")) 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, values={"normal_coefficient": config.normal_coefficient if config else 1,
"permanence_coefficient": config.permanence_coefficient if config else Decimal("0.4"), "permanence_coefficient": config.permanence_coefficient if config else Decimal("0.4"),
"closed_coefficient": config.closed_coefficient if config else 0, "closed_coefficient": config.closed_coefficient if config else 0,

View file

@ -31,8 +31,12 @@ def detail(id):
def create(): def create():
"""Créer une entreprise.""" """Créer une entreprise."""
if request.method == 'POST': if request.method == 'POST':
name = (request.form.get('name') or '').strip()
if not name:
flash('Le nom de lentreprise est obligatoire.', 'danger')
return render_template('companies/new.html', form_data=request.form), 400
company = Company( company = Company(
name=request.form.get('name'), name=name,
address=request.form.get('address'), address=request.form.get('address'),
contact_phone=request.form.get('contact_phone') or request.form.get('phone'), contact_phone=request.form.get('contact_phone') or request.form.get('phone'),
contact_email=request.form.get('contact_email') or request.form.get('email'), contact_email=request.form.get('contact_email') or request.form.get('email'),
@ -40,7 +44,12 @@ def create():
specialty=request.form.get('specialty'), specialty=request.form.get('specialty'),
) )
db.session.add(company) db.session.add(company)
try:
db.session.commit() db.session.commit()
except Exception:
db.session.rollback()
flash('Impossible denregistrer 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') flash('Entreprise créée avec succès.', 'success')
return redirect(url_for('companies.index')) return redirect(url_for('companies.index'))

View file

@ -10,6 +10,21 @@ from app_new.core.models.equipment import Equipment
rooms_bp = Blueprint('rooms', __name__, template_folder='templates') 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('/') @rooms_bp.route('/')
@login_required @login_required
def index(): def index():
@ -88,22 +103,48 @@ def detail(id):
def create(): def create():
"""Créer une salle.""" """Créer une salle."""
if request.method == 'POST': 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 nappartient 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( room = Room(
name=request.form.get('name'), name=name,
code=request.form.get('code') or None, code=request.form.get('code') or None,
zone_id=request.form.get('zone_id', type=int), zone_id=zone_id,
building_id=request.form.get('building_id', type=int), building_id=building.id,
room_type_id=request.form.get('room_type_id', type=int), room_type_id=request.form.get('room_type_id', type=int),
floor=request.form.get('floor', 0, type=int) floor=request.form.get('floor', 0, type=int)
) )
db.session.add(room) db.session.add(room)
try:
db.session.commit() db.session.commit()
except Exception:
db.session.rollback()
flash('Impossible denregistrer 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') flash(f"Salle '{room.name}' créée.", 'success')
return redirect(url_for('rooms.index')) return redirect(url_for('rooms.index'))
zones = Zone.query.join(Building).order_by(Building.name, Zone.name).all() context = _room_form_context()
room_types = RoomType.query.order_by(RoomType.name).all() context['form_data'] = {}
return render_template('equipments/room_form.html', zones=zones, room_types=room_types) context['title'] = 'Nouvelle salle'
return render_template('equipments/room_form.html', **context)
@rooms_bp.route('/<int:id>/edit', methods=['GET', 'POST']) @rooms_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@ -113,20 +154,39 @@ def edit(id):
room = Room.query.get_or_404(id) room = Room.query.get_or_404(id)
if request.method == 'POST': 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.code = request.form.get('code') or None
room.zone_id = request.form.get('zone_id', type=int) room.zone_id = zone_id
room.building_id = request.form.get('building_id', type=int) room.building_id = building.id
room.room_type_id = request.form.get('room_type_id', type=int) room.room_type_id = request.form.get('room_type_id', type=int)
room.floor = request.form.get('floor', 0, type=int) room.floor = request.form.get('floor', 0, type=int)
try:
db.session.commit() db.session.commit()
except Exception:
db.session.rollback()
flash('Impossible denregistrer 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') flash(f"Salle '{room.name}' mise à jour.", 'success')
return redirect(url_for('rooms.index')) return redirect(url_for('rooms.index'))
zones = Zone.query.join(Building).order_by(Building.name, Zone.name).all() context = _room_form_context(room)
room_types = RoomType.query.order_by(RoomType.name).all() context['form_data'] = {}
return render_template('equipments/room_form.html', context['title'] = 'Modifier la salle'
room=room, zones=zones, room_types=room_types) return render_template('equipments/room_form.html', **context)
@rooms_bp.route('/<int:id>/delete', methods=['POST']) @rooms_bp.route('/<int:id>/delete', methods=['POST'])

View file

@ -119,6 +119,24 @@ def index():
sort_dir=sort_dir) 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']) @interventions_bp.route('/new', methods=['GET', 'POST'])
@login_required @login_required
@permission_required('intervention.create') @permission_required('intervention.create')
@ -128,6 +146,10 @@ def create():
from ..outlook.models import OutlookMailInterpretation from ..outlook.models import OutlookMailInterpretation
if request.method == 'POST': if request.method == 'POST':
title = (request.form.get('title') or '').strip()
if not title:
flash('Le titre de lintervention est obligatoire.', 'danger')
return _render_intervention_form(form_data=request.form), 400
# Une intervention générale peut cibler une salle sans équipement. # Une intervention générale peut cibler une salle sans équipement.
equipment_value = request.form.get('equipment_id') equipment_value = request.form.get('equipment_id')
equipment_id = int(equipment_value) if equipment_value and equipment_value.isdigit() else None equipment_id = int(equipment_value) if equipment_value and equipment_value.isdigit() else None
@ -143,8 +165,15 @@ def create():
'preventif': 'preventive', 'amelioratif': 'travaux', 'preventif': 'preventive', 'amelioratif': 'travaux',
'prevention': 'prevention', 'administratif': 'prevention', 'prevention': 'prevention', 'administratif': 'prevention',
}.get(selected_type, 'corrective') }.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 lheure saisie est invalide.', 'danger')
return _render_intervention_form(form_data=request.form), 400
intervention = Intervention( intervention = Intervention(
title=request.form.get('title'), title=title,
description=request.form.get('description') or '', description=request.form.get('description') or '',
lot_id=request.form.get('lot_id') or None, lot_id=request.form.get('lot_id') or None,
equipment_id=equipment_id, equipment_id=equipment_id,
@ -157,9 +186,9 @@ def create():
workflow_type=workflow_type if workflow_type in WORKFLOW_TYPES else 'corrective', workflow_type=workflow_type if workflow_type in WORKFLOW_TYPES else 'corrective',
notes=request.form.get('notes') or '', notes=request.form.get('notes') or '',
requester_name=requester_name, 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_date=scheduled_date,
scheduled_start=datetime.strptime(request.form.get('scheduled_start'), '%H:%M').time() if request.form.get('scheduled_start') else None, scheduled_start=scheduled_start,
scheduled_end=datetime.strptime(request.form.get('scheduled_end'), '%H:%M').time() if request.form.get('scheduled_end') else None scheduled_end=scheduled_end
) )
db.session.add(intervention) db.session.add(intervention)
db.session.commit() db.session.commit()
@ -329,20 +358,7 @@ def create():
room_id = room.id room_id = room.id
room_match = room room_match = room
lots = Lot.query.order_by(Lot.name).all() return _render_intervention_form(prefilled=prefilled,
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_id=equipment_id,
equipment_match=equipment_match, equipment_match=equipment_match,
room_id=room_id, room_id=room_id,
@ -410,6 +426,7 @@ def detail(id):
# de ``interventions/detail.html`` est conservée pour compatibilité. # de ``interventions/detail.html`` est conservée pour compatibilité.
return render_template('interventions_module/detail.html', return render_template('interventions_module/detail.html',
intervention=intervention, intervention=intervention,
documents=intervention.documents.all(),
statuses=INTERVENTION_STATUSES, statuses=INTERVENTION_STATUSES,
transitions=transitions, transitions=transitions,
history=history, history=history,

View file

@ -570,9 +570,10 @@
</button> </button>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if intervention.documents.all() %} {% set intervention_documents = documents|default([]) %}
{% if intervention_documents %}
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for doc in intervention.documents %} {% for doc in intervention_documents %}
<div class="list-group-item d-flex justify-content-between align-items-center px-0"> <div class="list-group-item d-flex justify-content-between align-items-center px-0">
<div> <div>
{% set ext = doc.filename|lower %} {% set ext = doc.filename|lower %}
@ -584,13 +585,13 @@
<br><small class="text-muted">{{ doc.uploaded_at.strftime('%d/%m/%Y') if doc.uploaded_at else '—' }}</small> <br><small class="text-muted">{{ doc.uploaded_at.strftime('%d/%m/%Y') if doc.uploaded_at else '—' }}</small>
</div> </div>
<div> <div>
<a href="{{ url_for('documents.download_intervention', doc_id=doc.id) }}" class="btn btn-sm btn-outline-secondary" title="Télécharger"> <a href="{{ url_for('documents.download_intervention_document', doc_id=doc.id) }}" class="btn btn-sm btn-outline-secondary" title="Télécharger">
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
</a> </a>
<button type="button" class="btn btn-sm btn-outline-primary" title="Modifier la description" data-bs-toggle="modal" data-bs-target="#editInterventionDocumentModal{{ doc.id }}"> <button type="button" class="btn btn-sm btn-outline-primary" title="Modifier la description" data-bs-toggle="modal" data-bs-target="#editInterventionDocumentModal{{ doc.id }}">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</button> </button>
<form method="POST" action="{{ url_for('documents.delete_intervention', doc_id=doc.id) }}" style="display:inline;" onsubmit="return confirm('Supprimer ce document ?')"> <form method="POST" action="{{ url_for('documents.delete_intervention_document', doc_id=doc.id) }}" style="display:inline;" onsubmit="return confirm('Supprimer ce document ?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Supprimer"> <button type="submit" class="btn btn-sm btn-outline-danger" title="Supprimer">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
@ -710,7 +711,7 @@
</div> </div>
<!-- Modals Edit Documents Intervention --> <!-- Modals Edit Documents Intervention -->
{% for doc in intervention.documents %} {% for doc in intervention_documents|default([]) %}
<div class="modal fade" id="editInterventionDocumentModal{{ doc.id }}" tabindex="-1"> <div class="modal fade" id="editInterventionDocumentModal{{ doc.id }}" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

View file

@ -131,7 +131,7 @@
</div> </div>
<div class="fw-bold text-dark">#{{ interv.id }} — {{ interv.title[:80] }}{% if interv.title|length > 80 %}…{% endif %}</div> <div class="fw-bold text-dark">#{{ interv.id }} — {{ interv.title[:80] }}{% if interv.title|length > 80 %}…{% endif %}</div>
<div class="text-muted" style="font-size:0.8rem;"> <div class="text-muted" style="font-size:0.8rem;">
<i class="bi bi-hdd"></i> {{ interv.equipment.name[:40] if interv.equipment else (interv.room.name if interv.room else 'Localisation générale') }}{% if interv.equipment and interv.equipment.name|length > 40 %}…{% endif %} <i class="bi bi-hdd"></i> {% if interv.equipment %}{{ interv.equipment.name[:40] }}{% if interv.equipment.name|length > 40 %}…{% endif %}{% elif interv.room %}{{ interv.room.name }}{% else %}Localisation générale{% endif %}
{% if interv.assigned_to %} {% if interv.assigned_to %}
· <i class="bi bi-person-badge"></i> {{ interv.assigned_to.full_name }} · <i class="bi bi-person-badge"></i> {{ interv.assigned_to.full_name }}
{% endif %} {% endif %}

View file

@ -129,10 +129,10 @@
</button> </button>
<!-- Notifications système : interprétations et watchdogs --> <!-- Notifications système : interprétations et watchdogs -->
<a href="/logs/" class="btn btn-sm btn-outline-light me-2 position-relative" title="Notifications système et logs watchdogs" aria-label="Notifications système et logs watchdogs"> {% if has_permission('audit.view') %}<a href="/logs/" class="btn btn-sm btn-outline-light me-2 position-relative" title="Notifications système et logs watchdogs" aria-label="Notifications système et logs watchdogs">
<i class="bi bi-journal-text"></i> <i class="bi bi-journal-text"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="notif-count" style="font-size:0.6rem; display:none;">0</span> <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="notif-count" style="font-size:0.6rem; display:none;">0</span>
</a> </a>{% endif %}
<!-- Utilisateur --> <!-- Utilisateur -->
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
@ -163,67 +163,67 @@
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-group-label d-lg-none mt-2">Principal</li> <li class="nav-group-label d-lg-none mt-2">Principal</li>
<li class="nav-item"> {% if has_permission('dashboard.view') %}<li class="nav-item">
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="{{ url_for('dashboard.index') }}"> <a class="nav-link {% if request.path == '/' %}active{% endif %}" href="{{ url_for('dashboard.index') }}">
<i class="bi bi-speedometer2"></i> <span>Tableau de bord</span> <i class="bi bi-speedometer2"></i> <span>Tableau de bord</span>
</a> </a>
</li> </li>{% endif %}
<li class="nav-item"> {% if has_permission('integration.outlook.view') or has_permission('integration.ent.view') %}<li class="nav-item">
<a class="nav-link {% if '/messagerie' in request.path %}active{% endif %}" href="/messagerie"> <a class="nav-link {% if '/messagerie' in request.path %}active{% endif %}" href="/messagerie">
<i class="bi bi-envelope-paper"></i> <span>Messagerie</span> <i class="bi bi-envelope-paper"></i> <span>Messagerie</span>
{% if pending_interpretations_count is defined and pending_interpretations_count > 0 %} {% if pending_interpretations_count is defined and pending_interpretations_count > 0 %}
<span class="badge bg-danger ms-1">{{ pending_interpretations_count }}</span> <span class="badge bg-danger ms-1">{{ pending_interpretations_count }}</span>
{% endif %} {% endif %}
</a> </a>
</li> </li>{% endif %}
<li class="nav-item dropdown"> {% if has_permission('intervention.view') or has_permission('planning.view') or has_permission('prevention.view') %}<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if '/interventions' in request.path or '/planning' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle {% if '/interventions' in request.path or '/planning' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-wrench-adjustable"></i> <span>Interventions</span> <i class="bi bi-wrench-adjustable"></i> <span>Interventions</span>
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;"> <ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;">
<li><a class="dropdown-item" href="{{ url_for('interventions.index') }}"><i class="bi bi-list-ul"></i> Toutes les interventions</a></li> {% if has_permission('intervention.view') %}<li><a class="dropdown-item" href="{{ url_for('interventions.index') }}"><i class="bi bi-list-ul"></i> Toutes les interventions</a></li>{% endif %}
<li><a class="dropdown-item" href="{{ url_for('planning.index') }}"><i class="bi bi-calendar-check"></i> Planning unifié</a></li> {% if has_permission('planning.view') %}<li><a class="dropdown-item" href="{{ url_for('planning.index') }}"><i class="bi bi-calendar-check"></i> Planning unifié</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.tasks') }}"><i class="bi bi-list-check"></i> Tâches préventives</a></li> <li><a class="dropdown-item" href="{{ url_for('planning.tasks') }}"><i class="bi bi-list-check"></i> Tâches préventives</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.scheduled') }}"><i class="bi bi-calendar-week"></i> Tâches planifiées</a></li> <li><a class="dropdown-item" href="{{ url_for('planning.scheduled') }}"><i class="bi bi-calendar-week"></i> Tâches planifiées</a></li>{% endif %}
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('planning.meters') }}"><i class="bi bi-speedometer2"></i> Compteurs</a></li> {% if has_permission('planning.view') %}<li><a class="dropdown-item" href="{{ url_for('planning.meters') }}"><i class="bi bi-speedometer2"></i> Compteurs</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.consumables') }}"><i class="bi bi-box"></i> Consommables</a></li> <li><a class="dropdown-item" href="{{ url_for('planning.consumables') }}"><i class="bi bi-box"></i> Consommables</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.availability') }}"><i class="bi bi-clock"></i> Disponibilités</a></li> <li><a class="dropdown-item" href="{{ url_for('planning.availability') }}"><i class="bi bi-clock"></i> Disponibilités</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.time_tracking') }}"><i class="bi bi-clock-history"></i> Horaires et suivi annuel</a></li> <li><a class="dropdown-item" href="{{ url_for('planning.time_tracking') }}"><i class="bi bi-clock-history"></i> Horaires et suivi annuel</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.admin_tasks') }}"><i class="bi bi-clipboard"></i> Tâches administratives</a></li> <li><a class="dropdown-item" href="{{ url_for('planning.admin_tasks') }}"><i class="bi bi-clipboard"></i> Tâches administratives</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.rules') }}"><i class="bi bi-shield-check"></i> Règles d'accès</a></li> <li><a class="dropdown-item" href="{{ url_for('planning.rules') }}"><i class="bi bi-shield-check"></i> Règles d'accès</a></li>{% endif %}
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('prevention.index') }}"><i class="bi bi-shield-check"></i> Prévention / DUERP</a></li> {% if has_permission('prevention.view') %}<li><a class="dropdown-item" href="{{ url_for('prevention.index') }}"><i class="bi bi-shield-check"></i> Prévention / DUERP</a></li>
<li><a class="dropdown-item" href="{{ url_for('trainings.index') }}"><i class="bi bi-mortarboard"></i> Formations</a></li> <li><a class="dropdown-item" href="{{ url_for('trainings.index') }}"><i class="bi bi-mortarboard"></i> Formations</a></li>
<li><a class="dropdown-item" href="{{ url_for('trainings.my_trainings') }}"><i class="bi bi-calendar-check"></i> Mes formations</a></li> <li><a class="dropdown-item" href="{{ url_for('trainings.my_trainings') }}"><i class="bi bi-calendar-check"></i> Mes formations</a></li>{% endif %}
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('exports.export_interventions') }}"><i class="bi bi-file-earmark-pdf"></i> Export interventions</a></li> <li><a class="dropdown-item" href="{{ url_for('exports.export_interventions') }}"><i class="bi bi-file-earmark-pdf"></i> Export interventions</a></li>
<li><a class="dropdown-item" href="{{ url_for('exports.export_costs_csv') }}"><i class="bi bi-cash-stack"></i> Export coûts et stock</a></li> <li><a class="dropdown-item" href="{{ url_for('exports.export_costs_csv') }}"><i class="bi bi-cash-stack"></i> Export coûts et stock</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('scheduler.index') }}"><i class="bi bi-calendar-week"></i> Génération automatique</a></li> <li><a class="dropdown-item" href="{{ url_for('scheduler.index') }}"><i class="bi bi-calendar-week"></i> Génération automatique</a></li>
</ul> </ul>
</li> </li>{% endif %}
<li class="nav-item dropdown"> {% if has_permission('patrimoine.view') or has_permission('contract.view') or has_permission('stock.view') %}<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if '/equipments' in request.path or '/companies' in request.path or '/parts' in request.path or '/lots' in request.path or '/services' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle {% if '/equipments' in request.path or '/companies' in request.path or '/parts' in request.path or '/lots' in request.path or '/services' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-pc-display"></i> <span>Équipements</span> <i class="bi bi-pc-display"></i> <span>Équipements</span>
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;"> <ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;">
<li><a class="dropdown-item" href="{{ url_for('equipments.index') }}"><i class="bi bi-pc-display"></i> Tous les équipements</a></li> {% if has_permission('patrimoine.view') %}<li><a class="dropdown-item" href="{{ url_for('equipments.index') }}"><i class="bi bi-pc-display"></i> Tous les équipements</a></li>
<li><a class="dropdown-item" href="{{ url_for('equipments.categories') }}"><i class="bi bi-tags"></i> Catégories</a></li> <li><a class="dropdown-item" href="{{ url_for('equipments.categories') }}"><i class="bi bi-tags"></i> Catégories</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('buildings.index') }}"><i class="bi bi-buildings"></i> Bâtiments</a></li> <li><a class="dropdown-item" href="{{ url_for('buildings.index') }}"><i class="bi bi-buildings"></i> Bâtiments</a></li>
<li><a class="dropdown-item" href="{{ url_for('zones.index') }}"><i class="bi bi-diagram-3"></i> Zones</a></li> <li><a class="dropdown-item" href="{{ url_for('zones.index') }}"><i class="bi bi-diagram-3"></i> Zones</a></li>
<li><a class="dropdown-item" href="{{ url_for('rooms.index') }}"><i class="bi bi-door-open"></i> Salles</a></li> <li><a class="dropdown-item" href="{{ url_for('rooms.index') }}"><i class="bi bi-door-open"></i> Salles</a></li>
<li><a class="dropdown-item" href="{{ url_for('housing.index') }}"><i class="bi bi-house-door"></i> Logements de fonction</a></li> <li><a class="dropdown-item" href="{{ url_for('housing.index') }}"><i class="bi bi-house-door"></i> Logements de fonction</a></li>
<li><a class="dropdown-item" href="{{ url_for('room_types.index') }}"><i class="bi bi-tags"></i> Types de salles</a></li> <li><a class="dropdown-item" href="{{ url_for('room_types.index') }}"><i class="bi bi-tags"></i> Types de salles</a></li>{% endif %}
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('companies.index') }}"><i class="bi bi-building"></i> Entreprises</a></li> {% if has_permission('contract.view') %}<li><a class="dropdown-item" href="{{ url_for('companies.index') }}"><i class="bi bi-building"></i> Entreprises</a></li>
<li><a class="dropdown-item" href="{{ url_for('contracts.index') }}"><i class="bi bi-file-earmark-text"></i> Contrats</a></li> <li><a class="dropdown-item" href="{{ url_for('contracts.index') }}"><i class="bi bi-file-earmark-text"></i> Contrats</a></li>
<li><a class="dropdown-item" href="{{ url_for('lots.index') }}"><i class="bi bi-boxes"></i> Lots</a></li> <li><a class="dropdown-item" href="{{ url_for('lots.index') }}"><i class="bi bi-boxes"></i> Lots</a></li>
<li><a class="dropdown-item" href="{{ url_for('services.index') }}"><i class="bi bi-building-gear"></i> Services</a></li> <li><a class="dropdown-item" href="{{ url_for('services.index') }}"><i class="bi bi-building-gear"></i> Services</a></li>
<li><a class="dropdown-item" href="{{ url_for('parts.index') }}"><i class="bi bi-box-seam"></i> Pièces</a></li> <li><a class="dropdown-item" href="{{ url_for('parts.index') }}"><i class="bi bi-box-seam"></i> Pièces</a></li>{% endif %}
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('cleaning.dashboard') }}"><i class="bi bi-droplet-half"></i> Matériel &amp; Entretien</a></li> {% if has_permission('stock.view') %}<li><a class="dropdown-item" href="{{ url_for('cleaning.dashboard') }}"><i class="bi bi-droplet-half"></i> Matériel &amp; Entretien</a></li>
<li><a class="dropdown-item" href="{{ url_for('cleaning.products') }}"><i class="bi bi-flask"></i> Produits génériques</a></li> <li><a class="dropdown-item" href="{{ url_for('cleaning.products') }}"><i class="bi bi-flask"></i> Produits génériques</a></li>
<li><a class="dropdown-item" href="{{ url_for('cleaning.references') }}"><i class="bi bi-upc-scan"></i> Références / conditionnements</a></li> <li><a class="dropdown-item" href="{{ url_for('cleaning.references') }}"><i class="bi bi-upc-scan"></i> Références / conditionnements</a></li>
<li><a class="dropdown-item" href="{{ url_for('cleaning.documents') }}"><i class="bi bi-file-earmark-text"></i> FDS / fiches techniques</a></li> <li><a class="dropdown-item" href="{{ url_for('cleaning.documents') }}"><i class="bi bi-file-earmark-text"></i> FDS / fiches techniques</a></li>
@ -233,14 +233,14 @@
<li><a class="dropdown-item" href="{{ url_for('cleaning.transvasement_page') }}"><i class="bi bi-droplet"></i> Transvasements / dilutions</a></li> <li><a class="dropdown-item" href="{{ url_for('cleaning.transvasement_page') }}"><i class="bi bi-droplet"></i> Transvasements / dilutions</a></li>
<li><a class="dropdown-item" href="{{ url_for('cleaning.request_profiles') }}"><i class="bi bi-printer"></i> Fiches papier</a></li> <li><a class="dropdown-item" href="{{ url_for('cleaning.request_profiles') }}"><i class="bi bi-printer"></i> Fiches papier</a></li>
<li><a class="dropdown-item" href="{{ url_for('cleaning.material') }}"><i class="bi bi-tools"></i> Matériel durable</a></li> <li><a class="dropdown-item" href="{{ url_for('cleaning.material') }}"><i class="bi bi-tools"></i> Matériel durable</a></li>
<li><a class="dropdown-item" href="{{ url_for('cleaning.forecast_config') }}"><i class="bi bi-sliders"></i> Prévision / permanences</a></li> {% if has_permission('stock.configure') %}<li><a class="dropdown-item" href="{{ url_for('cleaning.forecast_config') }}"><i class="bi bi-sliders"></i> Prévision / permanences</a></li>{% endif %}
<li><a class="dropdown-item" href="{{ url_for('meters.index') }}"><i class="bi bi-graph-up-arrow"></i> Historique des compteurs</a></li> <li><a class="dropdown-item" href="{{ url_for('meters.index') }}"><i class="bi bi-graph-up-arrow"></i> Historique des compteurs</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('equipments.to_trash') }}"><i class="bi bi-trash"></i> À jeter</a></li> <li><a class="dropdown-item" href="{{ url_for('equipments.to_trash') }}"><i class="bi bi-trash"></i> À jeter</a></li>
<li><a class="dropdown-item" href="{{ url_for('equipments.lifecycle_dashboard') }}"><i class="bi bi-activity"></i> Cycle de vie</a></li> <li><a class="dropdown-item" href="{{ url_for('equipments.lifecycle_dashboard') }}"><i class="bi bi-activity"></i> Cycle de vie</a></li>
<li><a class="dropdown-item" href="{{ url_for('equipments.trashed') }}"><i class="bi bi-archive"></i> Anciens</a></li> <li><a class="dropdown-item" href="{{ url_for('equipments.trashed') }}"><i class="bi bi-archive"></i> Anciens</a></li>
</ul> </ul>
</li> </li>{% endif %}{% endif %}
{% if current_user.is_admin() %} {% if current_user.is_admin() %}
<li class="nav-group-label d-lg-none mt-2">Administration</li> <li class="nav-group-label d-lg-none mt-2">Administration</li>

View file

@ -10,11 +10,11 @@
<div class="card"><div class="card-body"> <div class="card"><div class="card-body">
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3"><label class="form-label">Nom *</label><input type="text" name="name" class="form-control" required></div> <div class="mb-3"><label class="form-label">Nom *</label><input type="text" name="name" class="form-control" value="{{ form_data.get('name', '') if form_data is defined else '' }}" required></div>
<div class="mb-3"><label class="form-label">Adresse</label><textarea name="address" class="form-control" rows="2"></textarea></div> <div class="mb-3"><label class="form-label">Adresse</label><textarea name="address" class="form-control" rows="2">{{ form_data.get('address', '') if form_data is defined else '' }}</textarea></div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"><label class="form-label">Téléphone</label><input type="text" name="phone" class="form-control"></div> <div class="col-md-6"><label class="form-label">Téléphone</label><input type="text" name="phone" class="form-control" value="{{ form_data.get('phone', '') if form_data is defined else '' }}"></div>
<div class="col-md-6"><label class="form-label">Email</label><input type="email" name="email" class="form-control"></div> <div class="col-md-6"><label class="form-label">Email</label><input type="email" name="email" class="form-control" value="{{ form_data.get('email', '') if form_data is defined else '' }}"></div>
</div> </div>
<div class="text-end"><button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Créer</button></div> <div class="text-end"><button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Créer</button></div>
</form> </form>

View file

@ -321,7 +321,7 @@
<div class="me-2" style="min-width:0;"> <div class="me-2" style="min-width:0;">
<a href="{{ url_for('interventions.detail', id=interv.id) }}" class="fw-bold text-decoration-none text-truncate d-block">{{ interv.title }}</a> <a href="{{ url_for('interventions.detail', id=interv.id) }}" class="fw-bold text-decoration-none text-truncate d-block">{{ interv.title }}</a>
<small class="text-muted d-block text-truncate"> <small class="text-muted d-block text-truncate">
<i class="bi bi-pc-display"></i> {{ interv.equipment.name }} <i class="bi bi-pc-display"></i> {% if interv.equipment %}{{ interv.equipment.name }}{% elif interv.room %}{{ interv.room.name }}{% else %}Aucune localisation{% endif %}
{% if interv.frequency_days %} {% if interv.frequency_days %}
· <i class="bi bi-arrow-repeat"></i> {{ frequency_labels.get(interv.frequency_days, interv.frequency_days ~ "j") }} · <i class="bi bi-arrow-repeat"></i> {{ frequency_labels.get(interv.frequency_days, interv.frequency_days ~ "j") }}
{% endif %} {% endif %}
@ -492,7 +492,7 @@
{% else %}<span class="badge bg-info">Préventif</span>{% endif %} {% else %}<span class="badge bg-info">Préventif</span>{% endif %}
</td> </td>
<td><span class="badge {{ interv.status|status_badge }}">{{ interv.status|format_status }}</span></td> <td><span class="badge {{ interv.status|status_badge }}">{{ interv.status|format_status }}</span></td>
<td>{{ interv.equipment.name[:30] }}{% if interv.equipment.name|length > 30 %}…{% endif %}</td> <td>{% if interv.equipment %}{{ interv.equipment.name[:30] }}{% if interv.equipment.name|length > 30 %}…{% endif %}{% elif interv.room %}{{ interv.room.name }}{% else %}Aucun équipement{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -1,8 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ title }} — GMAO Collège{% endblock %} {% block title %}{{ title|default('Salle') }} — GMAO Collège{% endblock %}
{% block content %} {% block content %}
<h1 class="mb-4">{{ title }}</h1> <h1 class="mb-4">{{ title|default('Salle') }}</h1>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<form method="POST"> <form method="POST">
@ -10,17 +10,17 @@
<div class="row"> <div class="row">
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label class="form-label">Nom *</label> <label class="form-label">Nom *</label>
<input type="text" name="name" class="form-control" value="{{ room.name if room else '' }}" required> <input type="text" name="name" class="form-control" value="{{ form_data.get('name', room.name if room else '') }}" required>
</div> </div>
<div class="col-md-2 mb-3"> <div class="col-md-2 mb-3">
<label class="form-label">Code</label> <label class="form-label">Code</label>
<input type="text" name="code" class="form-control" value="{{ room.code if room else '' }}"> <input type="text" name="code" class="form-control" value="{{ form_data.get('code', room.code if room else '') }}">
</div> </div>
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label">Bâtiment *</label> <label class="form-label">Bâtiment *</label>
<select name="building_id" class="form-select" required id="building-select"> <select name="building_id" class="form-select" required id="building-select">
{% for b in buildings %} {% for b in buildings %}
<option value="{{ b.id }}" {% if room and room.building_id == b.id %}selected{% endif %}>{{ b.name }}</option> <option value="{{ b.id }}" {% if form_data.get('building_id', room.building_id if room else '')|string == b.id|string %}selected{% endif %}>{{ b.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -29,7 +29,7 @@
<select name="zone_id" class="form-select" id="zone-select"> <select name="zone_id" class="form-select" id="zone-select">
<option value="">— Aucune —</option> <option value="">— Aucune —</option>
{% for z in zones %} {% for z in zones %}
<option value="{{ z.id }}" data-building="{{ z.building_id }}" {% if room and room.zone_id == z.id %}selected{% endif %}>{{ z.name }}</option> <option value="{{ z.id }}" data-building="{{ z.building_id }}" {% if form_data.get('zone_id', room.zone_id if room else '')|string == z.id|string %}selected{% endif %}>{{ z.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -38,11 +38,11 @@
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label class="form-label">Étage</label> <label class="form-label">Étage</label>
<select name="floor" class="form-select"> <select name="floor" class="form-select">
<option value="-1" {% if room and room.floor == -1 %}selected{% endif %}>Sous-sol</option> <option value="-1" {% if form_data.get('floor', room.floor if room else 0)|string == '-1' %}selected{% endif %}>Sous-sol</option>
<option value="0" {% if room and room.floor == 0 %}selected{% endif %}>RDC</option> <option value="0" {% if form_data.get('floor', room.floor if room else 0)|string == '0' %}selected{% endif %}>RDC</option>
<option value="1" {% if room and room.floor == 1 %}selected{% endif %}>1er étage</option> <option value="1" {% if form_data.get('floor', room.floor if room else 0)|string == '1' %}selected{% endif %}>1er étage</option>
<option value="2" {% if room and room.floor == 2 %}selected{% endif %}>2ème étage</option> <option value="2" {% if form_data.get('floor', room.floor if room else 0)|string == '2' %}selected{% endif %}>2ème étage</option>
<option value="3" {% if room and room.floor == 3 %}selected{% endif %}>3ème étage</option> <option value="3" {% if form_data.get('floor', room.floor if room else 0)|string == '3' %}selected{% endif %}>3ème étage</option>
</select> </select>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
@ -50,7 +50,7 @@
<select name="room_type_id" class="form-select"> <select name="room_type_id" class="form-select">
<option value="">— Aucun —</option> <option value="">— Aucun —</option>
{% for rt in room_types %} {% for rt in room_types %}
<option value="{{ rt.id }}" {% if room and room.room_type_id == rt.id %}selected{% endif %}>{{ rt.name }}{% if rt.is_teaching %} (enseignement){% endif %}</option> <option value="{{ rt.id }}" {% if form_data.get('room_type_id', room.room_type_id if room else '')|string == rt.id|string %}selected{% endif %}>{{ rt.name }}{% if rt.is_teaching %} (enseignement){% endif %}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View file

@ -494,9 +494,10 @@
</button> </button>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if intervention.documents.all() %} {% set intervention_documents = documents|default([]) %}
{% if intervention_documents %}
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for doc in intervention.documents %} {% for doc in intervention_documents %}
<div class="list-group-item d-flex justify-content-between align-items-center px-0"> <div class="list-group-item d-flex justify-content-between align-items-center px-0">
<div> <div>
{% set ext = doc.filename|lower %} {% set ext = doc.filename|lower %}

View file

@ -0,0 +1,98 @@
"""Non-regression tests for the P1 issues found during the UI audit.
The audit VM is not available in this environment; these tests exercise the
same routes against the local Flask test database.
"""
from app_new.extensions import db
from app_new.core.models.college import Building, Zone, Room
from app_new.core.models.maintenance import Intervention
from app_new.core.models.cleaning import ProductGeneric
def test_room_form_lists_buildings_and_rejects_cross_building_zone(authenticated_client, app):
authenticated_client.post('/equipments/buildings/new', data={'name': 'UI test bâtiment A'})
authenticated_client.post('/equipments/buildings/new', data={'name': 'UI test bâtiment B'})
with app.app_context():
building_a = Building.query.filter_by(name="UI test bâtiment A").one()
building_b = Building.query.filter_by(name="UI test bâtiment B").one()
zone_b = Zone(name="UI test zone B", building_id=building_b.id)
db.session.add(zone_b)
db.session.commit()
a_id, b_id, zone_id = building_a.id, building_b.id, zone_b.id
response = authenticated_client.get("/equipments/rooms/new")
assert response.status_code == 200
body = response.get_data(as_text=True)
assert "UI test bâtiment A" in body
assert "UI test bâtiment B" in body
response = authenticated_client.post("/equipments/rooms/new", data={
"name": "Salle incohérente",
"building_id": str(a_id),
"zone_id": str(zone_id),
})
assert response.status_code == 400
with app.app_context():
assert Room.query.filter_by(name="Salle incohérente").first() is None
def test_optional_location_is_safe_in_interventions(authenticated_client, app):
with app.app_context():
intervention = Intervention(title="UI intervention sans localisation")
db.session.add(intervention)
db.session.commit()
assert authenticated_client.get("/interventions/").status_code == 200
def test_optional_equipment_is_safe_on_dashboard(authenticated_client, app):
with app.app_context():
db.session.add(Intervention(title="UI dashboard sans équipement"))
db.session.commit()
assert authenticated_client.get("/").status_code == 200
def test_intervention_detail_template_exposes_document_actions():
template = open('app_new/interventions/templates/detail.html', encoding='utf-8').read()
assert 'documents.download_intervention_document' in template
assert 'documents.delete_intervention_document' in template
assert 'documents.upload_intervention' in template
def test_empty_intervention_and_company_are_validation_errors(authenticated_client):
assert authenticated_client.post("/interventions/new", data={}).status_code == 400
assert authenticated_client.post("/companies/new", data={}).status_code == 400
def test_invalid_product_quantity_is_validation_error(authenticated_client, app):
authenticated_client.post('/cleaning/products/new', data={
'name': 'UI produit quantité invalide',
'reference_unit': 'unité',
'stock_minimum': '0', 'stock_security': '0', 'forecast_daily_quantity': '0',
})
with app.app_context():
product = ProductGeneric.query.filter_by(name="UI produit quantité invalide").one()
product_id = product.id
response = authenticated_client.post(f"/cleaning/products/{product_id}/edit", data={
"name": "UI produit quantité invalide",
"stock_minimum": "pas-un-nombre",
"stock_security": "0",
"forecast_daily_quantity": "0",
})
assert response.status_code == 400
assert "numériques" in response.get_data(as_text=True)
def test_cleaning_pages_load_without_reference_data(authenticated_client, app):
# Some legacy RBAC tests intentionally alter the admin role. Restore only
# the permission required by this independent route smoke test.
with app.app_context():
from app_new.core.models.rbac import Role, Permission, RolePermission
role = Role.query.filter_by(slug='admin').first()
permission = Permission.query.filter_by(code='stock.view').first()
if role and permission and not RolePermission.query.filter_by(role_id=role.id, permission_id=permission.id).first():
db.session.add(RolePermission(role_id=role.id, permission_id=permission.id, effect='allow'))
db.session.commit()
assert authenticated_client.get("/cleaning/transvasement").status_code == 200
assert authenticated_client.get("/cleaning/forecast-config").status_code == 200
assert authenticated_client.get("/cleaning/stock").status_code == 200