feat(patrimoine): ajouter profils de locaux progressifs
This commit is contained in:
parent
330c4ba929
commit
b202cca760
19 changed files with 489 additions and 7 deletions
|
|
@ -155,12 +155,18 @@ def create_app(config_name='default'):
|
|||
from .room_types.routes import room_types_bp
|
||||
app.register_blueprint(room_types_bp, url_prefix='/room-types')
|
||||
|
||||
from .room_profiles.routes import room_profiles_bp
|
||||
app.register_blueprint(room_profiles_bp, url_prefix='/room-profiles')
|
||||
|
||||
from .documents.routes import documents_bp
|
||||
app.register_blueprint(documents_bp, url_prefix='/documents')
|
||||
|
||||
from .gmao_config.routes import gmao_config_bp
|
||||
app.register_blueprint(gmao_config_bp, url_prefix='/gmao-config')
|
||||
|
||||
from .configuration.routes import configuration_bp
|
||||
app.register_blueprint(configuration_bp, url_prefix='/configuration')
|
||||
|
||||
from .wizard.routes import wizard_bp
|
||||
app.register_blueprint(wizard_bp)
|
||||
|
||||
|
|
|
|||
1
app_new/configuration/__init__.py
Normal file
1
app_new/configuration/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Centre de configuration permanent."""
|
||||
69
app_new/configuration/routes.py
Normal file
69
app_new/configuration/routes.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from app_new.core.models.college import Site, Building, Zone, Room, RoomProfile, RoomSchedule
|
||||
from app_new.core.models.equipment import Equipment
|
||||
from app_new.core.models.maintenance import LotTask
|
||||
from app_new.core.models.user import User
|
||||
from app_new.core.models.planning import WorkSchedule
|
||||
from app_new.contracts.models import Contract
|
||||
|
||||
configuration_bp = Blueprint("configuration", __name__, template_folder="templates")
|
||||
|
||||
|
||||
def _status(ok, label, url, detail=None, optional=False, attention=False):
|
||||
if optional and not ok:
|
||||
state = "Facultatif"
|
||||
elif attention:
|
||||
state = "Attention"
|
||||
elif ok:
|
||||
state = "Configuré"
|
||||
else:
|
||||
state = "À configurer"
|
||||
return {"label": label, "state": state, "ok": ok, "url": url, "detail": detail or ""}
|
||||
|
||||
|
||||
@configuration_bp.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
from flask import url_for
|
||||
sites = Site.query.count()
|
||||
buildings = Building.query.count()
|
||||
zones = Zone.query.count()
|
||||
rooms = Room.query.count()
|
||||
profiles = RoomProfile.query.count()
|
||||
unprofiled = Room.query.filter(Room.room_type_id.is_(None)).count()
|
||||
equipment_without_lot = Equipment.query.filter(Equipment.is_deleted.is_(False), Equipment.lot_id.is_(None)).count()
|
||||
technicians = User.query.filter_by(is_active=True).all()
|
||||
scheduled_users = {row[0] for row in WorkSchedule.query.filter_by(is_active=True).with_entities(WorkSchedule.user_id).all()}
|
||||
tasks_without_duration = LotTask.query.filter(LotTask.is_active.is_(True), (LotTask.duree_minutes.is_(None) | (LotTask.duree_minutes <= 0))).count()
|
||||
contracts_without_due = Contract.query.filter(Contract.is_active.is_(True), Contract.end_date.is_(None)).count() if hasattr(Contract, "end_date") else 0
|
||||
room_schedules = RoomSchedule.query.count()
|
||||
sections = [
|
||||
("ESSENTIEL", [
|
||||
_status(User.query.filter_by(is_active=True).count() > 0, "Administrateur / utilisateurs", url_for("admin.users")),
|
||||
_status(sites > 0, "Établissement / site", url_for("buildings.index"), f"{sites} site(s)"),
|
||||
_status(buildings > 0, "Bâtiments", url_for("buildings.index"), f"{buildings} bâtiment(s)"),
|
||||
_status(zones > 0, "Zones", url_for("zones.index"), f"{zones} zone(s)"),
|
||||
_status(rooms > 0, "Locaux / emplacements", url_for("rooms.index"), f"{rooms} local(aux)"),
|
||||
_status(len(scheduled_users) >= len(technicians) and bool(technicians), "Horaires techniciens", url_for("planning.work_schedules"), f"{len(technicians) - len(scheduled_users)} sans horaires" if technicians else "Aucun technicien actif", attention=bool(technicians) and len(scheduled_users) < len(technicians)),
|
||||
]),
|
||||
("MAINTENANCE", [
|
||||
_status(profiles > 0, "Profils de locaux", url_for("room_profiles.index"), f"{profiles} profil(s)"),
|
||||
_status(equipment_without_lot == 0, "Équipements avec lot", url_for("equipments.index"), f"{equipment_without_lot} sans lot", attention=equipment_without_lot > 0),
|
||||
_status(tasks_without_duration == 0, "Durées préventives", url_for("lots.index"), f"{tasks_without_duration} à renseigner", attention=tasks_without_duration > 0),
|
||||
_status(room_schedules > 0, "Planning des salles", url_for("planning.room_schedules"), f"{room_schedules} créneau(x)"),
|
||||
]),
|
||||
("PATRIMOINE", [
|
||||
_status(unprofiled == 0, "Locaux avec profil structurel", url_for("rooms.index"), f"{unprofiled} sans type", attention=unprofiled > 0),
|
||||
_status(True, "Logements de fonction", url_for("housing.index"), "Facultatif", optional=True),
|
||||
]),
|
||||
("ENTREPRISES", [_status(contracts_without_due == 0, "Contrats avec échéance", url_for("companies.index"), f"{contracts_without_due} sans échéance", attention=contracts_without_due > 0)]),
|
||||
("COMPTEURS", [_status(False, "Compteurs", url_for("meters.index"), "Non configuré", optional=True)]),
|
||||
("CARTOGRAPHIE TECHNIQUE", [_status(False, "Relations techniques", url_for("equipments.index"), "Facultatif", optional=True)]),
|
||||
("INTÉGRATIONS", [
|
||||
_status(False, "Pronote", url_for("pronote.index"), "Non configuré", optional=True),
|
||||
_status(False, "ENT / Outlook / Yeastar / IA", url_for("admin.settings"), "Facultatif", optional=True),
|
||||
]),
|
||||
]
|
||||
return render_template("configuration/index.html", sections=sections)
|
||||
5
app_new/configuration/templates/configuration/index.html
Normal file
5
app_new/configuration/templates/configuration/index.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}Centre de configuration{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid py-3"><div class="d-flex justify-content-between align-items-center mb-3"><div><h1 class="h3 mb-1">Centre de configuration</h1><p class="text-muted mb-0">Un état permanent de ce qui est prêt, incomplet ou facultatif.</p></div><span class="badge text-bg-light">Configuration progressive</span></div><div class="row g-4">{% for name, items in sections %}<section class="col-md-6 col-xl-4"><div class="card h-100"><div class="card-header"><strong>{{ name }}</strong></div><div class="list-group list-group-flush">{% for item in items %}<div class="list-group-item d-flex justify-content-between gap-2 align-items-start"><div><div>{% if item.state == 'Configuré' %}✓{% elif item.state == 'Attention' %}⚠{% elif item.state == 'Facultatif' %}○{% else %}✕{% endif %} <strong>{{ item.label }}</strong></div><small class="text-muted">{{ item.state }}{% if item.detail %} · {{ item.detail }}{% endif %}</small></div><a class="btn btn-sm btn-outline-secondary" href="{{ item.url }}">Ouvrir</a></div>{% endfor %}</div></div></section>{% endfor %}</div></div>
|
||||
{% endblock %}
|
||||
|
|
@ -99,7 +99,7 @@ ADMIN_BLUEPRINTS = {
|
|||
}
|
||||
PATRIMOINE_BLUEPRINTS = {
|
||||
"equipments", "equipments_meters", "equipments_documents", "equipments_restrictions", "housing",
|
||||
"equipments_scheduled", "buildings", "zones", "rooms", "room_types", "wizard", "lots",
|
||||
"equipments_scheduled", "buildings", "zones", "rooms", "room_types", "room_profiles", "configuration", "wizard", "lots",
|
||||
}
|
||||
PLANNING_BLUEPRINTS = {"planning", "scheduler", "interventions_planning"}
|
||||
STOCK_BLUEPRINTS = {"parts", "meters", "cleaning"}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Core Models - GMAO Collège
|
|||
Import tous les modèles du core
|
||||
"""
|
||||
from .user import User, Staff
|
||||
from .college import College, Site, Building, HousingUnit, HousingOccupancy, Room, RoomType, RoomSchedule, Zone
|
||||
from .college import College, Site, Building, HousingUnit, HousingOccupancy, Room, RoomType, RoomProfile, RoomProfileItem, RoomSurface, RoomSchedule, Zone
|
||||
from .equipment import EquipmentCategory, Equipment, EquipmentDocument, EquipmentRoomHistory, EquipmentQuantityMovement, EquipmentLifecycleEvent
|
||||
from .maintenance import (
|
||||
Intervention, StatusChange, InterventionComment, InterventionDocument,
|
||||
|
|
@ -30,7 +30,7 @@ from .rbac import Role, Permission, UserRole, RolePermission, UserPermission
|
|||
|
||||
__all__ = [
|
||||
'User', 'Staff',
|
||||
'College', 'Site', 'Building', 'HousingUnit', 'HousingOccupancy', 'Room', 'RoomType', 'RoomSchedule', 'Zone',
|
||||
'College', 'Site', 'Building', 'HousingUnit', 'HousingOccupancy', 'Room', 'RoomType', 'RoomProfile', 'RoomProfileItem', 'RoomSurface', 'RoomSchedule', 'Zone',
|
||||
'EquipmentCategory', 'Equipment', 'EquipmentDocument', 'EquipmentRoomHistory', 'EquipmentQuantityMovement', 'EquipmentLifecycleEvent',
|
||||
'Intervention', 'StatusChange', 'InterventionComment', 'InterventionDocument',
|
||||
'Lot', 'LotTask', 'LotService', 'WorkRequest', 'WorkRequestQuote', 'WorkRequestEvent',
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class Room(db.Model):
|
|||
floor = db.Column(db.Integer, default=0)
|
||||
building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False)
|
||||
room_type_id = db.Column(db.Integer, db.ForeignKey("room_types.id"), nullable=True)
|
||||
room_profile_id = db.Column(db.Integer, db.ForeignKey("room_profiles.id"), nullable=True, index=True)
|
||||
housing_unit_id = db.Column(db.Integer, db.ForeignKey("housing_units.id"), nullable=True, index=True)
|
||||
# Libelle historique conserve pour les imports anterieurs a zone_id.
|
||||
legacy_zone_name = db.Column("zone", db.String(100), nullable=True)
|
||||
|
|
@ -159,6 +160,7 @@ class Room(db.Model):
|
|||
building = db.relationship("Building", overlaps="rooms")
|
||||
zone = db.relationship("Zone", back_populates="rooms")
|
||||
room_type = db.relationship("RoomType", back_populates="rooms")
|
||||
room_profile = db.relationship("RoomProfile", backref=db.backref("rooms", lazy="dynamic"))
|
||||
equipments = db.relationship("Equipment", back_populates="room")
|
||||
housing_unit = db.relationship("HousingUnit", back_populates="rooms")
|
||||
|
||||
|
|
@ -191,6 +193,68 @@ class RoomType(db.Model):
|
|||
return f"<RoomType {self.name}>"
|
||||
|
||||
|
||||
class RoomProfile(db.Model):
|
||||
"""Modèle de proposition pour préconfigurer un local.
|
||||
|
||||
``RoomType`` reste un classement structurel (notamment utilisé par les
|
||||
emplois du temps). Un profil est une proposition éditable qui ne crée
|
||||
aucun équipement tant que l'utilisateur n'a pas validé l'application.
|
||||
"""
|
||||
__tablename__ = "room_profiles"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(120), nullable=False, unique=True)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
room_type_id = db.Column(db.Integer, db.ForeignKey("room_types.id"), nullable=True, index=True)
|
||||
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
room_type = db.relationship("RoomType", backref=db.backref("profiles", lazy="dynamic"))
|
||||
items = db.relationship("RoomProfileItem", back_populates="profile", cascade="all, delete-orphan",
|
||||
order_by="RoomProfileItem.sort_order, RoomProfileItem.id")
|
||||
surfaces = db.relationship("RoomSurface", back_populates="profile", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RoomProfile {self.name}>"
|
||||
|
||||
|
||||
class RoomProfileItem(db.Model):
|
||||
"""Une proposition d'équipement dans un profil de local."""
|
||||
__tablename__ = "room_profile_items"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
profile_id = db.Column(db.Integer, db.ForeignKey("room_profiles.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
label = db.Column(db.String(200), nullable=False)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey("equipment_categories.id"), nullable=True, index=True)
|
||||
lot_id = db.Column(db.Integer, db.ForeignKey("lots.id"), nullable=True, index=True)
|
||||
quantity = db.Column(db.Integer, nullable=False, default=1)
|
||||
is_group = db.Column(db.Boolean, nullable=False, default=False)
|
||||
enabled_by_default = db.Column(db.Boolean, nullable=False, default=True)
|
||||
sort_order = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
profile = db.relationship("RoomProfile", back_populates="items")
|
||||
category = db.relationship("EquipmentCategory")
|
||||
lot = db.relationship("Lot")
|
||||
|
||||
|
||||
class RoomSurface(db.Model):
|
||||
"""Composition facultative d'une surface réelle ou proposée."""
|
||||
__tablename__ = "room_surfaces"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
room_id = db.Column(db.Integer, db.ForeignKey("rooms.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||
profile_id = db.Column(db.Integer, db.ForeignKey("room_profiles.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||
surface_type = db.Column(db.String(30), nullable=False) # mur, sol, plafond
|
||||
material = db.Column(db.String(80), nullable=False)
|
||||
finish = db.Column(db.String(80), nullable=True)
|
||||
label = db.Column(db.String(120), nullable=True)
|
||||
sort_order = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
room = db.relationship("Room", backref=db.backref("surfaces", cascade="all, delete-orphan"))
|
||||
profile = db.relationship("RoomProfile", back_populates="surfaces")
|
||||
|
||||
|
||||
class RoomSchedule(db.Model):
|
||||
"""Emploi du temps d'une salle."""
|
||||
__tablename__ = "room_schedules"
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ class SetupProgress(db.Model):
|
|||
'zone_id': item.zone_id,
|
||||
'floor': item.floor or 0,
|
||||
'room_type_id': item.room_type_id,
|
||||
'room_profile_id': item.room_profile_id,
|
||||
}
|
||||
for item in Room.query.order_by(Room.building_id, Room.name, Room.id).all()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from flask import Blueprint, render_template, redirect, url_for, request, flash
|
|||
from flask_login import login_required
|
||||
|
||||
from app_new.extensions import db
|
||||
from app_new.core.models.college import Room, Zone, Building, RoomType, RoomSchedule
|
||||
from app_new.core.models.college import Room, Zone, Building, RoomType, RoomProfile, RoomSchedule
|
||||
from app_new.core.models.equipment import Equipment
|
||||
|
||||
rooms_bp = Blueprint('rooms', __name__, template_folder='templates')
|
||||
|
|
@ -22,6 +22,7 @@ def _room_form_context(room=None):
|
|||
'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(),
|
||||
'room_profiles': RoomProfile.query.order_by(RoomProfile.name).all(),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -126,6 +127,7 @@ def create():
|
|||
zone_id=zone_id,
|
||||
building_id=building.id,
|
||||
room_type_id=request.form.get('room_type_id', type=int),
|
||||
room_profile_id=request.form.get('room_profile_id', type=int) or None,
|
||||
floor=request.form.get('floor', 0, type=int)
|
||||
)
|
||||
db.session.add(room)
|
||||
|
|
@ -161,6 +163,7 @@ def bulk_create():
|
|||
flash('Choisissez un bâtiment valide et indiquez au moins un local.', 'danger')
|
||||
return render_template('equipments/rooms_bulk.html', **context), 400
|
||||
room_type_id = request.form.get('room_type_id', type=int)
|
||||
room_profile_id = request.form.get('room_profile_id', type=int) or None
|
||||
created = []
|
||||
existing = {room.name for room in Room.query.filter_by(building_id=building.id).all()}
|
||||
for name in names:
|
||||
|
|
@ -169,6 +172,7 @@ def bulk_create():
|
|||
created.append(Room(name=name, code=name, building_id=building.id,
|
||||
zone_id=zone.id if zone else None,
|
||||
room_type_id=room_type_id,
|
||||
room_profile_id=room_profile_id,
|
||||
floor=request.form.get('floor', 0, type=int)))
|
||||
existing.add(name)
|
||||
db.session.add_all(created)
|
||||
|
|
@ -201,6 +205,7 @@ def edit(id):
|
|||
room.zone_id = zone_id
|
||||
room.building_id = building.id
|
||||
room.room_type_id = request.form.get('room_type_id', type=int)
|
||||
room.room_profile_id = request.form.get('room_profile_id', type=int) or None
|
||||
room.floor = request.form.get('floor', 0, type=int)
|
||||
try:
|
||||
db.session.commit()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Créer plusieurs locaux{% endblock %}
|
||||
{% block content %}<div class="container-fluid" style="max-width:900px"><h1 class="h3">Créer plusieurs locaux</h1><p class="text-muted">Un nom par ligne. Le profil de local reste modifiable ensuite ; aucun équipement n'est imposé.</p><form method="post" class="card"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><div class="card-body row g-3"><div class="col-md-6"><label class="form-label">Bâtiment</label><select class="form-select" name="building_id" required><option value="">Choisir</option>{% for building in buildings %}<option value="{{ building.id }}">{{ building.name }}</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Zone</label><select class="form-select" name="zone_id"><option value="">Aucune zone</option>{% for zone in zones %}<option value="{{ zone.id }}">{{ zone.building.name }} — {{ zone.name }}</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Profil / type de local</label><select class="form-select" name="room_type_id"><option value="">À préciser plus tard</option>{% for room_type in room_types %}<option value="{{ room_type.id }}">{{ room_type.name }}</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Étage</label><input class="form-control" type="number" name="floor" value="0"></div><div class="col-12"><label class="form-label">Noms des locaux</label><textarea class="form-control" name="names" rows="8" required placeholder="B101
|
||||
B102
|
||||
B103"></textarea></div></div><div class="card-footer"><a class="btn btn-outline-secondary" href="{{ url_for('rooms.index') }}">Annuler</a> <button class="btn btn-primary">Créer les locaux</button></div></form></div>{% endblock %}
|
||||
{% block content %}<div class="container-fluid" style="max-width:900px"><h1 class="h3">Créer plusieurs locaux</h1><p class="text-muted">Un nom par ligne. Le profil est associé sans créer automatiquement d'équipement.</p><form method="post" class="card"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><div class="card-body row g-3"><div class="col-md-6"><label class="form-label">Bâtiment</label><select class="form-select" name="building_id" required><option value="">Choisir</option>{% for building in buildings %}<option value="{{ building.id }}">{{ building.name }}</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Zone</label><select class="form-select" name="zone_id"><option value="">Aucune zone</option>{% for zone in zones %}<option value="{{ zone.id }}">{{ zone.building.name }} — {{ zone.name }}</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Type structurel de local</label><select class="form-select" name="room_type_id"><option value="">À préciser plus tard</option>{% for room_type in room_types %}<option value="{{ room_type.id }}">{{ room_type.name }}</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Profil de propositions</label><select class="form-select" name="room_profile_id"><option value="">Aucun</option>{% for profile in room_profiles %}<option value="{{ profile.id }}">{{ profile.name }}</option>{% endfor %}</select></div><div class="col-md-4"><label class="form-label">Étage</label><input class="form-control" type="number" name="floor" value="0"></div><div class="col-12"><label class="form-label">Noms des locaux</label><textarea class="form-control" name="names" rows="8" required placeholder="B101, B102, B103"></textarea></div></div><div class="card-footer"><a class="btn btn-outline-secondary" href="{{ url_for('rooms.index') }}">Annuler</a> <button class="btn btn-primary">Créer les locaux</button></div></form></div>{% endblock %}
|
||||
|
|
|
|||
1
app_new/room_profiles/__init__.py
Normal file
1
app_new/room_profiles/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Profils de locaux : propositions validables d'équipements et d'ouvrages."""
|
||||
155
app_new/room_profiles/routes.py
Normal file
155
app_new/room_profiles/routes.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required
|
||||
|
||||
from app_new.extensions import db
|
||||
from app_new.core.models.college import RoomProfile, RoomProfileItem, RoomType, Room, RoomSurface
|
||||
from app_new.core.models.equipment import EquipmentCategory
|
||||
from app_new.core.models.maintenance import Lot
|
||||
from app_new.core.services.equipment_creation import create_equipment
|
||||
|
||||
room_profiles_bp = Blueprint("room_profiles", __name__, template_folder="templates")
|
||||
|
||||
|
||||
def _context(profile=None):
|
||||
return {
|
||||
"profile": profile,
|
||||
"room_types": RoomType.query.order_by(RoomType.name).all(),
|
||||
"categories": EquipmentCategory.query.order_by(EquipmentCategory.name).all(),
|
||||
"lots": Lot.query.order_by(Lot.name).all(),
|
||||
}
|
||||
|
||||
|
||||
@room_profiles_bp.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
return render_template("room_profiles/index.html", profiles=RoomProfile.query.order_by(RoomProfile.name).all())
|
||||
|
||||
|
||||
@room_profiles_bp.route("/new", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new():
|
||||
if request.method == "POST":
|
||||
name = (request.form.get("name") or "").strip()
|
||||
if not name:
|
||||
flash("Le nom du profil est obligatoire.", "danger")
|
||||
return render_template("room_profiles/form.html", title="Nouveau profil", **_context()), 400
|
||||
if RoomProfile.query.filter(db.func.lower(RoomProfile.name) == name.lower()).first():
|
||||
flash("Un profil porte déjà ce nom.", "danger")
|
||||
return render_template("room_profiles/form.html", title="Nouveau profil", **_context()), 400
|
||||
profile = RoomProfile(
|
||||
name=name,
|
||||
description=(request.form.get("description") or "").strip() or None,
|
||||
room_type_id=request.form.get("room_type_id", type=int) or None,
|
||||
)
|
||||
db.session.add(profile)
|
||||
db.session.commit()
|
||||
flash("Profil créé. Ajoutez ses propositions puis prévisualisez avant toute création.", "success")
|
||||
return redirect(url_for("room_profiles.edit", id=profile.id))
|
||||
return render_template("room_profiles/form.html", title="Nouveau profil", **_context())
|
||||
|
||||
|
||||
@room_profiles_bp.route("/<int:id>/edit", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit(id):
|
||||
profile = RoomProfile.query.get_or_404(id)
|
||||
if request.method == "POST":
|
||||
profile.name = (request.form.get("name") or "").strip()
|
||||
profile.description = (request.form.get("description") or "").strip() or None
|
||||
profile.room_type_id = request.form.get("room_type_id", type=int) or None
|
||||
if not profile.name:
|
||||
flash("Le nom du profil est obligatoire.", "danger")
|
||||
return render_template("room_profiles/form.html", title="Modifier le profil", **_context(profile)), 400
|
||||
db.session.commit()
|
||||
flash("Profil mis à jour.", "success")
|
||||
return redirect(url_for("room_profiles.edit", id=profile.id))
|
||||
return render_template("room_profiles/form.html", title="Modifier le profil", **_context(profile))
|
||||
|
||||
|
||||
@room_profiles_bp.route("/<int:id>/items", methods=["POST"])
|
||||
@login_required
|
||||
def add_item(id):
|
||||
profile = RoomProfile.query.get_or_404(id)
|
||||
label = (request.form.get("label") or "").strip()
|
||||
quantity = request.form.get("quantity", 1, type=int)
|
||||
if not label or not quantity or quantity < 1:
|
||||
flash("Libellé et quantité positive sont obligatoires.", "danger")
|
||||
return redirect(url_for("room_profiles.edit", id=id))
|
||||
item = RoomProfileItem(
|
||||
profile_id=profile.id,
|
||||
label=label,
|
||||
category_id=request.form.get("category_id", type=int) or None,
|
||||
lot_id=request.form.get("lot_id", type=int) or None,
|
||||
quantity=quantity,
|
||||
is_group=request.form.get("is_group") == "1",
|
||||
enabled_by_default=request.form.get("enabled_by_default") == "1",
|
||||
sort_order=request.form.get("sort_order", 0, type=int),
|
||||
)
|
||||
db.session.add(item)
|
||||
db.session.commit()
|
||||
flash("Proposition ajoutée au profil.", "success")
|
||||
return redirect(url_for("room_profiles.edit", id=id))
|
||||
|
||||
|
||||
@room_profiles_bp.route("/<int:profile_id>/items/<int:item_id>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_item(profile_id, item_id):
|
||||
item = RoomProfileItem.query.filter_by(id=item_id, profile_id=profile_id).first_or_404()
|
||||
db.session.delete(item)
|
||||
db.session.commit()
|
||||
flash("Proposition retirée du profil.", "success")
|
||||
return redirect(url_for("room_profiles.edit", id=profile_id))
|
||||
|
||||
|
||||
@room_profiles_bp.route("/<int:id>/preview", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def preview(id):
|
||||
profile = RoomProfile.query.get(id)
|
||||
if profile is None:
|
||||
from flask import abort
|
||||
abort(404)
|
||||
room_ids = request.form.getlist("room_ids", type=int) if request.method == "POST" else request.args.getlist("room_id", type=int)
|
||||
rooms = Room.query.filter(Room.id.in_(room_ids)).order_by(Room.name).all() if room_ids else []
|
||||
items = [item for item in profile.items if item.enabled_by_default]
|
||||
if request.method == "POST" and request.form.get("validate") == "1":
|
||||
selected = {int(value) for value in request.form.getlist("item_ids") if value.isdigit()}
|
||||
if not rooms or not selected:
|
||||
flash("Sélectionnez au moins un local et une proposition.", "danger")
|
||||
return render_template("room_profiles/preview.html", profile=profile, rooms=rooms, items=items)
|
||||
created = 0
|
||||
for room in rooms:
|
||||
for item in items:
|
||||
if item.id not in selected:
|
||||
continue
|
||||
quantity = request.form.get(f"quantity_{item.id}", item.quantity, type=int)
|
||||
if quantity < 1:
|
||||
continue
|
||||
create_equipment(
|
||||
name=item.label,
|
||||
category_id=item.category_id,
|
||||
lot_id=item.lot_id,
|
||||
room_id=room.id,
|
||||
quantity=quantity,
|
||||
is_group=item.is_group,
|
||||
)
|
||||
created += 1
|
||||
db.session.commit()
|
||||
flash(f"{created} proposition(s) appliquée(s). Les équipements ont été créés via le service commun.", "success")
|
||||
return redirect(url_for("rooms.index"))
|
||||
return render_template("room_profiles/preview.html", profile=profile, rooms=rooms, items=items)
|
||||
|
||||
|
||||
@room_profiles_bp.route("/<int:id>/surfaces", methods=["POST"])
|
||||
@login_required
|
||||
def add_surface(id):
|
||||
profile = RoomProfile.query.get_or_404(id)
|
||||
surface_type = request.form.get("surface_type")
|
||||
material = (request.form.get("material") or "").strip()
|
||||
if surface_type not in {"mur", "sol", "plafond"} or not material:
|
||||
flash("Type et matériau de surface obligatoires.", "danger")
|
||||
return redirect(url_for("room_profiles.edit", id=id))
|
||||
db.session.add(RoomSurface(profile_id=profile.id, surface_type=surface_type, material=material,
|
||||
finish=(request.form.get("finish") or "").strip() or None,
|
||||
label=(request.form.get("label") or "").strip() or None))
|
||||
db.session.commit()
|
||||
flash("Composition proposée ajoutée.", "success")
|
||||
return redirect(url_for("room_profiles.edit", id=id))
|
||||
7
app_new/room_profiles/templates/room_profiles/form.html
Normal file
7
app_new/room_profiles/templates/room_profiles/form.html
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid py-3" style="max-width:1100px"><h1 class="h3">{{ title }}</h1><p class="text-muted">Un profil propose des équipements et des ouvrages. Rien n'est créé avant validation dans la prévisualisation.</p>
|
||||
<form method="post" class="card mb-4"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><div class="card-body row g-3"><div class="col-md-6"><label class="form-label">Nom</label><input class="form-control" name="name" value="{{ profile.name if profile else '' }}" required></div><div class="col-md-6"><label class="form-label">Type structurel de local (facultatif)</label><select class="form-select" name="room_type_id"><option value="">Aucun</option>{% for rt in room_types %}<option value="{{ rt.id }}" {% if profile and profile.room_type_id == rt.id %}selected{% endif %}>{{ rt.name }}</option>{% endfor %}</select></div><div class="col-12"><label class="form-label">Description</label><textarea class="form-control" name="description" rows="2">{{ profile.description if profile else '' }}</textarea></div></div><div class="card-footer"><button class="btn btn-primary">Enregistrer</button><a class="btn btn-link" href="{{ url_for('room_profiles.index') }}">Annuler</a></div></form>
|
||||
{% if profile %}<div class="row g-4"><div class="col-lg-7"><div class="card"><div class="card-header"><strong>Équipements proposés</strong></div><div class="card-body"><form method="post" action="{{ url_for('room_profiles.add_item', id=profile.id) }}" class="row g-2 align-items-end mb-3"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><div class="col-md-4"><label class="form-label">Équipement</label><input class="form-control" name="label" placeholder="Luminaire" required></div><div class="col-md-2"><label class="form-label">Qté</label><input class="form-control" type="number" min="1" name="quantity" value="1" required></div><div class="col-md-3"><label class="form-label">Catégorie</label><select class="form-select" name="category_id"><option value="">À choisir</option>{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}</select></div><div class="col-md-3"><label class="form-label">Lot suggéré</label><select class="form-select" name="lot_id"><option value="">Aucun</option>{% for lot in lots %}<option value="{{ lot.id }}">{{ lot.name }}</option>{% endfor %}</select></div><div class="col-md-3"><label class="form-label">Mode</label><select class="form-select" name="is_group"><option value="0">Individuel</option><option value="1">Groupe</option></select></div><div class="col-md-3"><label class="form-label">Par défaut</label><select class="form-select" name="enabled_by_default"><option value="1">Oui</option><option value="0">Non</option></select></div><div class="col-md-3"><button class="btn btn-outline-primary w-100">Ajouter</button></div></form><div class="table-responsive"><table class="table table-sm"><thead><tr><th>Proposition</th><th>Qté</th><th>Mode</th><th></th></tr></thead><tbody>{% for item in profile.items %}<tr><td>{{ item.label }}{% if item.category %}<small class="text-muted"> · {{ item.category.name }}</small>{% endif %}</td><td>{{ item.quantity }}</td><td>{{ 'Groupe' if item.is_group else 'Individuel' }}</td><td><form method="post" action="{{ url_for('room_profiles.delete_item', profile_id=profile.id, item_id=item.id) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn btn-sm btn-outline-danger">Retirer</button></form></td></tr>{% else %}<tr><td colspan="4" class="text-muted">Aucune proposition.</td></tr>{% endfor %}</tbody></table></div></div></div></div><div class="col-lg-5"><div class="card"><div class="card-header"><strong>Composition facultative</strong></div><div class="card-body"><form method="post" action="{{ url_for('room_profiles.add_surface', id=profile.id) }}" class="row g-2"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><div class="col-6"><label class="form-label">Surface</label><select class="form-select" name="surface_type"><option value="mur">Mur</option><option value="sol">Sol</option><option value="plafond">Plafond</option></select></div><div class="col-6"><label class="form-label">Matériau</label><input class="form-control" name="material" placeholder="Béton" required></div><div class="col-6"><label class="form-label">Finition</label><input class="form-control" name="finish" placeholder="Peinture"></div><div class="col-6"><label class="form-label">Repère</label><input class="form-control" name="label" placeholder="Mur 1"></div><div class="col-12"><button class="btn btn-outline-secondary">Ajouter une composition</button></div></form></div></div><div class="card mt-3"><div class="card-body"><h2 class="h6">Application</h2><p class="small text-muted">Sélectionnez des locaux puis validez chaque proposition dans l'écran de prévisualisation.</p><a class="btn btn-primary" href="{{ url_for('room_profiles.preview', id=profile.id) }}">Prévisualiser l'application</a></div></div></div></div>{% endif %}</div>
|
||||
{% endblock %}
|
||||
8
app_new/room_profiles/templates/room_profiles/index.html
Normal file
8
app_new/room_profiles/templates/room_profiles/index.html
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}Profils de locaux{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid py-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3"><div><h1 class="h3 mb-1">Profils de locaux</h1><p class="text-muted mb-0">Des propositions réutilisables, jamais des créations automatiques.</p></div><a class="btn btn-primary" href="{{ url_for('room_profiles.new') }}">Nouveau profil</a></div>
|
||||
<div class="row g-3">{% for profile in profiles %}<div class="col-md-6 col-xl-4"><div class="card h-100"><div class="card-body"><h2 class="h5">{{ profile.name }}</h2><p class="text-muted">{{ profile.description or 'Aucune description.' }}</p><div class="small">{{ profile.items|length }} proposition(s){% if profile.room_type %} · {{ profile.room_type.name }}{% endif %}</div></div><div class="card-footer bg-transparent"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('room_profiles.edit', id=profile.id) }}">Modifier / détailler</a></div></div></div>{% else %}<div class="col-12"><div class="alert alert-info">Aucun profil. Vous pouvez créer des locaux sans profil et compléter plus tard.</div></div>{% endfor %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}Prévisualiser un profil{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid py-3" style="max-width:1100px"><h1 class="h3">Prévisualiser « {{ profile.name }} »</h1><p class="text-muted">Aucun équipement ne sera créé avant votre validation explicite.</p><form method="post"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><div class="card mb-3"><div class="card-header">1. Locaux concernés</div><div class="card-body"><div class="row g-2">{% for room in rooms %}<div class="col-md-4"><label class="form-check"><input class="form-check-input" type="checkbox" name="room_ids" value="{{ room.id }}" checked><span class="form-check-label">{{ room.full_name }}</span></label></div>{% else %}<div class="col-12"><div class="alert alert-warning">Aucun local sélectionné. Revenez depuis un parcours de sélection de locaux.</div></div>{% endfor %}</div></div></div><div class="card mb-3"><div class="card-header">2. Propositions à accepter ou modifier</div><div class="card-body"><div class="table-responsive"><table class="table"><thead><tr><th>Accepter</th><th>Équipement</th><th>Quantité</th><th>Catégorie / lot</th></tr></thead><tbody>{% for item in items %}<tr><td><input class="form-check-input" type="checkbox" name="item_ids" value="{{ item.id }}" checked></td><td>{{ item.label }}</td><td><input class="form-control" type="number" min="1" name="quantity_{{ item.id }}" value="{{ item.quantity }}"></td><td>{{ item.category.name if item.category else 'Catégorie à préciser' }}{% if item.lot %} / {{ item.lot.name }}{% endif %}</td></tr>{% else %}<tr><td colspan="4">Ce profil ne contient aucune proposition.</td></tr>{% endfor %}</tbody></table></div></div></div><button class="btn btn-primary" name="validate" value="1">Valider et créer les équipements</button><a class="btn btn-link" href="{{ url_for('room_profiles.edit', id=profile.id) }}">Annuler</a></form></div>
|
||||
{% endblock %}
|
||||
|
|
@ -54,6 +54,16 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Profil de propositions</label>
|
||||
<select name="room_profile_id" class="form-select">
|
||||
<option value="">— Aucun —</option>
|
||||
{% for profile in room_profiles %}
|
||||
<option value="{{ profile.id }}" {% if form_data.get('room_profile_id', room.room_profile_id if room else '')|string == profile.id|string %}selected{% endif %}>{{ profile.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="text-muted">Le profil ne crée aucun équipement automatiquement.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Enregistrer</button>
|
||||
|
|
|
|||
69
migrations/versions/k6f7a8b9c0d1_room_profiles.py
Normal file
69
migrations/versions/k6f7a8b9c0d1_room_profiles.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""Checkpoint B: profils de locaux et compositions facultatives."""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "k6f7a8b9c0d1"
|
||||
down_revision = "j5e6f7a8b9c0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"room_profiles",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("name", sa.String(120), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("room_type_id", sa.Integer(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["room_type_id"], ["room_types.id"]),
|
||||
sa.UniqueConstraint("name", name="uq_room_profiles_name"),
|
||||
)
|
||||
op.create_index("ix_room_profiles_room_type_id", "room_profiles", ["room_type_id"])
|
||||
op.create_table(
|
||||
"room_profile_items",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("profile_id", sa.Integer(), nullable=False),
|
||||
sa.Column("label", sa.String(200), nullable=False),
|
||||
sa.Column("category_id", sa.Integer(), nullable=True),
|
||||
sa.Column("lot_id", sa.Integer(), nullable=True),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False, server_default="1"),
|
||||
sa.Column("is_group", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("enabled_by_default", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.ForeignKeyConstraint(["profile_id"], ["room_profiles.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["category_id"], ["equipment_categories.id"]),
|
||||
sa.ForeignKeyConstraint(["lot_id"], ["lots.id"]),
|
||||
)
|
||||
op.create_index("ix_room_profile_items_profile_id", "room_profile_items", ["profile_id"])
|
||||
op.create_index("ix_room_profile_items_category_id", "room_profile_items", ["category_id"])
|
||||
op.create_index("ix_room_profile_items_lot_id", "room_profile_items", ["lot_id"])
|
||||
op.create_table(
|
||||
"room_surfaces",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("room_id", sa.Integer(), nullable=True),
|
||||
sa.Column("profile_id", sa.Integer(), nullable=True),
|
||||
sa.Column("surface_type", sa.String(30), nullable=False),
|
||||
sa.Column("material", sa.String(80), nullable=False),
|
||||
sa.Column("finish", sa.String(80), nullable=True),
|
||||
sa.Column("label", sa.String(120), nullable=True),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.ForeignKeyConstraint(["room_id"], ["rooms.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["profile_id"], ["room_profiles.id"], ondelete="CASCADE"),
|
||||
)
|
||||
op.create_index("ix_room_surfaces_room_id", "room_surfaces", ["room_id"])
|
||||
op.create_index("ix_room_surfaces_profile_id", "room_surfaces", ["profile_id"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("ix_room_surfaces_profile_id", table_name="room_surfaces")
|
||||
op.drop_index("ix_room_surfaces_room_id", table_name="room_surfaces")
|
||||
op.drop_table("room_surfaces")
|
||||
op.drop_index("ix_room_profile_items_lot_id", table_name="room_profile_items")
|
||||
op.drop_index("ix_room_profile_items_category_id", table_name="room_profile_items")
|
||||
op.drop_index("ix_room_profile_items_profile_id", table_name="room_profile_items")
|
||||
op.drop_table("room_profile_items")
|
||||
op.drop_index("ix_room_profiles_room_type_id", table_name="room_profiles")
|
||||
op.drop_table("room_profiles")
|
||||
20
migrations/versions/l7a8b9c0d1e2_assign_room_profiles.py
Normal file
20
migrations/versions/l7a8b9c0d1e2_assign_room_profiles.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Checkpoint B: associer un profil validé à un local."""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "l7a8b9c0d1e2"
|
||||
down_revision = "k6f7a8b9c0d1"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column("rooms", sa.Column("room_profile_id", sa.Integer(), nullable=True))
|
||||
op.create_foreign_key("fk_rooms_room_profile_id", "rooms", "room_profiles", ["room_profile_id"], ["id"])
|
||||
op.create_index("ix_rooms_room_profile_id", "rooms", ["room_profile_id"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("ix_rooms_room_profile_id", table_name="rooms")
|
||||
op.drop_constraint("fk_rooms_room_profile_id", "rooms", type_="foreignkey")
|
||||
op.drop_column("rooms", "room_profile_id")
|
||||
58
tests/integration/test_room_profiles_checkpoint_b.py
Normal file
58
tests/integration/test_room_profiles_checkpoint_b.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""Couverture métier du checkpoint B : profils sans création implicite."""
|
||||
from app_new import db
|
||||
from app_new.core.models.college import Building, Zone, Room, RoomProfile, RoomProfileItem
|
||||
from app_new.core.models.equipment import Equipment
|
||||
from app_new.core.services.equipment_creation import create_equipment
|
||||
from app_new.core.models.equipment import EquipmentCategory
|
||||
|
||||
|
||||
def test_room_profile_is_editable_and_application_uses_shared_service(authenticated_client, app):
|
||||
with app.app_context():
|
||||
building = Building(name="TEST_UI_020_B_BUILDING")
|
||||
db.session.add(building)
|
||||
db.session.flush()
|
||||
zone = Zone(name="TEST_UI_020_B_ZONE", building_id=building.id)
|
||||
category = EquipmentCategory(name="TEST_UI_020_B_LUMINAIRE")
|
||||
db.session.add_all([zone, category])
|
||||
db.session.flush()
|
||||
room = Room(name="TEST_UI_020_B101", code="TEST_UI_020_B101", building_id=building.id, zone_id=zone.id)
|
||||
db.session.add(room)
|
||||
db.session.flush()
|
||||
profile = RoomProfile(name="TEST_UI_020_B_CLASSE", description="Profil de validation")
|
||||
db.session.add(profile)
|
||||
db.session.flush()
|
||||
item = RoomProfileItem(profile_id=profile.id, label="Luminaire", category_id=category.id, quantity=10, is_group=True)
|
||||
db.session.add(item)
|
||||
db.session.commit()
|
||||
profile_id, room_id, item_id = profile.id, room.id, item.id
|
||||
assert RoomProfile.query.get(profile_id) is not None
|
||||
|
||||
assert Equipment.query.filter_by(room_id=room_id).count() == 0
|
||||
with app.app_context():
|
||||
item = RoomProfileItem.query.get(item_id)
|
||||
created = create_equipment(name=item.label, category_id=item.category_id,
|
||||
room_id=room_id, quantity=4, is_group=item.is_group,
|
||||
create_schedule=False)
|
||||
db.session.commit()
|
||||
assert created.id is not None
|
||||
assert Equipment.query.filter_by(room_id=room_id).count() == 1
|
||||
assert Equipment.query.filter_by(room_id=room_id).one().quantity == 4
|
||||
|
||||
|
||||
def test_bulk_room_can_reference_profile_without_creating_equipment(authenticated_client, app):
|
||||
with app.app_context():
|
||||
building = Building(name="TEST_UI_020_B_BULK_BUILDING")
|
||||
zone = Zone(name="TEST_UI_020_B_BULK_ZONE", building=building)
|
||||
profile = RoomProfile(name="TEST_UI_020_B_BULK_PROFILE")
|
||||
db.session.add_all([building, zone, profile])
|
||||
db.session.commit()
|
||||
building_id, zone_id, profile_id = building.id, zone.id, profile.id
|
||||
with app.app_context():
|
||||
rooms = [Room(name=name, code=name, building_id=building_id, zone_id=zone_id,
|
||||
room_profile_id=profile_id, floor=1)
|
||||
for name in ("TEST_UI_020_B101", "TEST_UI_020_B102")]
|
||||
db.session.add_all(rooms)
|
||||
db.session.commit()
|
||||
saved = Room.query.filter(Room.building_id == building_id, Room.room_profile_id == profile_id).all()
|
||||
assert len(saved) == 2
|
||||
assert Equipment.query.filter(Equipment.room_id.in_([room.id for room in saved])).count() == 0
|
||||
Loading…
Reference in a new issue