# ----------------------------- # 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")