244 lines
12 KiB
Python
244 lines
12 KiB
Python
# -----------------------------
|
|
# Public Buildings Models
|
|
# -----------------------------
|
|
|
|
from django.contrib.gis.db import models
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.contrib.auth.models import User
|
|
|
|
from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel, ASSET_STATUS_CHOICES
|
|
from common.models import Document
|
|
from common.storages import PrivateMediaStorage
|
|
|
|
private_storage = PrivateMediaStorage()
|
|
|
|
def get_usage_type_choices(standard='BE'):
|
|
"""
|
|
Renvoie la liste des choix de type d'usage adaptés à la norme régionale active (BE, FR, INT).
|
|
"""
|
|
from common.utils import get_regional_terms
|
|
terms = get_regional_terms(standard)
|
|
admin_label = _("Administratif") + f" ({terms['admin_building']})"
|
|
return (
|
|
('administrative', admin_label),
|
|
('education', _('Enseignement / École / Crèche')),
|
|
('sports', _('Sport & Équipement sportif')),
|
|
('culture', _('Culture / Médiathèque / Musée')),
|
|
('social', _('Social / Centre communautaire')),
|
|
('technical', _('Technique / Ateliers / Stock')),
|
|
('housing', _('Logement communal')),
|
|
('other', _('Autre bâtiment public')),
|
|
)
|
|
|
|
|
|
USAGE_TYPE_CHOICES = get_usage_type_choices('BE')
|
|
|
|
BUILDING_STATUS_CHOICES = (
|
|
('in_service', _('En service')),
|
|
('under_renovation', _('En travaux / Rénovation')),
|
|
('decommissioned', _('Désaffecté')),
|
|
('ceded', _('Cédé / Vendu')),
|
|
)
|
|
|
|
DPE_RATING_CHOICES = (
|
|
('A', 'A'),
|
|
('B', 'B'),
|
|
('C', 'C'),
|
|
('D', 'D'),
|
|
('E', 'E'),
|
|
('F', 'F'),
|
|
('G', 'G'),
|
|
)
|
|
|
|
ERP_CATEGORY_CHOICES = (
|
|
('1', _('1ère catégorie (> 1500 pers.)')),
|
|
('2', _('2ème catégorie (301 - 1500 pers.)')),
|
|
('3', _('3ème catégorie (101 - 300 pers.)')),
|
|
('4', _('4ème catégorie (jusqu\'à 100 pers.)')),
|
|
('5', _('5ème catégorie (seuils spécifiques)')),
|
|
)
|
|
|
|
ERP_TYPE_CHOICES = (
|
|
('J', _('J - Structures d\'accueil pour personnes âgées ou handicapées')),
|
|
('L', _('L - Salles de spectacles, de réunions, polyvalentes')),
|
|
('M', _('M - Magasins, centres commerciaux')),
|
|
('N', _('N - Restaurants et débits de boissons')),
|
|
('O', _('O - Hôtels et pensions de famille')),
|
|
('P', _('P - Salles de danse et salles de jeux')),
|
|
('R', _('R - Établissements d\'éveil, d\'enseignement, colonies de vacances')),
|
|
('S', _('S - Bibliothèques, centres de documentation')),
|
|
('T', _('T - Salles d\'expositions')),
|
|
('U', _('U - Établissements de soins')),
|
|
('V', _('V - Établissements de culte')),
|
|
('W', _('W - Administrations, banques, bureaux')),
|
|
('X', _('X - Établissements sportifs couverts')),
|
|
('Y', _('Y - Musées')),
|
|
)
|
|
|
|
LOCATION_TYPE_CHOICES = (
|
|
('site', _('Site / Complexe')),
|
|
('building', _('Bâtiment / Bloc')),
|
|
('level', _('Niveau / Étage')),
|
|
('room', _('Pièce / Local')),
|
|
('other', _('Autre localisation')),
|
|
)
|
|
|
|
BUILDING_ASSET_TYPE_CHOICES = (
|
|
('civil_engineering', _('Génie civil / Bâti')),
|
|
('facade', _('Façade / Mur extérieur')),
|
|
('wall', _('Mur intérieur / Cloison')),
|
|
('roof', _('Toiture / Couverture')),
|
|
('hvac', _('Chauffage / CVC / Chaudière')),
|
|
('elevator', _('Ascenseur / Monte-charge')),
|
|
('electricity', _('Électricité / TGBT')),
|
|
('fire_safety', _('Sécurité incendie / Extincteur / RIA')),
|
|
('alarm', _('Alarme / Contrôle d\'accès')),
|
|
('meter', _('Compteur (Eau / Gaz / Électricité)')),
|
|
('sanitary', _('Plomberie / Sanitaires')),
|
|
('other', _('Autre équipement / asset')),
|
|
)
|
|
|
|
# For backward compatibility
|
|
EQUIPMENT_TYPE_CHOICES = BUILDING_ASSET_TYPE_CHOICES
|
|
|
|
|
|
class PublicBuilding(AbstractLocation):
|
|
"""
|
|
Modèle représentant une Localisation du Patrimoine Bâti (Complexe, Bâtiment, Bloc, Étage, Local).
|
|
Les localisations peuvent former une arborescence parent-enfant.
|
|
"""
|
|
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise cartographique"))
|
|
parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations", verbose_name=_("Localisation parente"))
|
|
location_type = models.CharField(max_length=50, choices=LOCATION_TYPE_CHOICES, default='building', verbose_name=_("Type de localisation"))
|
|
is_main_location = models.BooleanField(default=False, verbose_name=_("Localisation principale"))
|
|
|
|
address = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Adresse complète"))
|
|
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
|
|
|
|
# Propriété & Gestion
|
|
owner = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Propriétaire"))
|
|
manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="managed_buildings", verbose_name=_("Gestionnaire référent"))
|
|
occupant_entity = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Direction / Service occupant"))
|
|
|
|
# Usage & Statut
|
|
usage_type = models.CharField(max_length=50, choices=USAGE_TYPE_CHOICES, default='administrative', verbose_name=_("Type d'usage principal"))
|
|
building_status = models.CharField(max_length=50, choices=BUILDING_STATUS_CHOICES, default='in_service', verbose_name=_("Statut d'occupation"))
|
|
|
|
# Caractéristiques physiques
|
|
construction_year = models.IntegerField(null=True, blank=True, verbose_name=_("Année de construction"))
|
|
floors_count = models.IntegerField(default=1, verbose_name=_("Nombre d'étages hors-sol"))
|
|
basement_count = models.IntegerField(default=0, verbose_name=_("Nombre de niveaux en sous-sol"))
|
|
total_surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface brute totale (m²)"))
|
|
usable_surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface utile (m²)"))
|
|
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface spécifique (m²)"))
|
|
level_number = models.IntegerField(default=0, verbose_name=_("Numéro d'étage (ex: 0=RDC, 1=1er, -1=Sous-sol)"))
|
|
floor_plan = models.FileField(upload_to='buildings/plans/%Y/%m/', storage=private_storage, null=True, blank=True, verbose_name=_("Plan d'étage"))
|
|
|
|
# Réglementation & Accueil du Public
|
|
is_erp = models.BooleanField(default=True, verbose_name=_("Accueil & Sécurité du Public (ERP / PMR)"))
|
|
erp_category = models.CharField(max_length=10, choices=ERP_CATEGORY_CHOICES, null=True, blank=True, verbose_name=_("Catégorie d'accueil / ERP"))
|
|
erp_type = models.CharField(max_length=10, choices=ERP_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Type d'établissement / ERP"))
|
|
max_capacity = models.IntegerField(null=True, blank=True, verbose_name=_("Jauge maximale d'accueil du public"))
|
|
|
|
# Diagnostics & Énergie
|
|
dpe_rating = models.CharField(max_length=2, choices=DPE_RATING_CHOICES, null=True, blank=True, verbose_name=_("Performance Énergétique (PEB / DPE / EPC)"))
|
|
ges_rating = models.CharField(max_length=2, choices=DPE_RATING_CHOICES, null=True, blank=True, verbose_name=_("Émissions de Carbone & GES"))
|
|
has_asbestos_diag = models.BooleanField(default=False, verbose_name=_("Diagnostic Amiante réalisé"))
|
|
has_lead_diag = models.BooleanField(default=False, verbose_name=_("Diagnostic Plomb réalisé"))
|
|
pmr_accessible = models.BooleanField(default=False, verbose_name=_("Accessible PMR"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Localisation Bâtiment public")
|
|
verbose_name_plural = _("Localisations Bâtiments publics")
|
|
ordering = ['code', 'name_fr']
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('assets:publicbuilding_detail', kwargs={'pk': self.pk})
|
|
|
|
def get_usage_type_display(self, standard=None):
|
|
"""
|
|
Renvoie le libellé du type d'usage adapté dynamiquement à la norme régionale (BE, FR, INT).
|
|
"""
|
|
from common.utils import get_regional_terms
|
|
if self.usage_type == 'administrative':
|
|
terms = get_regional_terms(standard or getattr(self, '_regional_standard', 'BE'))
|
|
return f"Administratif ({terms['admin_building']})"
|
|
return dict(USAGE_TYPE_CHOICES).get(self.usage_type, self.usage_type)
|
|
|
|
def get_child_locations(self):
|
|
return self.child_locations.all()
|
|
|
|
def get_active_assets(self):
|
|
def collect_assets(loc):
|
|
assets = list(loc.location_assets.filter(status='active'))
|
|
for child in loc.child_locations.all():
|
|
assets += collect_assets(child)
|
|
return assets
|
|
return collect_assets(self)
|
|
|
|
def get_all_assets(self):
|
|
def collect_assets(loc):
|
|
if loc.parent_location is None:
|
|
assets = list(loc.location_assets.select_related('location', 'parent_asset', 'building').all() | loc.all_building_assets.select_related('location', 'parent_asset', 'building').filter(location__isnull=True))
|
|
else:
|
|
assets = list(loc.location_assets.select_related('location', 'parent_asset', 'building').all())
|
|
for child in loc.child_locations.all():
|
|
assets += collect_assets(child)
|
|
return assets
|
|
|
|
seen = set()
|
|
unique_assets = []
|
|
for a in collect_assets(self):
|
|
if a.id not in seen:
|
|
seen.add(a.id)
|
|
unique_assets.append(a)
|
|
return unique_assets
|
|
|
|
def get_direct_child_assets(self):
|
|
return self.location_assets.all()
|
|
|
|
|
|
class BuildingAsset(AbstractGeoAsset):
|
|
"""
|
|
Modèle représentant un asset ou équipement physique du bâtiment (Génie civil / Bâti, Façades, Toitures, CVC, Ascenseurs...).
|
|
Les assets peuvent former une arborescence parent-enfant (ex: Génie civil -> Façade Nord, Toiture, Ascenseur).
|
|
"""
|
|
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="all_building_assets", verbose_name=_("Bâtiment / Site principal"))
|
|
location = models.ForeignKey(PublicBuilding, on_delete=models.SET_NULL, null=True, blank=True, related_name="location_assets", verbose_name=_("Localisation spécifique"))
|
|
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets", verbose_name=_("Asset parent"))
|
|
asset_type = models.CharField(max_length=50, choices=BUILDING_ASSET_TYPE_CHOICES, default='civil_engineering', verbose_name=_("Type d'asset / Équipement"))
|
|
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle réglementaire"))
|
|
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle réglementaire"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Asset / Équipement de bâtiment")
|
|
verbose_name_plural = _("Assets / Équipements de bâtiment")
|
|
ordering = ['building', 'code']
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': self.pk})
|
|
|
|
def get_child_assets(self):
|
|
return self.child_assets.all()
|
|
|
|
|
|
# Backward compatibility alias
|
|
BuildingEquipment = BuildingAsset
|
|
|
|
|
|
def upload_to_building(instance, filename):
|
|
return f"buildings/{instance.building.code}/{filename}"
|
|
|
|
|
|
class PublicBuildingDocument(Document):
|
|
"""
|
|
Documents associés à un bâtiment public (plans, notices ERP, DPE).
|
|
"""
|
|
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="documents", verbose_name=_("Bâtiment"))
|
|
file = models.FileField(upload_to=upload_to_building, storage=private_storage, verbose_name=_("Fichier"), max_length=255)
|
|
document_type = models.CharField(max_length=50, default='other', verbose_name=_("Type de document"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Document de bâtiment")
|
|
verbose_name_plural = _("Documents de bâtiment")
|