fix(ui): corriger les erreurs P1 de l audit
This commit is contained in:
parent
b116955299
commit
5238b18e39
12 changed files with 307 additions and 105 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
return render_template('companies/new.html')
|
||||
|
|
|
|||
|
|
@ -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('/<int:id>/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('/<int:id>/delete', methods=['POST'])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -570,9 +570,10 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if intervention.documents.all() %}
|
||||
{% set intervention_documents = documents|default([]) %}
|
||||
{% if intervention_documents %}
|
||||
<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>
|
||||
{% 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>
|
||||
</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>
|
||||
</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 }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</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">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
|
|
@ -710,7 +711,7 @@
|
|||
</div>
|
||||
|
||||
<!-- 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-dialog">
|
||||
<div class="modal-content">
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@
|
|||
</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;">
|
||||
<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 %}
|
||||
· <i class="bi bi-person-badge"></i> {{ interv.assigned_to.full_name }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -129,10 +129,10 @@
|
|||
</button>
|
||||
|
||||
<!-- 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>
|
||||
<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 -->
|
||||
{% if current_user.is_authenticated %}
|
||||
|
|
@ -163,67 +163,67 @@
|
|||
<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-item">
|
||||
{% if has_permission('dashboard.view') %}<li class="nav-item">
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
</li>{% endif %}
|
||||
{% 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">
|
||||
<i class="bi bi-envelope-paper"></i> <span>Messagerie</span>
|
||||
{% if pending_interpretations_count is defined and pending_interpretations_count > 0 %}
|
||||
<span class="badge bg-danger ms-1">{{ pending_interpretations_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
</li>{% endif %}
|
||||
{% 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">
|
||||
<i class="bi bi-wrench-adjustable"></i> <span>Interventions</span>
|
||||
</a>
|
||||
<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>
|
||||
<li><a class="dropdown-item" href="{{ url_for('planning.index') }}"><i class="bi bi-calendar-check"></i> Planning unifié</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 %}
|
||||
{% 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.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><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.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.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><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.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><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><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>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
</li>{% endif %}
|
||||
{% 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">
|
||||
<i class="bi bi-pc-display"></i> <span>Équipements</span>
|
||||
</a>
|
||||
<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><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('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('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><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('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('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><a class="dropdown-item" href="{{ url_for('cleaning.dashboard') }}"><i class="bi bi-droplet-half"></i> Matériel & 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 & 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.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>
|
||||
|
|
@ -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.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.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><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.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>
|
||||
</ul>
|
||||
</li>
|
||||
</li>{% endif %}{% endif %}
|
||||
|
||||
{% if current_user.is_admin() %}
|
||||
<li class="nav-group-label d-lg-none mt-2">Administration</li>
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@
|
|||
<div class="card"><div class="card-body">
|
||||
<form method="POST">
|
||||
<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">Adresse</label><textarea name="address" class="form-control" rows="2"></textarea></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">{{ form_data.get('address', '') if form_data is defined else '' }}</textarea></div>
|
||||
<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">Email</label><input type="email" name="email" 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" value="{{ form_data.get('email', '') if form_data is defined else '' }}"></div>
|
||||
</div>
|
||||
<div class="text-end"><button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Créer</button></div>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@
|
|||
<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>
|
||||
<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 %}
|
||||
· <i class="bi bi-arrow-repeat"></i> {{ frequency_labels.get(interv.frequency_days, interv.frequency_days ~ "j") }}
|
||||
{% endif %}
|
||||
|
|
@ -492,7 +492,7 @@
|
|||
{% else %}<span class="badge bg-info">Préventif</span>{% endif %}
|
||||
</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{ title }} — GMAO Collège{% endblock %}
|
||||
{% block title %}{{ title|default('Salle') }} — GMAO Collège{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-4">{{ title }}</h1>
|
||||
<h1 class="mb-4">{{ title|default('Salle') }}</h1>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
|
|
@ -10,17 +10,17 @@
|
|||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<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 class="col-md-2 mb-3">
|
||||
<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 class="col-md-3 mb-3">
|
||||
<label class="form-label">Bâtiment *</label>
|
||||
<select name="building_id" class="form-select" required id="building-select">
|
||||
{% 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 %}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<select name="zone_id" class="form-select" id="zone-select">
|
||||
<option value="">— Aucune —</option>
|
||||
{% 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 %}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -38,11 +38,11 @@
|
|||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Étage</label>
|
||||
<select name="floor" class="form-select">
|
||||
<option value="-1" {% if room and room.floor == -1 %}selected{% endif %}>Sous-sol</option>
|
||||
<option value="0" {% if room and room.floor == 0 %}selected{% endif %}>RDC</option>
|
||||
<option value="1" {% if room and room.floor == 1 %}selected{% endif %}>1er étage</option>
|
||||
<option value="2" {% if room and room.floor == 2 %}selected{% endif %}>2ème étage</option>
|
||||
<option value="3" {% if room and room.floor == 3 %}selected{% endif %}>3ème étage</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 form_data.get('floor', room.floor if room else 0)|string == '0' %}selected{% endif %}>RDC</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 form_data.get('floor', room.floor if room else 0)|string == '2' %}selected{% endif %}>2è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>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
<select name="room_type_id" class="form-select">
|
||||
<option value="">— Aucun —</option>
|
||||
{% 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 %}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -143,4 +143,4 @@ document.getElementById('building-select').addEventListener('change', function()
|
|||
// Déclencher le filtre au chargement
|
||||
document.getElementById('building-select').dispatchEvent(new Event('change'));
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -494,9 +494,10 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if intervention.documents.all() %}
|
||||
{% set intervention_documents = documents|default([]) %}
|
||||
{% if intervention_documents %}
|
||||
<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>
|
||||
{% set ext = doc.filename|lower %}
|
||||
|
|
|
|||
98
tests/integration/test_ui_p1_regressions.py
Normal file
98
tests/integration/test_ui_p1_regressions.py
Normal 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
|
||||
Loading…
Reference in a new issue