feat(preventive): embed historical lot task catalogue
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-23 12:08:05 +00:00
parent b32d1a989e
commit 0f6770611f
15 changed files with 359 additions and 21 deletions

View file

@ -1 +1 @@
0.1.0-dev.9 0.1.0-dev.10

View file

@ -0,0 +1,160 @@
"""Catalogue préventif historique embarqué.
Le blob est une extraction figée du dump de référence (330 lignes de
``lot_tasks``). Le dump n'est jamais lu à l'exécution : cette représentation
compacte permet de garder une seule source embarquée et réutilisable par le
setup wizard et les tests de comparaison.
"""
import base64
import json
import zlib
_BLOB = (
"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"
)
_FIELDS = ("lot", "category", "num_tache", "tache", "periodicite",
"contrat", "gestionnaire", "jours_entre_interventions",
"type_maintenance", "duree_minutes", "trigger_type", "trigger_event",
"meter_threshold", "season_start_month", "season_end_month",
"advance_days", "is_active", "application_scope",
"unavailable_policy", "after_repair_policy", "is_safety_critical")
def load_preventive_catalog():
raw = json.loads(zlib.decompress(base64.b64decode(_BLOB)))
return [dict(zip(_FIELDS, row)) for row in raw]
LOT_TASKS = tuple(load_preventive_catalog())

View file

@ -13,12 +13,12 @@ from ...extensions import db, csrf
from ..models.user import User from ..models.user import User
from ..models.college import College, Building, Zone, Room from ..models.college import College, Building, Zone, Room
from ..models.equipment import EquipmentCategory from ..models.equipment import EquipmentCategory
from ..models.maintenance import Lot from ..models.maintenance import Lot, LotTask
from ..models.rbac import Role from ..models.rbac import Role
from ..services.rbac import ensure_rbac_catalog, replace_user_roles from ..services.rbac import ensure_rbac_catalog, replace_user_roles
from ..authorization import has_permission from ..authorization import has_permission
from ..models.planning import WorkSchedule, CollegeClosure, ClosureWorkDay from ..models.planning import WorkSchedule, CollegeClosure, ClosureWorkDay
from ..setup_catalog import EQUIPMENT_CATEGORIES, LOTS, grouped_catalog from ..setup_catalog import EQUIPMENT_CATEGORIES, LOTS, LOT_TASKS, grouped_catalog, task_count_by_lot
setup_wizard_bp = Blueprint('setup_wizard', __name__, url_prefix='/setup-wizard') setup_wizard_bp = Blueprint('setup_wizard', __name__, url_prefix='/setup-wizard')
@ -208,7 +208,8 @@ def index():
return render_template('setup_wizard/index.html', return render_template('setup_wizard/index.html',
progress=progress.to_dict(), progress=progress.to_dict(),
lots_data=lots_data) lots_data=lots_data,
task_counts=task_count_by_lot())
@setup_wizard_bp.route('/api/progress') @setup_wizard_bp.route('/api/progress')
@ -439,7 +440,7 @@ def api_save_step(step):
return jsonify({'success': True, 'step': 5, 'rooms': saved}) return jsonify({'success': True, 'step': 5, 'rooms': saved})
elif step == 6: elif step == 6:
# Étape 6 : catalogue fixe des catégories et lots du dump SQL. # Étape 6 : catalogue fixe des catégories, lots et tâches du dump SQL.
categories_by_name = {} categories_by_name = {}
for name in EQUIPMENT_CATEGORIES: for name in EQUIPMENT_CATEGORIES:
category = EquipmentCategory.query.filter( category = EquipmentCategory.query.filter(
@ -451,20 +452,58 @@ def api_save_step(step):
db.session.flush() db.session.flush()
categories_by_name[name] = category categories_by_name[name] = category
selected_names = data.get('selected_lots')
selected_names = {str(name).strip().casefold() for name in selected_names} if isinstance(selected_names, list) else None
imported_lots = []
lots_by_name = {}
for lot_name, category_name in LOTS: for lot_name, category_name in LOTS:
if selected_names is not None and lot_name.casefold() not in selected_names:
continue
lot = Lot.query.filter(db.func.lower(Lot.name) == lot_name.lower()).first() lot = Lot.query.filter(db.func.lower(Lot.name) == lot_name.lower()).first()
if lot is None: if lot is None:
lot = Lot(name=lot_name) lot = Lot(name=lot_name)
db.session.add(lot) db.session.add(lot)
db.session.flush()
lot.category_id = categories_by_name[category_name].id lot.category_id = categories_by_name[category_name].id
lots_by_name[lot_name.casefold()] = lot
imported_lots.append(lot_name)
# Les tâches du catalogue sont ajoutées par clé fonctionnelle. Une
# ligne déjà présente n'est jamais écrasée : elle peut avoir été
# adaptée par l'établissement depuis le dernier import.
task_count = 0
for item in LOT_TASKS:
lot = lots_by_name.get(item['lot'].casefold())
if lot is None:
continue
existing = LotTask.query.filter(
LotTask.lot_id == lot.id,
db.func.lower(LotTask.tache) == item['tache'].casefold(),
).first() if item.get('tache') else None
if existing is not None:
continue
task = LotTask(lot_id=lot.id)
for field in (
'num_tache', 'tache', 'periodicite', 'contrat', 'gestionnaire',
'jours_entre_interventions', 'type_maintenance', 'duree_minutes',
'trigger_type', 'trigger_event', 'meter_threshold',
'season_start_month', 'season_end_month', 'advance_days',
'is_active', 'application_scope', 'unavailable_policy',
'after_repair_policy', 'is_safety_critical',
):
if field in item:
setattr(task, field, item[field])
db.session.add(task)
task_count += 1
progress.categories_json = json.dumps(list(EQUIPMENT_CATEGORIES), ensure_ascii=False) progress.categories_json = json.dumps(list(EQUIPMENT_CATEGORIES), ensure_ascii=False)
progress.set_lots([name for name, _category in LOTS]) progress.set_lots(imported_lots)
progress.step = max(progress.step or 0, 6) progress.step = max(progress.step or 0, 6)
db.session.commit() db.session.commit()
return jsonify({ return jsonify({
'success': True, 'step': 6, 'success': True, 'step': 6,
'categories_count': len(EQUIPMENT_CATEGORIES), 'lots_count': len(LOTS), 'categories_count': len(EQUIPMENT_CATEGORIES), 'lots_count': len(imported_lots),
'lot_tasks_count': task_count,
}) })
elif step == 7: elif step == 7:

View file

@ -4,6 +4,7 @@ Ces valeurs reprennent les catégories et les lots du dump SQL de référence.
Les noms sont les clés fonctionnelles utilisées pour une synchronisation idempotente : Les noms sont les clés fonctionnelles utilisées pour une synchronisation idempotente :
le wizard ne supprime jamais une ligne déjà présente dans la base. le wizard ne supprime jamais une ligne déjà présente dans la base.
""" """
from .preventive_catalog import LOT_TASKS
EQUIPMENT_CATEGORIES = ( EQUIPMENT_CATEGORIES = (
"Chauffage", "Courants faibles", "Gros œuvre", "Revêtement de façades", "Chauffage", "Courants faibles", "Gros œuvre", "Revêtement de façades",
@ -85,3 +86,11 @@ def grouped_catalog():
for lot_name, category_name in LOTS: for lot_name, category_name in LOTS:
grouped[category_name].append(lot_name) grouped[category_name].append(lot_name)
return grouped return grouped
def task_count_by_lot():
"""Nombre de règles préventives historiques pour chaque lot."""
counts = {}
for task in LOT_TASKS:
counts[task["lot"]] = counts.get(task["lot"], 0) + 1
return counts

View file

@ -10,7 +10,7 @@ from ..core.models.planning import ScheduledTask
from ..core.services.maintenance_engine import generate_due_tasks from ..core.services.maintenance_engine import generate_due_tasks
from ..core.models.company import Company from ..core.models.company import Company
from ..contracts.models import Contract from ..contracts.models import Contract
from ..core.models.equipment import Equipment from ..core.models.equipment import Equipment, EquipmentCategory
lots_bp = Blueprint('lots', __name__) lots_bp = Blueprint('lots', __name__)
@ -37,6 +37,27 @@ def index():
companies = Company.query.order_by(Company.name).all() companies = Company.query.order_by(Company.name).all()
return render_template('lots/index.html', lots=lots, companies=companies) return render_template('lots/index.html', lots=lots, companies=companies)
@lots_bp.route('/new', methods=['GET', 'POST'])
@login_required
def create():
"""Créer un lot indépendamment de la création d'un équipement."""
categories = EquipmentCategory.query.order_by(EquipmentCategory.name).all()
if request.method == 'POST':
name = (request.form.get('name') or '').strip()
category_id = request.form.get('category_id', type=int) or None
if not name:
flash('Le nom du lot est obligatoire.', 'danger')
elif category_id and db.session.get(EquipmentCategory, category_id) is None:
flash('La catégorie sélectionnée est invalide.', 'danger')
else:
lot = Lot(name=name, category_id=category_id, is_present=request.form.get('is_present') == 'on')
db.session.add(lot)
db.session.commit()
flash('Lot créé. Ajoutez maintenant ses tâches préventives.', 'success')
return redirect(url_for('lots.detail', id=lot.id))
return render_template('lots/create.html', categories=categories)
@lots_bp.route('/<int:id>') @lots_bp.route('/<int:id>')
@login_required @login_required
def detail(id): def detail(id):

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Ajouter un lot — GMAO{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<div><h1><i class="bi bi-boxes"></i> Ajouter un lot</h1>
<p class="text-muted mb-0">Un lot regroupe des équipements et leurs actions de maintenance préventive.</p></div>
<a href="{{ url_for('lots.index') }}" class="btn btn-secondary">Annuler</a>
</div>
<div class="card"><div class="card-body">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3"><label for="name" class="form-label">Nom du lot <span class="text-danger">*</span></label>
<input id="name" name="name" class="form-control" required maxlength="500" value="{{ request.form.get('name','') }}" placeholder="Ex. Éclairage des salles"></div>
<div class="mb-3"><label for="category_id" class="form-label">Catégorie</label>
<select id="category_id" name="category_id" class="form-select"><option value="">-- À choisir --</option>{% for category in categories %}<option value="{{ category.id }}" {% if request.form.get('category_id')|int == category.id %}selected{% endif %}>{{ category.name }}</option>{% endfor %}</select></div>
<div class="form-check mb-3"><input id="is_present" name="is_present" class="form-check-input" type="checkbox" checked><label for="is_present" class="form-check-label">Ce lot est présent dans l'établissement</label></div>
<button class="btn btn-primary" type="submit"><i class="bi bi-check-lg"></i> Créer le lot</button>
</form>
</div></div>
</div>
{% endblock %}

View file

@ -5,8 +5,11 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-boxes"></i> Gestion des Lots</h1> <h1><i class="bi bi-boxes"></i> Gestion des Lots</h1>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{{ url_for('wizard.equipment_wizard') }}" class="btn btn-primary"> <a href="{{ url_for('lots.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Créer un lot et son équipement <i class="bi bi-plus-circle"></i> Ajouter un lot
</a>
<a href="{{ url_for('wizard.equipment_wizard') }}" class="btn btn-outline-primary">
<i class="bi bi-hdd-stack"></i> Ajouter un équipement
</a> </a>
<a href="{{ url_for('equipments.index') }}" class="btn btn-secondary"> <a href="{{ url_for('equipments.index') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Retour aux équipements <i class="bi bi-arrow-left"></i> Retour aux équipements
@ -16,7 +19,7 @@
<div class="alert alert-info" role="note"> <div class="alert alert-info" role="note">
Les lots regroupent les équipements suivis ensemble et leurs actions préventives. Les lots regroupent les équipements suivis ensemble et leurs actions préventives.
Vous pouvez créer un premier lot depuis l'assistant d'ajout d'équipement. Vous pouvez créer un lot seul, puis y associer des équipements et des tâches.
</div> </div>
<!-- Filtres --> <!-- Filtres -->

View file

@ -65,7 +65,7 @@
<div class="accordion" id="catalogAccordion"> <div class="accordion" id="catalogAccordion">
{% for category, lots in lots_data.items() %} {% for category, lots in lots_data.items() %}
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#catalog{{ loop.index }}">{{ category }} <span class="badge bg-secondary ms-2">{{ lots|length }}</span></button></h2> <div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#catalog{{ loop.index }}">{{ category }} <span class="badge bg-secondary ms-2">{{ lots|length }}</span></button></h2>
<div id="catalog{{ loop.index }}" class="accordion-collapse collapse" data-bs-parent="#catalogAccordion"><div class="accordion-body"><ul class="mb-0">{% for lot in lots %}<li>{{ lot }}</li>{% endfor %}</ul></div></div></div> <div id="catalog{{ loop.index }}" class="accordion-collapse collapse" data-bs-parent="#catalogAccordion"><div class="accordion-body"><ul class="mb-0">{% for lot in lots %}<li>{{ lot }}{% if task_counts.get(lot) %}<span class="badge bg-info text-dark ms-2">{{ task_counts.get(lot) }} tâche(s) préventive(s)</span>{% endif %}</li>{% endfor %}</ul></div></div></div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
services: services:
gmao: gmao:
environment: environment:
GMAO_VERSION: ${GMAO_VERSION:-0.1.0-dev.9} GMAO_VERSION: ${GMAO_VERSION:-0.1.0-dev.10}
GMAO_GUNICORN_FLAGS: --reload --reload-engine poll GMAO_GUNICORN_FLAGS: --reload --reload-engine poll
FLASK_ENV: development FLASK_ENV: development
# Ce profil est servi en HTTP sur le réseau de test ; un cookie Secure # Ce profil est servi en HTTP sur le réseau de test ; un cookie Secure

View file

@ -25,12 +25,12 @@ services:
memory: 128M memory: 128M
gmao: gmao:
image: gmao-college:${GMAO_VERSION:-0.1.0-dev.9} image: gmao-college:${GMAO_VERSION:-0.1.0-dev.10}
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
GMAO_VERSION: ${GMAO_VERSION:-0.1.0-dev.9} GMAO_VERSION: ${GMAO_VERSION:-0.1.0-dev.10}
GIT_VERSION: ${GIT_VERSION:-unknown} GIT_VERSION: ${GIT_VERSION:-unknown}
container_name: gmao-flask container_name: gmao-flask
restart: unless-stopped restart: unless-stopped
@ -51,7 +51,7 @@ services:
- TZ=Europe/Paris - TZ=Europe/Paris
- DB_HOST=mariadb - DB_HOST=mariadb
- DB_PORT=3306 - DB_PORT=3306
- GMAO_VERSION=${GMAO_VERSION:-0.1.0-dev.9} - GMAO_VERSION=${GMAO_VERSION:-0.1.0-dev.10}
- GMAO_GUNICORN_FLAGS=--preload - GMAO_GUNICORN_FLAGS=--preload
- DOCKER_GMAO_WORKDIR=/app - DOCKER_GMAO_WORKDIR=/app
working_dir: /app working_dir: /app

View file

@ -1,7 +1,7 @@
# Requalification Phase 2.1 — prérequis métier et interface # Requalification Phase 2.1 — prérequis métier et interface
Date : 2026-08-23 Date : 2026-08-23
Version auditée : 0.1.0-dev.8 Version auditée initialement : 0.1.0-dev.8 ; correction catalogue : 0.1.0-dev.10
## Conclusion ## Conclusion
@ -123,4 +123,5 @@ aucune donnée n'a été nettoyée.
La phase est **PARTIELLEMENT VALIDÉE** : l'isolation des horaires et l'absence La phase est **PARTIELLEMENT VALIDÉE** : l'isolation des horaires et l'absence
de fallback silencieux sont corrigées et testées ; le parcours UI préventif de fallback silencieux sont corrigées et testées ; le parcours UI préventif
réel, la création de deux techniciens depuis l'UI et la génération sans réel, la création de deux techniciens depuis l'UI et la génération sans
fixture restent à démontrer. fixture restent à démontrer. Le catalogue embarqué contient désormais les 330
tâches historiques de `lot_tasks` et son import est idempotent.

View file

@ -0,0 +1,65 @@
# Audit du catalogue préventif historique
Date : 2026-08-23
Source de référence : `/opt/gmao/dump_sql_base_remplie.sql` (lecture seule)
## Tables analysées
| Table | Lignes dans le dump | Utilité |
|---|---:|---|
| `equipment_categories` | 36 | catégories patrimoniales |
| `lots` | 92 | lots, catégorie par `category_id`, présence |
| `lot_tasks` | 330 | référentiel historique des actions par `lot_id` |
| `preventive_tasks` | 0 | ancienne table, structure présente mais aucune donnée |
| `scheduled_tasks` | 516 | échéances historiques, pas le catalogue initial |
Le modèle actuel utilise `LotTask`, et non `preventive_tasks`, pour les règles
préventives. Le dump contient 36 catégories et 92 lots, déjà représentés dans
`setup_catalog.py`; il manquait les 330 lignes de `lot_tasks`.
## Durée et périodicité
- 330 tâches ont été extraites sans modification des valeurs historiques ;
- 5 tâches ont une `duree_minutes` renseignée ; 325 ont une durée inconnue ;
- 327 ont une périodicité textuelle ; 3 ont une périodicité vide ;
- les valeurs `jours_entre_interventions`, type, contrat, gestionnaire,
déclenchement et politiques ont été conservées lorsqu'elles existent.
La valeur de 60 minutes de `LotTask.effective_duration()` n'est pas présente
dans le dump : c'est un fallback du code actuel, pas une donnée historique.
Elle reste temporairement compatible avec les anciennes données, mais une tâche
sans durée doit être signalée avant une validation métier définitive.
## Catalogue embarqué
`app_new/core/preventive_catalog.py` contient une extraction compressée et
figée des 330 tâches. Le dump n'est jamais ouvert par l'application. Le module
expose `LOT_TASKS`, utilisé par le setup wizard et les tests.
Le wizard crée les catégories, les lots sélectionnés et leurs tâches. La clé
fonctionnelle de tâche est `(lot, tache)` ; une ligne déjà existante n'est pas
écrasée, afin de préserver une personnalisation locale. Les éléments manquants
sont ajoutés. La seconde exécution est donc idempotente.
## Relation équipement
`Equipment.lot_id -> Lot.id`, avec héritage par `parent.effective_lot_id`.
Les tâches sont donc visibles via le lot effectif et peuvent générer des
`ScheduledTask` pour les équipements actifs. Un équipement rebuté ou supprimé
ne doit pas produire de nouvelle échéance.
## Première échéance
Le mécanisme actuel `generate_due_tasks()` calcule la première échéance à
partir de la date de génération et de `jours_entre_interventions`/périodicité.
Le dump ne fournit pas une date de première échéance catalogue. Une règle
métier explicite (date d'association, prochaine échéance saisie ou campagne)
reste à valider avant une automatisation plus ambitieuse.
## Écarts et limites
- le catalogue est maintenant complet pour les tables historiques validées ;
- les tâches sans durée restent à renseigner pour une planification fiable ;
- la validation bout en bout par interface (installation, association d'un
équipement, génération, Ma journée) reste à exécuter avec un compte UI ;
- `preventive_tasks` est documentée mais vide et n'est pas importée.

View file

@ -141,8 +141,8 @@ def test_setup_creates_zones_and_rooms_with_valid_links(client, app, monkeypatch
def test_setup_imports_embedded_lot_catalog_idempotently(client, app, monkeypatch, tmp_path): def test_setup_imports_embedded_lot_catalog_idempotently(client, app, monkeypatch, tmp_path):
from app_new.core.models.equipment import EquipmentCategory from app_new.core.models.equipment import EquipmentCategory
from app_new.core.models.maintenance import Lot from app_new.core.models.maintenance import Lot, LotTask
from app_new.core.setup_catalog import EQUIPMENT_CATEGORIES, LOTS from app_new.core.setup_catalog import EQUIPMENT_CATEGORIES, LOTS, LOT_TASKS
_authorize_setup(client, monkeypatch, tmp_path) _authorize_setup(client, monkeypatch, tmp_path)
first = client.post('/setup-wizard/api/step/6', json={}) first = client.post('/setup-wizard/api/step/6', json={})
@ -150,6 +150,7 @@ def test_setup_imports_embedded_lot_catalog_idempotently(client, app, monkeypatc
assert first.status_code == second.status_code == 200 assert first.status_code == second.status_code == 200
assert first.get_json()['categories_count'] == len(EQUIPMENT_CATEGORIES) assert first.get_json()['categories_count'] == len(EQUIPMENT_CATEGORIES)
assert first.get_json()['lots_count'] == len(LOTS) assert first.get_json()['lots_count'] == len(LOTS)
assert first.get_json()['lot_tasks_count'] == len(LOT_TASKS)
with app.app_context(): with app.app_context():
for name in EQUIPMENT_CATEGORIES: for name in EQUIPMENT_CATEGORIES:
@ -157,6 +158,7 @@ def test_setup_imports_embedded_lot_catalog_idempotently(client, app, monkeypatc
for name, category_name in LOTS: for name, category_name in LOTS:
lot = Lot.query.filter_by(name=name).one() lot = Lot.query.filter_by(name=name).one()
assert lot.category.name == category_name assert lot.category.name == category_name
assert LotTask.query.count() == len(LOT_TASKS)
def test_setup_saves_exact_work_days_inside_school_holidays(client, app, monkeypatch, tmp_path): def test_setup_saves_exact_work_days_inside_school_holidays(client, app, monkeypatch, tmp_path):

View file

@ -0,0 +1,16 @@
from app_new.core.setup_catalog import LOT_TASKS
def test_embedded_preventive_catalog_has_historical_shape():
"""Garde-fou du catalogue extrait du dump de référence."""
assert len(LOT_TASKS) == 330
assert all(item['lot'] and item['tache'] for item in LOT_TASKS)
assert all(item['type_maintenance'] in ('préventive', 'preventive', 'curative', None) for item in LOT_TASKS)
assert sum(item['duree_minutes'] is not None for item in LOT_TASKS) == 5
assert sum(bool(item['periodicite']) for item in LOT_TASKS) == 327
assert len({(item['lot'], item['num_tache']) for item in LOT_TASKS}) == len(LOT_TASKS)
def test_catalog_tasks_have_stable_functional_keys():
keys = [(item['lot'].casefold(), item['tache'].casefold()) for item in LOT_TASKS]
assert len(keys) == len(set(keys))

View file

@ -3,7 +3,7 @@ from app_new.version import get_version
def test_project_version_is_available(monkeypatch): def test_project_version_is_available(monkeypatch):
monkeypatch.delenv('GMAO_VERSION', raising=False) monkeypatch.delenv('GMAO_VERSION', raising=False)
assert get_version() == '0.1.0-dev.9' assert get_version() == '0.1.0-dev.10'
def test_environment_version_takes_precedence(monkeypatch): def test_environment_version_takes_precedence(monkeypatch):