This commit is contained in:
Karl Determe 2026-08-04 11:13:27 +02:00
commit c139343278
212 changed files with 193538 additions and 31919 deletions

View file

@ -55,7 +55,6 @@ RUN pip install --no-cache-dir -r requirements/base.txt && \
fi fi
# 1. Copie des répertoires volumineux et statiques (très bonne mise en cache) # 1. Copie des répertoires volumineux et statiques (très bonne mise en cache)
COPY streetup/road_detection/detection_models/ /app/road_detection/detection_models/
COPY streetup/scripts/ /app/scripts/ COPY streetup/scripts/ /app/scripts/
COPY streetup/common/ /app/common/ COPY streetup/common/ /app/common/
COPY streetup/home/ /app/home/ COPY streetup/home/ /app/home/
@ -72,7 +71,7 @@ COPY streetup/maps/ /app/maps/
COPY streetup/notifications/ /app/notifications/ COPY streetup/notifications/ /app/notifications/
COPY streetup/projects/ /app/projects/ COPY streetup/projects/ /app/projects/
COPY streetup/reporting/ /app/reporting/ COPY streetup/reporting/ /app/reporting/
COPY streetup/schools/ /app/schools/ COPY streetup/bim/ /app/bim/
COPY streetup/sign/ /app/sign/ COPY streetup/sign/ /app/sign/
COPY streetup/stock/ /app/stock/ COPY streetup/stock/ /app/stock/
@ -83,6 +82,7 @@ COPY streetup/observations/ /app/observations/
COPY streetup/controls/ /app/controls/ COPY streetup/controls/ /app/controls/
COPY streetup/interventions/ /app/interventions/ COPY streetup/interventions/ /app/interventions/
COPY streetup/assets/ /app/assets/ COPY streetup/assets/ /app/assets/
COPY streetup/panoramax/ /app/panoramax/
# 4. Copie des fichiers de code de road_detection (sans réimporter les modèles de détection) # 4. Copie des fichiers de code de road_detection (sans réimporter les modèles de détection)
COPY streetup/road_detection/__init__.py /app/road_detection/ COPY streetup/road_detection/__init__.py /app/road_detection/

View file

@ -1,6 +1,15 @@
# StreetUp — Street Asset Management System # StreetUp — Asset Management pour l'espace public
**StreetUp** est une application web complète de gestion des actifs de voirie (Street Asset Management System). Elle permet aux administrations publiques, gestionnaires d'infrastructures et prestataires de maintenance de cartographier, de suivre et de maintenir l'ensemble des équipements et infrastructures présents sur l'espace public. **StreetUp** est une application web complète de gestion des actifs de l'espace public. Elle permet aux administrations publiques, gestionnaires d'infrastructures et prestataires de maintenance de cartographier, de suivre et de maintenir l'ensemble des équipements et infrastructures présents sur l'espace public.
Le projet est publié sous licence libre **EUPL (European Union Public Licence)**.
Les grandes lignes de la licence EUPL :
* **Liberté d'utilisation, de modification et de redistribution** : Vous êtes libre d'utiliser le logiciel pour tout usage (y compris commercial), d'accéder au code source, de le modifier et de le redistribuer.
* **Réciprocité (Copyleft)** : Tout code dérivé ou modifié qui est redistribué doit rester sous licence EUPL (ou sous une licence compatible), afin de garantir la pérennité du code ouvert.
* **Compatibilité inter-licences** : La licence comporte une clause expresse de compatibilité avec d'autres licences copyleft majeures (GPL v2/v3, AGPL v3, MPL, CeCILL, EPL, etc.), facilitant la réutilisation de composants open source.
* **Cadre juridique européen & multilinguisme** : Conçue par la Commission européenne, elle possède une valeur juridique identique dans les 24 langues officielles de l'Union européenne.
* **Absence de garantie** : Le logiciel est fourni "en l'état", sans garantie explicite ou implicite de la part des contributeurs.
Le projet est propulsé par **Django** et s'appuie sur une base de données **PostgreSQL avec l'extension PostGIS** pour toute la dimension géospatiale et cartographique. Le projet est propulsé par **Django** et s'appuie sur une base de données **PostgreSQL avec l'extension PostGIS** pour toute la dimension géospatiale et cartographique.
@ -10,7 +19,8 @@ Le projet est propulsé par **Django** et s'appuie sur une base de données **Po
StreetUp permet de modéliser et de localiser précisément les équipements liés à plusieurs domaines de la voirie : StreetUp permet de modéliser et de localiser précisément les équipements liés à plusieurs domaines de la voirie :
* **Feux tricolores / de signalisation** : Carrefours à feux, poteaux, lanternes, contrôleurs, câblages et détecteurs. * **Bâtiments** : Bâtiments publics, gestion des salles, etc.
* **Espaces extérieurs** : Plaines de jeux, terrains de sport extérieurs, etc.
* **Éclairage public** : Rues d'éclairage, poteaux, luminaires et caractéristiques d'éclairage. * **Éclairage public** : Rues d'éclairage, poteaux, luminaires et caractéristiques d'éclairage.
* **Signalisation routière** : Panneaux de signalisation (types de panneaux, poteaux supports, etc.). * **Signalisation routière** : Panneaux de signalisation (types de panneaux, poteaux supports, etc.).
* **Ouvrages d'art** : Suivi des tunnels, ponts, viaducs et des équipements associés à ces structures. * **Ouvrages d'art** : Suivi des tunnels, ponts, viaducs et des équipements associés à ces structures.

View file

@ -1,19 +1,21 @@
services: services:
db: db:
image: postgis/postgis:18-3.6 image: postgis/postgis:18-3.6
container_name: streetup_db container_name: streetup_core_db
restart: unless-stopped restart: unless-stopped
ports: ports:
- "5434:5432" # Mappé sur le port 5434 de l'hôte pour éviter tout conflit avec PostgreSQL local - "5436:5432" # Mappé sur le port 5436 de l'hôte
env_file:
- ./streetup/.env
environment: environment:
POSTGRES_DB: streetup POSTGRES_DB: ${DB_NAME:-streetup}
POSTGRES_USER: admin POSTGRES_USER: ${DB_USER:-admin}
POSTGRES_PASSWORD: replace_by_your_pwd POSTGRES_PASSWORD: ${DB_PASSWORD:?La variable DB_PASSWORD est obligatoire dans le fichier .env}
volumes: volumes:
- streetup_db_data:/var/lib/postgresql - streetup_core_db_data:/var/lib/postgresql
- ./streetup/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro - ./streetup/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
healthcheck: healthcheck:
test: [ "CMD-SHELL", "pg_isready -U admin -d streetup" ] test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-admin} -d ${DB_NAME:-streetup}" ]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@ -25,15 +27,15 @@ services:
args: args:
# Mettre "true" pour installer ultralytics, OpenCV et PyTorch dans l'image # Mettre "true" pour installer ultralytics, OpenCV et PyTorch dans l'image
INSTALL_AI_LIBS: "false" INSTALL_AI_LIBS: "false"
container_name: streetup_web container_name: streetup_core_web
restart: unless-stopped restart: unless-stopped
command: python manage.py runserver 0.0.0.0:8000 command: python manage.py runserver 0.0.0.0:8000
ports: ports:
- "8000:8000" - "8003:8000" # Mappé sur le port 8003 de l'hôte
volumes: volumes:
- ./streetup:/app # Volume mount pour le hot-reload local - ./streetup:/app # Volume mount pour le hot-reload local
- streetup_media:/app/media - streetup_core_media:/app/media
- streetup_private_media:/app/private_media - streetup_core_private_media:/app/private_media
env_file: env_file:
- ./streetup/.env # Chargement du fichier d'environnement local - ./streetup/.env # Chargement du fichier d'environnement local
environment: environment:
@ -52,22 +54,20 @@ services:
caddy: caddy:
image: caddy:latest image: caddy:latest
container_name: streetup_caddy container_name: streetup_core_caddy
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:80" - "8083:80" # Mappé sur le port 8083 de l'hôte
- "443:443"
- "443:443/udp"
volumes: volumes:
- ./Caddyfile:/etc/caddy/Caddyfile - ./Caddyfile:/etc/caddy/Caddyfile
- streetup_caddy_data:/data - streetup_core_caddy_data:/data
- streetup_caddy_config:/config - streetup_core_caddy_config:/config
depends_on: depends_on:
- web - web
volumes: volumes:
streetup_db_data: streetup_core_db_data:
streetup_media: streetup_core_media:
streetup_private_media: streetup_core_private_media:
streetup_caddy_data: streetup_core_caddy_data:
streetup_caddy_config: streetup_core_caddy_config:

View file

@ -648,6 +648,37 @@ admin.site.register(CleanGlassContainer, AssetAdmin)
admin.site.register(CleanLocationContract, AssetAdmin) admin.site.register(CleanLocationContract, AssetAdmin)
from .models import CleanLitterBinPhoto, CleanLitterBinObservation from .models import CleanLitterBinPhoto, CleanLitterBinObservation
from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument
from .models import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
admin.site.register(CleanLitterBinPhoto, AssetAdmin) @admin.register(PublicBuilding)
admin.site.register(CleanLitterBinObservation, AssetAdmin) class PublicBuildingAdmin(admin.GISModelAdmin):
list_display = ("code", "name_fr", "location_type", "parent_location", "usage_type", "building_status", "owner", "manager", "is_erp", "dpe_rating")
list_filter = ("location_type", "usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible")
search_fields = ("code", "name_fr", "name_nl", "address", "owner")
raw_id_fields = ("parent_location", "manager")
@admin.register(BuildingAsset)
class BuildingAssetAdmin(admin.GISModelAdmin):
list_display = ("code", "name_fr", "asset_type", "building", "location", "parent_asset", "status", "obsolescence")
list_filter = ("asset_type", "status", "obsolescence")
search_fields = ("code", "name_fr", "name_nl", "serial_number")
raw_id_fields = ("building", "location", "parent_asset")
admin.site.register(PublicBuildingDocument, AssetAdmin)
@admin.register(OutdoorLocation)
class OutdoorLocationAdmin(admin.GISModelAdmin):
list_display = ("code", "name_fr", "site_type", "parent_location", "owner", "manager", "pmr_accessible", "has_water_point", "has_power_supply", "is_fenced")
list_filter = ("site_type", "pmr_accessible", "has_water_point", "has_power_supply", "is_fenced")
search_fields = ("code", "name_fr", "name_nl", "address", "owner")
raw_id_fields = ("parent_location", "manager")
@admin.register(OutdoorAsset)
class OutdoorAssetAdmin(admin.GISModelAdmin):
list_display = ("code", "name_fr", "asset_type", "site", "location", "norm", "last_annual_inspection", "next_annual_inspection", "status")
list_filter = ("asset_type", "norm", "status", "obsolescence")
search_fields = ("code", "name_fr", "name_nl", "serial_number", "certifying_body")
raw_id_fields = ("site", "location", "parent_asset")
admin.site.register(OutdoorLocationDocument, AssetAdmin)

View file

@ -4,6 +4,7 @@ from assets.models import TrafficLightIntersectionDocument, PublicLightingStreet
from assets.models import SignStreetDocument, RoadStreetDocument, ITSLocationDocument from assets.models import SignStreetDocument, RoadStreetDocument, ITSLocationDocument
from assets.models import TrafficLightIntersection from assets.models import TrafficLightIntersection
from assets.models.its import ITSLocation from assets.models.its import ITSLocation
from assets.models.buildings import PublicBuilding
from assets.models.trafficlights import TL_INTERSECTION_STATUS_CHOICES, TL_VOLTAGE_CHOICES from assets.models.trafficlights import TL_INTERSECTION_STATUS_CHOICES, TL_VOLTAGE_CHOICES
@ -271,3 +272,120 @@ LOCATION_FORM_REGISTRY = {
'label': 'Localisation ITS', 'label': 'Localisation ITS',
}, },
} }
from assets.models.buildings import PublicBuilding, BuildingAsset
class PublicBuildingForm(forms.ModelForm):
"""Formulaire de création et d'édition d'une Localisation de Bâtiment Public."""
class Meta:
model = PublicBuilding
fields = [
'code', 'name_fr', 'name_nl', 'parent_location', 'location_type', 'is_main_location',
'address', 'description', 'owner', 'manager', 'occupant_entity',
'usage_type', 'building_status',
'construction_year', 'floors_count', 'basement_count',
'total_surface_m2', 'usable_surface_m2', 'surface_m2', 'level_number',
'is_erp', 'erp_category', 'erp_type', 'max_capacity', 'pmr_accessible',
'dpe_rating', 'ges_rating', 'has_asbestos_diag', 'has_lead_diag',
'lat', 'lon',
]
widgets = {
'code': forms.TextInput(attrs={'class': 'form-control'}),
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
'parent_location': forms.Select(attrs={'class': 'form-select'}),
'location_type': forms.Select(attrs={'class': 'form-select'}),
'is_main_location': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'address': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'owner': forms.TextInput(attrs={'class': 'form-control'}),
'manager': forms.Select(attrs={'class': 'form-select'}),
'occupant_entity': forms.TextInput(attrs={'class': 'form-control'}),
'usage_type': forms.Select(attrs={'class': 'form-select'}),
'building_status': forms.Select(attrs={'class': 'form-select'}),
'construction_year': forms.NumberInput(attrs={'class': 'form-control'}),
'floors_count': forms.NumberInput(attrs={'class': 'form-control'}),
'basement_count': forms.NumberInput(attrs={'class': 'form-control'}),
'total_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
'usable_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
'surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
'level_number': forms.NumberInput(attrs={'class': 'form-control'}),
'is_erp': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'erp_category': forms.Select(attrs={'class': 'form-select'}),
'erp_type': forms.Select(attrs={'class': 'form-select'}),
'max_capacity': forms.NumberInput(attrs={'class': 'form-control'}),
'pmr_accessible': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'dpe_rating': forms.Select(attrs={'class': 'form-select'}),
'ges_rating': forms.Select(attrs={'class': 'form-select'}),
'has_asbestos_diag': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'has_lead_diag': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'lat': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
'lon': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name_fr'].required = False
self.fields['name_nl'].required = False
self.fields['parent_location'].required = False
self.fields['location_type'].required = False
self.fields['address'].required = False
self.fields['description'].required = False
self.fields['owner'].required = False
self.fields['manager'].required = False
self.fields['occupant_entity'].required = False
self.fields['construction_year'].required = False
self.fields['floors_count'].required = False
self.fields['basement_count'].required = False
self.fields['total_surface_m2'].required = False
self.fields['usable_surface_m2'].required = False
self.fields['surface_m2'].required = False
self.fields['level_number'].required = False
self.fields['erp_category'].required = False
self.fields['erp_type'].required = False
self.fields['max_capacity'].required = False
self.fields['dpe_rating'].required = False
self.fields['ges_rating'].required = False
self.fields['lat'].required = False
self.fields['lon'].required = False
class BuildingAssetForm(forms.ModelForm):
"""Formulaire pour les assets/équipements physiques d'un bâtiment."""
class Meta:
model = BuildingAsset
fields = [
'code', 'name_fr', 'name_nl', 'building', 'location', 'parent_asset',
'asset_type', 'category', 'status', 'obsolescence', 'installation_date',
'serial_number', 'last_control_date', 'next_control_date', 'note'
]
widgets = {
'code': forms.TextInput(attrs={'class': 'form-control'}),
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
'building': forms.Select(attrs={'class': 'form-select'}),
'location': forms.Select(attrs={'class': 'form-select'}),
'parent_asset': forms.Select(attrs={'class': 'form-select'}),
'asset_type': forms.Select(attrs={'class': 'form-select'}),
'category': forms.Select(attrs={'class': 'form-select'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'obsolescence': forms.Select(attrs={'class': 'form-select'}),
'installation_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
'serial_number': forms.TextInput(attrs={'class': 'form-control'}),
'last_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'next_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'note': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name in ['name_fr', 'name_nl', 'location', 'parent_asset', 'category', 'installation_date', 'serial_number', 'last_control_date', 'next_control_date', 'note']:
if field_name in self.fields:
self.fields[field_name].required = False
from common.models import Thematic
from assets.models import AssetCategory
t = Thematic.objects.filter(code='publicbuildings').first()
if t and 'category' in self.fields:
self.fields['category'].queryset = AssetCategory.objects.filter(thematic=t)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,171 @@
# Generated by Django 6.0.7 on 2026-07-26 13:20
import assets.models.buildings
import common.storages
import django.contrib.gis.db.models.fields
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0004_initial'),
('documents', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BuildingLevel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50, verbose_name='Code du niveau')),
('name', models.CharField(max_length=200, verbose_name='Nom du niveau')),
('level_number', models.IntegerField(default=0, verbose_name="Numéro d'étage (0=RDC, 1=1er, -1=Sous-sol)")),
('floor_plan', models.FileField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage")),
],
options={
'verbose_name': 'Niveau / Étage',
'verbose_name_plural': 'Niveaux / Étages',
'ordering': ['building', 'level_number'],
},
),
migrations.CreateModel(
name='PublicBuilding',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
('installation_date', models.DateTimeField(blank=True, null=True)),
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
('last_survey_date', models.DateTimeField(blank=True, null=True)),
('brand', models.CharField(blank=True, max_length=500, null=True)),
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
('updated_at', models.DateTimeField(auto_now=True)),
('lon', models.FloatField(blank=True, null=True)),
('lat', models.FloatField(blank=True, null=True)),
('geojson', models.TextField(blank=True, null=True)),
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise cartographique')),
('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse complète')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('owner', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propriétaire')),
('occupant_entity', models.CharField(blank=True, max_length=255, null=True, verbose_name='Direction / Service occupant')),
('usage_type', models.CharField(choices=[('administrative', 'Administratif / Mairie'), ('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')], default='administrative', max_length=50, verbose_name="Type d'usage principal")),
('building_status', models.CharField(choices=[('in_service', 'En service'), ('under_renovation', 'En travaux / Rénovation'), ('decommissioned', 'Désaffecté'), ('ceded', 'Cédé / Vendu')], default='in_service', max_length=50, verbose_name="Statut d'occupation")),
('construction_year', models.IntegerField(blank=True, null=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(blank=True, null=True, verbose_name='Surface brute totale (m²)')),
('usable_surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface utile (m²)')),
('is_erp', models.BooleanField(default=True, verbose_name='Établissement Recevant du Public (ERP)')),
('erp_category', models.CharField(blank=True, 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)')], max_length=10, null=True, verbose_name='Catégorie ERP')),
('erp_type', models.CharField(blank=True, 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')], max_length=10, null=True, verbose_name='Type ERP')),
('max_capacity', models.IntegerField(blank=True, null=True, verbose_name="Jauge maximale d'accueil du public")),
('dpe_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Diagnostic de Performance Énergétique (DPE)')),
('ges_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Émission de Gaz à Effet de Serre (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')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_buildings', to=settings.AUTH_USER_MODEL, verbose_name='Gestionnaire référent')),
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.publicbuilding', verbose_name='Remplacé par')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
],
options={
'verbose_name': 'Bâtiment public',
'verbose_name_plural': 'Bâtiments publics',
'ordering': ['code', 'name_fr'],
},
),
migrations.CreateModel(
name='BuildingRoom',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50, verbose_name='Code du local')),
('name', models.CharField(max_length=200, verbose_name='Nom de la pièce')),
('room_type', models.CharField(blank=True, max_length=100, null=True, verbose_name='Type de local (ex: Bureau, Classe, Sanitaires)')),
('surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface (m²)')),
('level', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rooms', to='assets.buildinglevel', verbose_name='Niveau / Étage')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='assets.publicbuilding', verbose_name='Bâtiment')),
],
options={
'verbose_name': 'Pièce / Local',
'verbose_name_plural': 'Pièces / Locaux',
'ordering': ['building', 'code'],
},
),
migrations.AddField(
model_name='buildinglevel',
name='building',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='assets.publicbuilding', verbose_name='Bâtiment'),
),
migrations.CreateModel(
name='BuildingEquipment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
('installation_date', models.DateTimeField(blank=True, null=True)),
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
('last_survey_date', models.DateTimeField(blank=True, null=True)),
('brand', models.CharField(blank=True, max_length=500, null=True)),
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
('updated_at', models.DateTimeField(auto_now=True)),
('equipment_type', models.CharField(choices=[('hvac', 'Chauffage / CVC / Chaudière'), ('elevator', 'Ascenseur / Monte-charge'), ('fire_safety', 'Sécurité incendie / Extincteur / RIA'), ('electricity', 'Tableau électrique / TGBT'), ('alarm', "Alarme / Contrôle d'accès"), ('meter', 'Compteur (Eau / Gaz / Électricité)'), ('sanitary', 'Plomberie / Sanitaires'), ('other', 'Autre équipement technique')], default='hvac', max_length=50, verbose_name="Type d'équipement")),
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle réglementaire')),
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle réglementaire')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingequipment', verbose_name='Remplacé par')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
('room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipments', to='assets.buildingroom', verbose_name='Local')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipments', to='assets.publicbuilding', verbose_name='Bâtiment')),
],
options={
'verbose_name': 'Équipement technique de bâtiment',
'verbose_name_plural': 'Équipements techniques de bâtiment',
'ordering': ['building', 'code'],
},
),
migrations.CreateModel(
name='PublicBuildingDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, null=True, verbose_name='Description du document')),
('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')),
('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')),
('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.buildings.upload_to_building, verbose_name='Fichier')),
('document_type', models.CharField(default='other', max_length=50, verbose_name='Type de document')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.publicbuilding', verbose_name='Bâtiment')),
('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')),
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')),
],
options={
'verbose_name': 'Document de bâtiment',
'verbose_name_plural': 'Documents de bâtiment',
},
),
]

View file

@ -0,0 +1,219 @@
# Generated by Django 6.0.7 on 2026-07-27 12:00
import common.storages
import django.contrib.gis.db.models.fields
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_buildinglevel_publicbuilding_buildingroom_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='buildinglevel',
name='building',
),
migrations.RemoveField(
model_name='buildingroom',
name='level',
),
migrations.RemoveField(
model_name='buildingroom',
name='building',
),
migrations.AlterModelOptions(
name='publicbuilding',
options={'ordering': ['code', 'name_fr'], 'verbose_name': 'Localisation Bâtiment public', 'verbose_name_plural': 'Localisations Bâtiments publics'},
),
migrations.RemoveField(
model_name='publicbuilding',
name='brand',
),
migrations.RemoveField(
model_name='publicbuilding',
name='category',
),
migrations.RemoveField(
model_name='publicbuilding',
name='installation_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_corrective_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_inspection_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_preventive_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_survey_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='marked_as_removed_by',
),
migrations.RemoveField(
model_name='publicbuilding',
name='marked_as_removed_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='note',
),
migrations.RemoveField(
model_name='publicbuilding',
name='obsolescence',
),
migrations.RemoveField(
model_name='publicbuilding',
name='replaced_by',
),
migrations.RemoveField(
model_name='publicbuilding',
name='serial_number',
),
migrations.RemoveField(
model_name='publicbuilding',
name='status',
),
migrations.RemoveField(
model_name='publicbuilding',
name='uninstallation_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='validation_status',
),
migrations.RemoveField(
model_name='publicbuilding',
name='warranty_duration',
),
migrations.AddField(
model_name='publicbuilding',
name='floor_plan',
field=models.FileField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage"),
),
migrations.AddField(
model_name='publicbuilding',
name='is_main_location',
field=models.BooleanField(default=False, verbose_name='Localisation principale'),
),
migrations.AddField(
model_name='publicbuilding',
name='level_number',
field=models.IntegerField(default=0, verbose_name="Numéro d'étage (ex: 0=RDC, 1=1er, -1=Sous-sol)"),
),
migrations.AddField(
model_name='publicbuilding',
name='location_type',
field=models.CharField(choices=[('site', 'Site / Complexe'), ('building', 'Bâtiment / Bloc'), ('level', 'Niveau / Étage'), ('room', 'Pièce / Local'), ('other', 'Autre localisation')], default='building', max_length=50, verbose_name='Type de localisation'),
),
migrations.AddField(
model_name='publicbuilding',
name='parent_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.publicbuilding', verbose_name='Localisation parente'),
),
migrations.AddField(
model_name='publicbuilding',
name='surface_m2',
field=models.FloatField(blank=True, null=True, verbose_name='Surface spécifique (m²)'),
),
migrations.AlterField(
model_name='publicbuilding',
name='dpe_rating',
field=models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Performance Énergétique (PEB / DPE / EPC)'),
),
migrations.AlterField(
model_name='publicbuilding',
name='erp_category',
field=models.CharField(blank=True, 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)')], max_length=10, null=True, verbose_name="Catégorie d'accueil / ERP"),
),
migrations.AlterField(
model_name='publicbuilding',
name='erp_type',
field=models.CharField(blank=True, 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')], max_length=10, null=True, verbose_name="Type d'établissement / ERP"),
),
migrations.AlterField(
model_name='publicbuilding',
name='ges_rating',
field=models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Émissions de Carbone & GES'),
),
migrations.AlterField(
model_name='publicbuilding',
name='is_erp',
field=models.BooleanField(default=True, verbose_name='Accueil & Sécurité du Public (ERP / PMR)'),
),
migrations.AlterField(
model_name='publicbuilding',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'),
),
migrations.AlterField(
model_name='publicbuilding',
name='usage_type',
field=models.CharField(choices=[('administrative', 'Administrative (Town Hall / Municipal Building)'), ('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')], default='administrative', max_length=50, verbose_name="Type d'usage principal"),
),
migrations.CreateModel(
name='BuildingAsset',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
('installation_date', models.DateTimeField(blank=True, null=True)),
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
('last_survey_date', models.DateTimeField(blank=True, null=True)),
('brand', models.CharField(blank=True, max_length=500, null=True)),
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
('updated_at', models.DateTimeField(auto_now=True)),
('lon', models.FloatField(blank=True, null=True)),
('lat', models.FloatField(blank=True, null=True)),
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)),
('geojson', models.TextField(blank=True, null=True)),
('asset_type', models.CharField(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')], default='civil_engineering', max_length=50, verbose_name="Type d'asset / Équipement")),
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle réglementaire')),
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle réglementaire')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_building_assets', to='assets.publicbuilding', verbose_name='Bâtiment / Site principal')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='location_assets', to='assets.publicbuilding', verbose_name='Localisation spécifique')),
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
('parent_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_assets', to='assets.buildingasset', verbose_name='Asset parent')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingasset', verbose_name='Remplacé par')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
],
options={
'verbose_name': 'Asset / Équipement de bâtiment',
'verbose_name_plural': 'Assets / Équipements de bâtiment',
'ordering': ['building', 'code'],
},
),
migrations.DeleteModel(
name='BuildingEquipment',
),
migrations.DeleteModel(
name='BuildingLevel',
),
migrations.DeleteModel(
name='BuildingRoom',
),
]

View file

@ -0,0 +1,121 @@
# Generated by Django 6.0.7 on 2026-07-27 12:54
import assets.models.outdoor
import common.storages
import django.contrib.gis.db.models.fields
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0006_remove_buildinglevel_building_and_more'),
('documents', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='OutdoorLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('lon', models.FloatField(blank=True, null=True)),
('lat', models.FloatField(blank=True, null=True)),
('geojson', models.TextField(blank=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise cartographique')),
('site_type', models.CharField(choices=[('playground', 'Aire / Plaine de jeux'), ('sports_ground', 'Terrain de sport extérieur'), ('multisports_agora', 'Terrain multisports / City-stadium'), ('skatepark_pumptrack', 'Skatepark / Pumptrack / Bowl'), ('outdoor_fitness', 'Fitness plein air / Street Workout'), ('open_air_theater', 'Théâtre de verdure / Scène extérieure'), ('park_recreation', 'Parc & Espace récréatif'), ('dog_park', 'Espace canin / Canisite'), ('cemetery', 'Cimetière communal'), ('event_plaza', 'Place publique / Zone événementielle'), ('other', 'Autre espace plein air')], default='playground', max_length=50, verbose_name='Type de site plein air')),
('is_main_location', models.BooleanField(default=False, verbose_name='Localisation principale')),
('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse complète')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('owner', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propriétaire')),
('occupant_entity', models.CharField(blank=True, max_length=255, null=True, verbose_name='Direction / Service gestionnaire')),
('surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface totale (m²)')),
('max_capacity', models.IntegerField(blank=True, null=True, verbose_name="Jauge / Capacité d'accueil")),
('pmr_accessible', models.BooleanField(default=False, verbose_name='Accessible PMR')),
('has_water_point', models.BooleanField(default=False, verbose_name="Point d'eau potable")),
('has_power_supply', models.BooleanField(default=False, verbose_name='Alimentation électrique disponible')),
('is_fenced', models.BooleanField(default=False, verbose_name='Site clôturé / Sécurisé')),
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_outdoor_locations', to=settings.AUTH_USER_MODEL, verbose_name='Gestionnaire référent')),
('parent_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.outdoorlocation', verbose_name='Localisation parente')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
],
options={
'verbose_name': "Espace public d'extérieur",
'verbose_name_plural': "Espaces publics d'extérieur",
'ordering': ['code', 'name_fr'],
},
),
migrations.CreateModel(
name='OutdoorAsset',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
('installation_date', models.DateTimeField(blank=True, null=True)),
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
('last_survey_date', models.DateTimeField(blank=True, null=True)),
('brand', models.CharField(blank=True, max_length=500, null=True)),
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
('updated_at', models.DateTimeField(auto_now=True)),
('lon', models.FloatField(blank=True, null=True)),
('lat', models.FloatField(blank=True, null=True)),
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)),
('geojson', models.TextField(blank=True, null=True)),
('asset_type', models.CharField(choices=[('play_structure', 'Structure combinée / Tour de jeu'), ('swing', 'Balançoire / Portique'), ('spring_rider', 'Jeu à ressort / Bascule'), ('slide', 'Toboggan'), ('zip_line', 'Tyrolienne'), ('amortizing_surface', 'Sol amortissant / Réception'), ('sports_goal', 'But / Cage / Panneau de basket'), ('fitness_station', 'Agrès de fitness outdoor / Workout'), ('skate_module', 'Module de skate / Rampe / Rail'), ('stage_platform', 'Scène extérieure / Podium'), ('audience_seating', 'Gradins / Bancs de spectateurs'), ('event_power_box', 'Borne foraine / Armoire électrique événementielle'), ('outdoor_lighting', 'Éclairage plein air / Projecteur scénique'), ('bench_table', 'Banc / Table de pique-nique'), ('shade_shelter', "Kiosque / Voile d'ombrage / Abri"), ('drinking_fountain', "Fontaine d'eau potable"), ('fence_gate', 'Clôture / Portillon de sécurité'), ('info_panel', "Panneau d'information / Consignes"), ('other', 'Autre équipement plein air')], default='play_structure', max_length=50, verbose_name="Type d'asset / Équipement")),
('norm', models.CharField(blank=True, choices=[('EN_1176', "EN 1176 - Équipements & sols d'aires de jeux"), ('EN_16630', "EN 16630 - Modules de fitness d'extérieur"), ('EN_14974', 'EN 14974 - Équipements de skatepark'), ('EN_15312', 'EN 15312 - Équipements multisports en accès libre'), ('other', 'Autre norme / Non spécifié')], max_length=50, null=True, verbose_name='Norme de sécurité appliquée')),
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle visuel/opérationnel')),
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle requis')),
('last_annual_inspection', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle annuel certifié')),
('next_annual_inspection', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle annuel certifié')),
('certifying_body', models.CharField(blank=True, max_length=255, null=True, verbose_name='Organisme de contrôle certifié')),
('impact_surface_type', models.CharField(blank=True, max_length=150, null=True, verbose_name="Type de sol d'amortissement")),
('critical_fall_height_m', models.FloatField(blank=True, null=True, verbose_name='Hauteur critique de chute (m)')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
('parent_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_assets', to='assets.outdoorasset', verbose_name='Asset parent')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.outdoorasset', verbose_name='Remplacé par')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='location_assets', to='assets.outdoorlocation', verbose_name='Localisation spécifique')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_outdoor_assets', to='assets.outdoorlocation', verbose_name='Site / Espace principal')),
],
options={
'verbose_name': 'Asset / Équipement plein air',
'verbose_name_plural': 'Assets / Équipements plein air',
'ordering': ['site', 'code'],
},
),
migrations.CreateModel(
name='OutdoorLocationDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, null=True, verbose_name='Description du document')),
('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')),
('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')),
('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.outdoor.upload_to_outdoor, verbose_name='Fichier')),
('document_type', models.CharField(default='other', max_length=50, verbose_name='Type de document')),
('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.outdoorlocation', verbose_name='Espace extérieur')),
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')),
],
options={
'verbose_name': "Document d'espace extérieur",
'verbose_name_plural': "Documents d'espace extérieur",
},
),
]

View file

@ -10,3 +10,5 @@ from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignP
from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain
from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument

View file

@ -0,0 +1,244 @@
# -----------------------------
# 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")

View file

@ -0,0 +1,177 @@
# -----------------------------
# Outdoor Spaces & Assets 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, AbstractGeoAsset, ASSET_STATUS_CHOICES
from common.models import Document
from common.storages import PrivateMediaStorage
private_storage = PrivateMediaStorage()
SITE_TYPE_CHOICES = (
('playground', _('Aire / Plaine de jeux')),
('sports_ground', _('Terrain de sport extérieur')),
('multisports_agora', _('Terrain multisports / City-stadium')),
('skatepark_pumptrack', _('Skatepark / Pumptrack / Bowl')),
('outdoor_fitness', _('Fitness plein air / Street Workout')),
('open_air_theater', _('Théâtre de verdure / Scène extérieure')),
('park_recreation', _('Parc & Espace récréatif')),
('dog_park', _('Espace canin / Canisite')),
('cemetery', _('Cimetière communal')),
('event_plaza', _('Place publique / Zone événementielle')),
('other', _('Autre espace plein air')),
)
OUTDOOR_ASSET_TYPE_CHOICES = (
('play_structure', _('Structure combinée / Tour de jeu')),
('swing', _('Balançoire / Portique')),
('spring_rider', _('Jeu à ressort / Bascule')),
('slide', _('Toboggan')),
('zip_line', _('Tyrolienne')),
('amortizing_surface', _('Sol amortissant / Réception')),
('sports_goal', _('But / Cage / Panneau de basket')),
('fitness_station', _('Agrès de fitness outdoor / Workout')),
('skate_module', _('Module de skate / Rampe / Rail')),
('stage_platform', _('Scène extérieure / Podium')),
('audience_seating', _('Gradins / Bancs de spectateurs')),
('event_power_box', _('Borne foraine / Armoire électrique événementielle')),
('outdoor_lighting', _('Éclairage plein air / Projecteur scénique')),
('bench_table', _('Banc / Table de pique-nique')),
('shade_shelter', _('Kiosque / Voile d\'ombrage / Abri')),
('drinking_fountain', _('Fontaine d\'eau potable')),
('fence_gate', _('Clôture / Portillon de sécurité')),
('info_panel', _('Panneau d\'information / Consignes')),
('other', _('Autre équipement plein air')),
)
NORM_CHOICES = (
('EN_1176', _('EN 1176 - Équipements & sols d\'aires de jeux')),
('EN_16630', _('EN 16630 - Modules de fitness d\'extérieur')),
('EN_14974', _('EN 14974 - Équipements de skatepark')),
('EN_15312', _('EN 15312 - Équipements multisports en accès libre')),
('other', _('Autre norme / Non spécifié')),
)
class OutdoorLocation(AbstractLocation):
"""
Modèle représentant un Espace Public Extérieur (Plaine de jeux, Terrain de sport, Théâtre plein air, Parc, etc.).
Peut comporter une arborescence (ex: Parc municipal > Aire de jeux > Zone toboggan).
"""
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"))
site_type = models.CharField(max_length=50, choices=SITE_TYPE_CHOICES, default='playground', verbose_name=_("Type de site plein air"))
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_outdoor_locations", verbose_name=_("Gestionnaire référent"))
occupant_entity = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Direction / Service gestionnaire"))
# Caractéristiques physiques & aménagements
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface totale (m²)"))
max_capacity = models.IntegerField(null=True, blank=True, verbose_name=_("Jauge / Capacité d'accueil"))
pmr_accessible = models.BooleanField(default=False, verbose_name=_("Accessible PMR"))
has_water_point = models.BooleanField(default=False, verbose_name=_("Point d'eau potable"))
has_power_supply = models.BooleanField(default=False, verbose_name=_("Alimentation électrique disponible"))
is_fenced = models.BooleanField(default=False, verbose_name=_("Site clôturé / Sécurisé"))
class Meta:
verbose_name = _("Espace public d'extérieur")
verbose_name_plural = _("Espaces publics d'extérieur")
ordering = ['code', 'name_fr']
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', 'site').all() | loc.all_outdoor_assets.select_related('location', 'parent_asset', 'site').filter(location__isnull=True))
else:
assets = list(loc.location_assets.select_related('location', 'parent_asset', 'site').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()
def get_absolute_url(self):
return reverse('assets:outdoor_location_detail', kwargs={'pk': self.pk})
class OutdoorAsset(AbstractGeoAsset):
"""
Modèle représentant un asset ou équipement physique d'extérieur (Structure de jeu, Sol amortissant, Agrès, Cage, Scène...).
Les assets peuvent former une arborescence parent-enfant.
"""
site = models.ForeignKey(OutdoorLocation, on_delete=models.CASCADE, related_name="all_outdoor_assets", verbose_name=_("Site / Espace principal"))
location = models.ForeignKey(OutdoorLocation, 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=OUTDOOR_ASSET_TYPE_CHOICES, default='play_structure', verbose_name=_("Type d'asset / Équipement"))
# Norme & Sécurité réglementaire
norm = models.CharField(max_length=50, choices=NORM_CHOICES, blank=True, null=True, verbose_name=_("Norme de sécurité appliquée"))
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle visuel/opérationnel"))
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle requis"))
last_annual_inspection = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle annuel certifié"))
next_annual_inspection = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle annuel certifié"))
certifying_body = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Organisme de contrôle certifié"))
# Sols amortissants & hauteurs d'impact
impact_surface_type = models.CharField(max_length=150, blank=True, null=True, verbose_name=_("Type de sol d'amortissement"))
critical_fall_height_m = models.FloatField(null=True, blank=True, verbose_name=_("Hauteur critique de chute (m)"))
class Meta:
verbose_name = _("Asset / Équipement plein air")
verbose_name_plural = _("Assets / Équipements plein air")
ordering = ['site', 'code']
def get_absolute_url(self):
return reverse('assets:outdoor_assets_detail', kwargs={'asset_model': 'outdoorasset', 'asset_id': self.pk})
def get_child_assets(self):
return self.child_assets.all()
def upload_to_outdoor(instance, filename):
return f"outdoor/{instance.site.code}/{filename}"
class OutdoorLocationDocument(Document):
"""
Documents associés à un espace extérieur (certificats de conformité, registres de sécurité, plans).
"""
site = models.ForeignKey(OutdoorLocation, on_delete=models.CASCADE, related_name="documents", verbose_name=_("Espace extérieur"))
file = models.FileField(upload_to=upload_to_outdoor, 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 d'espace extérieur")
verbose_name_plural = _("Documents d'espace extérieur")

View file

@ -260,6 +260,10 @@ def _get_asset_thematic(asset):
if hasattr(asset, 'category') and asset.category: if hasattr(asset, 'category') and asset.category:
return asset.category.thematic return asset.category.thematic
# Try from parent building category
if hasattr(asset, 'building') and asset.building and hasattr(asset.building, 'category') and asset.building.category:
return asset.building.category.thematic
# Try from model's category # Try from model's category
if hasattr(asset, 'model') and asset.model and hasattr(asset.model, 'category') and asset.model.category: if hasattr(asset, 'model') and asset.model and hasattr(asset.model, 'category') and asset.model.category:
return asset.model.category.thematic return asset.model.category.thematic
@ -280,6 +284,8 @@ def _get_asset_thematic(asset):
'sign': 'sign', 'sign': 'sign',
'controlcenter': 'controlcenters', 'controlcenter': 'controlcenters',
'its': 'its', 'its': 'its',
'building': 'publicbuildings',
'publicbuilding': 'publicbuildings',
} }
for prefix, thematic_code in thematic_mappings.items(): for prefix, thematic_code in thematic_mappings.items():
@ -310,6 +316,8 @@ def _get_location_thematic(location):
'cleanlocation': 'clean', 'cleanlocation': 'clean',
'publiclightingstreet': 'publiclighting', 'publiclightingstreet': 'publiclighting',
'signstreet': 'sign', 'signstreet': 'sign',
'publicbuilding': 'publicbuildings',
'buildinglocation': 'publicbuildings',
} }
thematic_code = thematic_mappings.get(class_name) thematic_code = thematic_mappings.get(class_name)
if not thematic_code: if not thematic_code:

View file

@ -588,7 +588,9 @@
'controlcenters': 'controlcentergeoasset', 'controlcenters': 'controlcentergeoasset',
'its': 'itsgeoasset', 'its': 'itsgeoasset',
'sign': 'signgeoasset', 'sign': 'signgeoasset',
'roads': 'roadgeoasset' 'roads': 'roadgeoasset',
'publicbuildings': 'publicbuilding',
'buildings': 'publicbuilding'
}; };
return mapping[thematicCode] || 'geoasset'; return mapping[thematicCode] || 'geoasset';
} }

View file

@ -414,7 +414,7 @@ function initUnifiedLocationPickerMap() {
setPoint(lng, lat, true); setPoint(lng, lat, true);
} }
}, },
() => {}, () => { },
{ enableHighAccuracy: true, timeout: 10000 } { enableHighAccuracy: true, timeout: 10000 }
); );
}); });
@ -501,8 +501,9 @@ function buildColumns() {
return [ return [
{ field: 'check', checkbox: true, width: 36 }, { field: 'check', checkbox: true, width: 36 },
{ field: 'actions', title: '', formatter: zoomFormatter, width: 50 }, { field: 'actions', title: '', formatter: zoomFormatter, width: 50 },
{ field: 'code', title: gettext('Code'), sortable: true, {
formatter: function(v, row) { field: 'code', title: gettext('Code'), sortable: true,
formatter: function (v, row) {
if (row.detail_url) return `<a href="${row.detail_url}" class="unified-code-link">${v || '-'}</a>`; if (row.detail_url) return `<a href="${row.detail_url}" class="unified-code-link">${v || '-'}</a>`;
return v || '-'; return v || '-';
} }
@ -512,11 +513,13 @@ function buildColumns() {
{ field: 'thematic_name', title: gettext('Thématique'), sortable: true }, { field: 'thematic_name', title: gettext('Thématique'), sortable: true },
{ field: 'category', title: gettext('Catégorie'), sortable: true, visible: false }, { field: 'category', title: gettext('Catégorie'), sortable: true, visible: false },
{ field: 'model', title: gettext('Modèle'), sortable: true, visible: false }, { field: 'model', title: gettext('Modèle'), sortable: true, visible: false },
{ field: 'status', title: gettext('Statut'), sortable: true, visible: false, {
formatter: function(v) { return v ? `<i class="bi bi-circle-fill status_${v}"></i> ${v}` : ''; } field: 'status', title: gettext('Statut'), sortable: true, visible: false,
formatter: function (v) { return v ? `<i class="bi bi-circle-fill status_${v}"></i> ${v}` : ''; }
}, },
{ field: 'validation_status', title: gettext('Statut de validation'), sortable: true, visible: true, {
formatter: function(v) { field: 'validation_status', title: gettext('Statut de validation'), sortable: true, visible: true,
formatter: function (v) {
const labels = { const labels = {
'to_be_validated': gettext('À valider'), 'to_be_validated': gettext('À valider'),
'validated': gettext('Validé'), 'validated': gettext('Validé'),
@ -583,7 +586,7 @@ function applyVisibleColumnsPreference(visibleColumns) {
* explicit thematic selection (GET params / locked_thematic_code). * explicit thematic selection (GET params / locked_thematic_code).
*/ */
function applyInitialPreferences() { function applyInitialPreferences() {
if (PREVENTIVE_DRAFT_ID || LOCKED_THEMATIC_CODE || SELECTED_THEMATICS.length > 0) return; if (PREVENTIVE_DRAFT_ID || LOCKED_THEMATIC_CODE || (typeof HAS_URL_THEMATICS !== 'undefined' && HAS_URL_THEMATICS)) return;
if (ASSETS_INDEX_PREFS && typeof ASSETS_INDEX_PREFS === 'object' && Object.keys(ASSETS_INDEX_PREFS).length > 0) { if (ASSETS_INDEX_PREFS && typeof ASSETS_INDEX_PREFS === 'object' && Object.keys(ASSETS_INDEX_PREFS).length > 0) {
const prefs = ASSETS_INDEX_PREFS; const prefs = ASSETS_INDEX_PREFS;
@ -694,7 +697,7 @@ function savePreferences() {
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() }, headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
body: JSON.stringify(prefs), body: JSON.stringify(prefs),
keepalive: true, keepalive: true,
}).catch(() => {}); }).catch(() => { });
} }
// ─── Table init ─────────────────────────────────────────────────────────────── // ─── Table init ───────────────────────────────────────────────────────────────
@ -721,11 +724,11 @@ $(document).ready(function () {
ignoreClickToSelectOn: function (element) { ignoreClickToSelectOn: function (element) {
return $(element).closest('.zoom-btn, .unified-code-link').length > 0; return $(element).closest('.zoom-btn, .unified-code-link').length > 0;
}, },
onCheck: function(row) { selectedIds.add(row.id); updateSelectionStatus(); }, onCheck: function (row) { selectedIds.add(row.id); updateSelectionStatus(); },
onUncheck: function(row) { selectedIds.delete(row.id); updateSelectionStatus(); }, onUncheck: function (row) { selectedIds.delete(row.id); updateSelectionStatus(); },
onCheckAll: function(rows) { rows.forEach(r => selectedIds.add(r.id)); updateSelectionStatus(); }, onCheckAll: function (rows) { rows.forEach(r => selectedIds.add(r.id)); updateSelectionStatus(); },
onUncheckAll: function(rows) { rows.forEach(r => selectedIds.delete(r.id)); updateSelectionStatus(); }, onUncheckAll: function (rows) { rows.forEach(r => selectedIds.delete(r.id)); updateSelectionStatus(); },
onClickRow: function(row) { onClickRow: function (row) {
if (!row.lon || !row.lat) return; if (!row.lon || !row.lat) return;
const source = row.is_location ? 'locations' : 'assets'; const source = row.is_location ? 'locations' : 'assets';
if (typeof highlightMapFeature === 'function') { if (typeof highlightMapFeature === 'function') {
@ -1229,6 +1232,10 @@ function hasTriggerFilter() {
const signFilters = activeConditionalFilters.sign || {}; const signFilters = activeConditionalFilters.sign || {};
if (Array.isArray(signFilters.municipality) && signFilters.municipality.length) return true; if (Array.isArray(signFilters.municipality) && signFilters.municipality.length) return true;
if (Array.isArray(signFilters.panel_type) && signFilters.panel_type.length) return true; if (Array.isArray(signFilters.panel_type) && signFilters.panel_type.length) return true;
const outdoorFilters = activeConditionalFilters.outdoorfacilities || activeConditionalFilters.outdoor || {};
if (Array.isArray(outdoorFilters.site_type) && outdoorFilters.site_type.length) return true;
if (Array.isArray(outdoorFilters.norm) && outdoorFilters.norm.length) return true;
if (outdoorFilters.pmr_accessible) return true;
return false; return false;
} }

View file

@ -209,6 +209,12 @@ function loadUnifiedMapIcons(map) {
{ name: 'icon-artworkfountain', codepoint: 'f617', color: '#1e88e5' }, // water — bleu fontaine { name: 'icon-artworkfountain', codepoint: 'f617', color: '#1e88e5' }, // water — bleu fontaine
{ name: 'icon-artworkmonument', codepoint: 'f8f9', color: '#f24141' }, // person-standing — statue { name: 'icon-artworkmonument', codepoint: 'f8f9', color: '#f24141' }, // person-standing — statue
{ name: 'icon-controlcenters', codepoint: 'f301', color: '#7b1fa2' }, // display-fill — violet { name: 'icon-controlcenters', codepoint: 'f301', color: '#7b1fa2' }, // display-fill — violet
{ name: 'icon-publicbuildings', codepoint: 'f1dd', color: '#2B579A' }, // building — bleu nuit
{ name: 'icon-publicbuilding', codepoint: 'f1dd', color: '#2B579A' }, // building — bleu nuit
{ name: 'icon-outdoorfacilities', codepoint: 'f302', color: '#198754' }, // dribbble/dribbble icon — vert plein air
{ name: 'icon-outdoor', codepoint: 'f302', color: '#198754' },
{ name: 'icon-outdoorlocation', codepoint: 'f302', color: '#198754' },
{ name: 'icon-outdoorasset', codepoint: 'f302', color: '#198754' },
{ name: 'icon-roads', codepoint: 'f47e', color: '#607d8b' }, // map-fill — bleu-gris { name: 'icon-roads', codepoint: 'f47e', color: '#607d8b' }, // map-fill — bleu-gris
]; ];
@ -285,6 +291,8 @@ const THEMATIC_COLOR_EXPR = [
'clean', '#5e5e5e', // grey — cleanliness 'clean', '#5e5e5e', // grey — cleanliness
'artworks', '#f24141', // coral — artworks / monuments 'artworks', '#f24141', // coral — artworks / monuments
'controlcenters', '#7b1fa2', // purple — control centres 'controlcenters', '#7b1fa2', // purple — control centres
'publicbuildings', '#2B579A', // navy blue — public buildings
'buildings', '#2B579A', // navy blue — public buildings
'roads', [ 'roads', [
'match', ['get', 'asset_type'], 'match', ['get', 'asset_type'],
'roadway', '#4a4a4a', 'roadway', '#4a4a4a',
@ -508,6 +516,8 @@ const _unified_assets_symbol = {
'clean', 'icon-clean', 'clean', 'icon-clean',
'artworks', 'icon-artworks', 'artworks', 'icon-artworks',
'controlcenters', 'icon-controlcenters', 'controlcenters', 'icon-controlcenters',
'publicbuildings', 'icon-publicbuildings',
'publicbuilding', 'icon-publicbuilding',
'roads', 'icon-roads', 'roads', 'icon-roads',
'icon-roads' // fallback 'icon-roads' // fallback
] ]

View file

@ -0,0 +1,38 @@
{% load i18n %}
<ul class="list-group list-group-flush bg-transparent">
{% for asset_node in asset_nodes %}
<li class="list-group-item bg-transparent border-0 py-1 px-0">
<!-- Row Header -->
<div class="d-flex align-items-center justify-content-between py-1 px-2">
<!-- Contenu gauche (indenté selon la profondeur) -->
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
{% if asset_node.children %}
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-asset-{{ asset_node.asset.id }}"
aria-expanded="false">
<i class="bi bi-chevron-right small"></i>
</button>
{% else %}
<span class="px-1"></span>
{% endif %}
<i class="bi bi-gear-fill text-info small"></i>
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
<a href="{{ asset_node.asset.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
{{ asset_node.asset.code }}
</a>
<span class="small">{{ asset_node.asset.get_name }}</span>
<span class="badge bg-info-subtle text-info border small ms-1">{{ asset_node.asset.get_asset_type_display }}</span>
</div>
</div>
{% if asset_node.children %}
<div id="tree-asset-{{ asset_node.asset.id }}" class="collapse mt-1">
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=asset_node.children depth=depth|default:1|add:1 only %}
</div>
{% endif %}
</li>
{% endfor %}
</ul>

View file

@ -0,0 +1,500 @@
{% extends "base.html" %}
{% load static i18n documents_tags %}
{% block title %}{{ building.code }} - {{ building.get_name }} | StreetUp{% endblock %}
{% block extra_head %}
<!-- MapLibre GL CSS -->
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb & Actions -->
<div class="d-flex justify-content-between align-items-center mb-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'assets:buildings_index' %}">{% translate "Bâtiments publics" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ building.code }}</li>
</ol>
</nav>
<div class="d-flex gap-2">
{% if can_edit %}
<a href="{% url 'assets:publicbuilding_edit' building.pk %}" class="btn btn-primary btn-sm">
<i class="bi bi-pencil me-1"></i>{% translate "Modifier" %}
</a>
{% endif %}
<a href="{% url 'assets:buildings_index' %}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>{% translate "Retour à la liste" %}
</a>
</div>
</div>
<!-- Header Card -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h2 class="h4 mb-1">
<i class="bi bi-building text-primary me-2"></i>{{ building.code }} - {{ building.get_name }}
</h2>
<p class="text-muted mb-2"><i class="bi bi-geo-alt me-1"></i>{{ building.address|default:_("Adresse non renseignée") }}</p>
<div class="d-flex gap-2 flex-wrap">
<span class="badge bg-primary-subtle text-primary border">{{ building.get_usage_type_display }}</span>
<span class="badge bg-success-subtle text-success border">{{ building.get_building_status_display }}</span>
{% if building.is_erp %}
<span class="badge bg-info-subtle text-info border">{{ regional_terms.public_access_short }} (Cat. {{ building.erp_category|default:"-" }})</span>
{% endif %}
</div>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
{% if building.dpe_rating %}
<div class="d-inline-block text-center me-3">
<span class="d-block text-muted small">{{ regional_terms.energy_diag_short }}</span>
<span class="badge bg-success fs-5 px-3 py-2">Classe {{ building.dpe_rating }}</span>
</div>
{% endif %}
{% if building.ges_rating %}
<div class="d-inline-block text-center">
<span class="d-block text-muted small">{{ regional_terms.carbon_diag_short }}</span>
<span class="badge bg-primary fs-5 px-3 py-2">Classe {{ building.ges_rating }}</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 3 Core Tabs Navigation -->
<ul class="nav nav-tabs mb-4" id="buildingTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab">
<i class="bi bi-info-circle me-1"></i>{% translate "Vue d'ensemble" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments" type="button" role="tab">
<i class="bi bi-gear me-1"></i>{% translate "Assets & Équipements" %} ({{ equipments|length }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="interventions-tab" data-bs-toggle="tab" data-bs-target="#interventions" type="button" role="tab">
<i class="bi bi-tools me-1"></i>{% translate "Interventions" %} ({{ interventions.count }})
</button>
</li>
</ul>
<!-- Tabs Content -->
<div class="tab-content" id="buildingTabsContent">
<!-- 1. Vue d'ensemble (Dashboard avec Cards directes) -->
<div class="tab-pane fade show active" id="overview" role="tabpanel">
<div class="row g-4">
<!-- Left Column: Propriété, Caractéristiques & Arborescence Niveaux/Locaux/Équipements -->
<div class="col-md-7">
<!-- Card 1: Propriété & Responsables -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold"><i class="bi bi-person-badge me-2"></i>{% translate "Propriété & Responsables" %}</div>
<div class="card-body">
<table class="table table-borderless mb-0">
<tr>
<th class="w-50 text-muted">{% translate "Propriétaire" %} :</th>
<td>{{ building.owner|default:"-" }}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Gestionnaire référent" %} :</th>
<td>{% if building.manager %}{{ building.manager.get_full_name|default:building.manager.username }}{% else %}-{% endif %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Service occupant" %} :</th>
<td>{{ building.occupant_entity|default:"-" }}</td>
</tr>
</table>
</div>
</div>
<!-- Card 2: Caractéristiques physiques & Normes -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold"><i class="bi bi-rulers me-2"></i>{% translate "Caractéristiques physiques &" %} {{ regional_terms.public_access_short }}</div>
<div class="card-body">
<table class="table table-borderless mb-0">
<tr>
<th class="w-50 text-muted">{% translate "Année de construction" %} :</th>
<td>{{ building.construction_year|default:"-" }}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Étages / Sous-sols" %} :</th>
<td>{{ building.floors_count }} {% translate "étage(s)" %} / {{ building.basement_count }} {% translate "sous-sol(s)" %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Surface Utile / Brut" %} :</th>
<td>{{ building.usable_surface_m2|default:"-" }} m² / {{ building.total_surface_m2|default:"-" }} m²</td>
</tr>
<tr>
<th class="text-muted">{{ regional_terms.public_access_category }} :</th>
<td>{{ building.erp_category|default:"-" }}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Accessibilité PMR" %} :</th>
<td>{% if building.pmr_accessible %}<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>{% translate "Oui" %}</span>{% else %}<span class="text-muted">{% translate "Non" %}</span>{% endif %}</td>
</tr>
</table>
</div>
</div>
<!-- Card 3: Arborescence Unifiée des Localisations & Assets -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence Hiérarchique" %}</span>
<span class="badge bg-primary-subtle text-primary border">{{ location_tree.children|length }} {% translate "sous-localisation(s)" %}</span>
</div>
<div class="card-body p-3">
<!-- Localisation Parente (si existante) -->
{% if parent_location %}
<div class="card bg-light border-secondary-subtle p-2 mb-3">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-geo-alt-fill text-danger fs-5"></i>
<div>
<div class="small text-muted fw-semibold">{% translate "Localisation parente" %} :</div>
<a href="{{ parent_location.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline">
{{ parent_location.code }} - {{ parent_location.get_name }}
</a>
<span class="badge bg-secondary-subtle text-dark border small ms-1">{{ parent_location.get_location_type_display }}</span>
</div>
</div>
<a href="{{ parent_location.get_absolute_url }}" class="btn btn-sm btn-outline-secondary py-0 px-2 small">
<i class="bi bi-arrow-up me-1"></i>{% translate "Voir la parente" %}
</a>
</div>
</div>
{% endif %}
<!-- Localisation Courante (Racine de l'affichage) -->
<div class="p-2 border rounded bg-white mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-building text-primary fs-5"></i>
<span class="fw-bold fs-6 text-primary">{{ building.code }}</span>
<span class="fw-bold fs-6">- {{ building.get_name }}</span>
<span class="badge bg-primary text-white ms-1">{{ building.get_location_type_display }}</span>
{% if location_tree.assets %}
<button class="btn btn-sm btn-outline-info py-0 px-2 small ms-2 text-nowrap"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-assets-root"
aria-expanded="false">
<i class="bi bi-tools me-1"></i>Assets ({{ location_tree.assets|length }})
</button>
{% endif %}
</div>
</div>
<!-- Collapsible Assets de la localisation courante (NON VISIBLES PAR DÉFAUT) -->
{% if location_tree.assets %}
<div id="tree-assets-root" class="collapse mt-2 ms-4">
<div class="card card-body bg-light border-info-subtle p-2 mb-2">
<div class="fw-bold small text-info mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ building.code }} :</div>
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=location_tree.assets only %}
</div>
</div>
{% endif %}
<!-- Sous-localisations & leurs assets -->
<div class="mt-2">
{% include "assets/buildings/publicbuilding_location_tree.html" with node=location_tree depth=1 only %}
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Localisation & Documents (sous la carte) -->
<div class="col-md-5">
<!-- Card Localisation & Carte interactive -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
<span><i class="bi bi-geo-alt me-2"></i>{% translate "Localisation & Carte" %}</span>
{% if center_lat and center_lon %}
<a href="https://www.google.com/maps?cbll={{ center_lat|floatformat:'6u'}},{{ center_lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid="
target="_blank" class="btn btn-sm btn-outline-secondary py-0 px-2">
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="14" class="me-1">
Street View
</a>
{% endif %}
</div>
<div class="card-body p-0">
<div id="building-map" style="height: 340px; width: 100%; border-radius: 0 0 4px 4px;"></div>
</div>
<div class="card-footer bg-white p-3">
<p class="mb-2 small">
<strong>{% translate "Adresse" %} :</strong> {{ building.address|default:_("Non renseignée") }}
</p>
{% if center_lat and center_lon %}
<div class="small text-muted font-monospace">
WGS84: {{ center_lat|floatformat:6 }}, {{ center_lon|floatformat:6 }}
</div>
{% endif %}
</div>
</div>
<!-- Card Documents associés (placée sous la localisation) -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<span class="fw-bold"><i class="bi bi-file-earmark-text me-2"></i>{% translate "Documents associés" %} ({{ documents.count }})</span>
<a href="{% url 'admin:assets_publicbuildingdocument_add' %}?building={{ building.pk }}" class="btn btn-sm btn-outline-primary py-0 px-2">
<i class="bi bi-plus-lg me-1"></i>{% translate "Ajouter" %}
</a>
</div>
<div class="card-body">
<div class="row row-cols-1 row-cols-sm-2 g-3">
{% for doc in documents %}
<div class="col">
<div class="card h-100 border text-center p-2">
{% if doc.document.file %}
<i class="bi bi-file-earmark-pdf text-danger fs-2 mb-1"></i>
<div class="small text-truncate w-100 fw-bold" title="{{ doc.document.file.name }}">{{ doc.document.file.name }}</div>
<a href="{{ doc.document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary mt-2">
<i class="bi bi-download me-1"></i>{% translate "Télécharger" %}
</a>
{% else %}
<i class="bi bi-file-earmark text-secondary fs-2 mb-1"></i>
<span class="text-muted small">{% translate "Fichier indisponible" %}</span>
{% endif %}
</div>
</div>
{% empty %}
<div class="col-12 text-center text-muted py-3">
<i class="bi bi-folder-x fs-4 d-block mb-1"></i>
{% translate "Aucun document associé." %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. Assets & Équipements Tab -->
<div class="tab-pane fade" id="equipments" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title h6 mb-0"><i class="bi bi-tools me-2"></i>{% translate "Assets Physiques & Équipements de la Localisation et de ses sous-localisations" %}</h5>
<span class="badge bg-info-subtle text-info border">{{ equipments|length }} {% translate "asset(s)" %}</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% translate "Code / Nom" %}</th>
<th>{% translate "Type d'asset" %}</th>
<th>{% translate "Localisation spécifique" %}</th>
<th>{% translate "Asset Parent" %}</th>
<th>{% translate "Statut" %}</th>
<th>{% translate "Prochain contrôle" %}</th>
<th class="text-end pe-3">{% translate "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for eq in equipments %}
<tr>
<td>
<a href="{{ eq.get_absolute_url }}" class="fw-bold text-decoration-none">{{ eq.code }}</a>
<div class="small text-muted">{{ eq.get_name }}</div>
</td>
<td>
<span class="badge bg-info-subtle text-info border">
{{ eq.get_asset_type_display }}
</span>
</td>
<td>
{% if eq.location %}
<a href="{{ eq.location.get_absolute_url }}" class="small text-dark text-decoration-none">
<i class="bi bi-geo-alt me-1"></i>{{ eq.location.code }} - {{ eq.location.get_name }}
</a>
{% else %}
<span class="small text-muted">{{ eq.building.code }}</span>
{% endif %}
</td>
<td>
{% if eq.parent_asset %}
<a href="{{ eq.parent_asset.get_absolute_url }}" class="small text-decoration-none">
<i class="bi bi-diagram-2 me-1"></i>{{ eq.parent_asset.code }}
</a>
{% else %}
<span class="text-muted small">-</span>
{% endif %}
</td>
<td>
<span class="badge {% if eq.status == 'active' %}bg-success-subtle text-success border{% else %}bg-secondary-subtle text-secondary border{% endif %}">
{{ eq.get_status_display }}
</span>
</td>
<td>{{ eq.next_control_date|date:"d/m/Y"|default:"-" }}</td>
<td class="text-end pe-3">
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap" title="{% translate 'Détail de l\'asset' %}">
<i class="bi bi-eye me-1"></i>{% translate "Détail" %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center text-muted py-4">{% translate "Aucun asset ou équipement technique enregistré." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 3. Interventions Tab -->
<div class="tab-pane fade" id="interventions" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title h6 mb-0"><i class="bi bi-tools me-2"></i>{% translate "Interventions & Maintenance" %}</h5>
<a href="{% url 'interventions:interventions_index' %}?asset={{ building.pk }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouvelle intervention" %}
</a>
</div>
<div class="card-body p-0">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width: 50px;"></th>
<th>{% translate "Code" %}</th>
<th>{% translate "Titre / Objet" %}</th>
<th>{% translate "Date de début" %}</th>
<th>{% translate "Statut" %}</th>
</tr>
</thead>
<tbody>
{% for interv in interventions %}
<tr>
<td>
<a href="{% url 'interventions:interventions_index' %}{{ interv.id }}" class="btn btn-sm btn-light border" title="{% translate 'Détail' %}">
<i class="bi bi-info-circle"></i>
</a>
</td>
<td><strong>{{ interv.code }}</strong></td>
<td>{{ interv.title }}</td>
<td>{{ interv.begin_time|date:"d/m/Y H:i"|default:"-" }}</td>
<td><span class="badge bg-secondary">{{ interv.get_status_display }}</span></td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-tools fs-2 d-block mb-2 text-muted"></i>
{% translate "Aucune intervention enregistrée pour ce bâtiment." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const buildingGeom = {{ building_geojson_json|safe }};
const centerLon = {{ center_lon|default:4.3572 }};
const centerLat = {{ center_lat|default:50.8477 }};
let map = null;
function initMap() {
const container = document.getElementById('building-map');
if (!container) return;
map = new maplibregl.Map({
container: 'building-map',
style: '{{ MAP_STYLE_LIGHT|default:"https://api.maptiler.com/maps/streets-v2-light/style.json" }}',
center: [centerLon, centerLat],
zoom: 16
});
map.addControl(new maplibregl.NavigationControl());
map.on('load', function() {
if (buildingGeom) {
map.addSource('building-footprint', {
'type': 'geojson',
'data': buildingGeom
});
map.addLayer({
'id': 'building-fill',
'type': 'fill',
'source': 'building-footprint',
'paint': {
'fill-color': '#2B579A',
'fill-opacity': 0.4
}
});
map.addLayer({
'id': 'building-outline',
'type': 'line',
'source': 'building-footprint',
'paint': {
'line-color': '#2B579A',
'line-width': 3
}
});
}
});
}
initMap();
const overviewTab = document.getElementById('overview-tab');
if (overviewTab) {
overviewTab.addEventListener('shown.bs.tab', function () {
if (map) map.resize();
});
}
// Gestion du chevron plié / déplié dans l'arborescence
document.querySelectorAll('.tree-toggle-btn').forEach(function(btn) {
const targetId = btn.getAttribute('data-bs-target');
const target = document.querySelector(targetId);
if (target) {
target.addEventListener('show.bs.collapse', function (e) {
if (e.target !== target) return;
const icon = btn.querySelector('i');
if (icon) {
icon.classList.remove('bi-chevron-right');
icon.classList.add('bi-chevron-down');
}
});
target.addEventListener('hide.bs.collapse', function (e) {
if (e.target !== target) return;
const icon = btn.querySelector('i');
if (icon) {
icon.classList.remove('bi-chevron-down');
icon.classList.add('bi-chevron-right');
}
});
}
});
});
</script>
{% endblock script %}

View file

@ -0,0 +1,415 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% translate "Modifier le bâtiment" %} {{ building.code }} | StreetUp{% endblock %}
{% block content %}
<div class="container py-4">
<!-- Breadcrumb & Title -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'assets:buildings_index' %}">{% translate "Bâtiments publics" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'assets:publicbuilding_detail' building.pk %}">{{ building.code }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% translate "Modifier" %}</li>
</ol>
</nav>
<h1 class="h3 mb-0">
<i class="bi bi-pencil-square text-primary me-2"></i>{% translate "Modifier le bâtiment" %} {{ building.code }}
</h1>
</div>
<div>
<a href="{% url 'assets:publicbuilding_detail' building.pk %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>{% translate "Annuler" %}
</a>
</div>
</div>
<form method="post" action="" class="needs-validation" novalidate>
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>{% translate "Certains champs contiennent des erreurs." %}</strong> {% translate "Veuillez vérifier les informations ci-dessous." %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<!-- Card 1: Identités & Localisation -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold">
<i class="bi bi-info-circle me-2"></i>{% translate "Informations générales" %}
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label for="{{ form.code.id_for_label }}" class="form-label fw-semibold">{% translate "Code du bâtiment" %} <span class="text-danger">*</span></label>
{{ form.code }}
{% if form.code.errors %}<div class="text-danger small mt-1">{{ form.code.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.name_fr.id_for_label }}" class="form-label fw-semibold">{% translate "Nom (FR)" %}</label>
{{ form.name_fr }}
{% if form.name_fr.errors %}<div class="text-danger small mt-1">{{ form.name_fr.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.name_nl.id_for_label }}" class="form-label fw-semibold">{% translate "Nom (NL)" %}</label>
{{ form.name_nl }}
{% if form.name_nl.errors %}<div class="text-danger small mt-1">{{ form.name_nl.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.location_type.id_for_label }}" class="form-label fw-semibold">{% translate "Type de localisation" %}</label>
{{ form.location_type }}
{% if form.location_type.errors %}<div class="text-danger small mt-1">{{ form.location_type.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.parent_location.id_for_label }}" class="form-label fw-semibold">{% translate "Localisation parente" %}</label>
{{ form.parent_location }}
{% if form.parent_location.errors %}<div class="text-danger small mt-1">{{ form.parent_location.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4 d-flex align-items-end mb-2">
<div class="form-check">
{{ form.is_main_location }}
<label for="{{ form.is_main_location.id_for_label }}" class="form-check-label fw-semibold">{% translate "Localisation principale" %}</label>
</div>
</div>
<div class="col-md-12">
<label for="{{ form.address.id_for_label }}" class="form-label fw-semibold">{% translate "Adresse complète" %}</label>
{{ form.address }}
{% if form.address.errors %}<div class="text-danger small mt-1">{{ form.address.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-12">
<label for="{{ form.description.id_for_label }}" class="form-label fw-semibold">{% translate "Description" %}</label>
{{ form.description }}
{% if form.description.errors %}<div class="text-danger small mt-1">{{ form.description.errors.0 }}</div>{% endif %}
</div>
</div>
</div>
</div>
<!-- Card 2: Propriété & Gestion -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold">
<i class="bi bi-person-badge me-2"></i>{% translate "Propriété & Gestion" %}
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label for="{{ form.owner.id_for_label }}" class="form-label fw-semibold">{% translate "Propriétaire" %}</label>
{{ form.owner }}
{% if form.owner.errors %}<div class="text-danger small mt-1">{{ form.owner.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.manager.id_for_label }}" class="form-label fw-semibold">{% translate "Gestionnaire référent" %}</label>
{{ form.manager }}
{% if form.manager.errors %}<div class="text-danger small mt-1">{{ form.manager.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.occupant_entity.id_for_label }}" class="form-label fw-semibold">{% translate "Direction / Service occupant" %}</label>
{{ form.occupant_entity }}
{% if form.occupant_entity.errors %}<div class="text-danger small mt-1">{{ form.occupant_entity.errors.0 }}</div>{% endif %}
</div>
</div>
</div>
</div>
<!-- Card 3: Usage & Caractéristiques physiques -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold">
<i class="bi bi-building-gear me-2"></i>{% translate "Usage & Caractéristiques physiques" %}
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="{{ form.usage_type.id_for_label }}" class="form-label fw-semibold">{% translate "Type d'usage principal" %}</label>
{{ form.usage_type }}
{% if form.usage_type.errors %}<div class="text-danger small mt-1">{{ form.usage_type.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.building_status.id_for_label }}" class="form-label fw-semibold">{% translate "Statut d'occupation" %}</label>
{{ form.building_status }}
{% if form.building_status.errors %}<div class="text-danger small mt-1">{{ form.building_status.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.construction_year.id_for_label }}" class="form-label fw-semibold">{% translate "Année de construction" %}</label>
{{ form.construction_year }}
{% if form.construction_year.errors %}<div class="text-danger small mt-1">{{ form.construction_year.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.floors_count.id_for_label }}" class="form-label fw-semibold">{% translate "Nombre d'étages hors-sol" %}</label>
{{ form.floors_count }}
{% if form.floors_count.errors %}<div class="text-danger small mt-1">{{ form.floors_count.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.basement_count.id_for_label }}" class="form-label fw-semibold">{% translate "Nombre de niveaux en sous-sol" %}</label>
{{ form.basement_count }}
{% if form.basement_count.errors %}<div class="text-danger small mt-1">{{ form.basement_count.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.total_surface_m2.id_for_label }}" class="form-label fw-semibold">{% translate "Surface brute totale (m²)" %}</label>
{{ form.total_surface_m2 }}
{% if form.total_surface_m2.errors %}<div class="text-danger small mt-1">{{ form.total_surface_m2.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.usable_surface_m2.id_for_label }}" class="form-label fw-semibold">{% translate "Surface utile (m²)" %}</label>
{{ form.usable_surface_m2 }}
{% if form.usable_surface_m2.errors %}<div class="text-danger small mt-1">{{ form.usable_surface_m2.errors.0 }}</div>{% endif %}
</div>
</div>
</div>
</div>
<!-- Card 4: Réglementation & Accueil du Public (ERP / PMR) -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold">
<i class="bi bi-shield-check me-2"></i>{% translate "Accueil & Sécurité du Public (ERP / PMR)" %}
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6 d-flex align-items-center">
<div class="form-check mt-3">
{{ form.is_erp }}
<label for="{{ form.is_erp.id_for_label }}" class="form-check-label fw-semibold">
{% translate "Bâtiment recevant du public (ERP / PMR)" %}
</label>
</div>
</div>
<div class="col-md-6 d-flex align-items-center">
<div class="form-check mt-3">
{{ form.pmr_accessible }}
<label for="{{ form.pmr_accessible.id_for_label }}" class="form-check-label fw-semibold">
{% translate "Accessible PMR" %}
</label>
</div>
</div>
<div class="col-md-4">
<label for="{{ form.erp_category.id_for_label }}" class="form-label fw-semibold">{% translate "Catégorie d'accueil / ERP" %}</label>
{{ form.erp_category }}
{% if form.erp_category.errors %}<div class="text-danger small mt-1">{{ form.erp_category.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.erp_type.id_for_label }}" class="form-label fw-semibold">{% translate "Type d'établissement / ERP" %}</label>
{{ form.erp_type }}
{% if form.erp_type.errors %}<div class="text-danger small mt-1">{{ form.erp_type.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-4">
<label for="{{ form.max_capacity.id_for_label }}" class="form-label fw-semibold">{% translate "Jauge maximale d'accueil du public" %}</label>
{{ form.max_capacity }}
{% if form.max_capacity.errors %}<div class="text-danger small mt-1">{{ form.max_capacity.errors.0 }}</div>{% endif %}
</div>
</div>
</div>
</div>
<!-- Card 5: Diagnostics & Énergie -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold">
<i class="bi bi-lightning-charge me-2"></i>{% translate "Diagnostics & Énergie" %}
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="{{ form.dpe_rating.id_for_label }}" class="form-label fw-semibold">{% translate "Performance Énergétique (PEB / DPE / EPC)" %}</label>
{{ form.dpe_rating }}
{% if form.dpe_rating.errors %}<div class="text-danger small mt-1">{{ form.dpe_rating.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.ges_rating.id_for_label }}" class="form-label fw-semibold">{% translate "Émissions de Carbone & GES" %}</label>
{{ form.ges_rating }}
{% if form.ges_rating.errors %}<div class="text-danger small mt-1">{{ form.ges_rating.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-6 d-flex align-items-center">
<div class="form-check mt-2">
{{ form.has_asbestos_diag }}
<label for="{{ form.has_asbestos_diag.id_for_label }}" class="form-check-label fw-semibold">
{% translate "Diagnostic Amiante réalisé" %}
</label>
</div>
</div>
<div class="col-md-6 d-flex align-items-center">
<div class="form-check mt-2">
{{ form.has_lead_diag }}
<label for="{{ form.has_lead_diag.id_for_label }}" class="form-check-label fw-semibold">
{% translate "Diagnostic Plomb réalisé" %}
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Card 6: Géolocalisation (Carte & Coordonnées) -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold">
<i class="bi bi-geo-alt me-2"></i>{% translate "Coordonnées géographiques (GPS WGS84)" %}
</div>
<div class="card-body">
<p class="text-muted small mb-3">
<i class="bi bi-info-circle me-1"></i>{% translate "Cliquez sur la carte ou déplacez le marqueur pour ajuster la position du bâtiment. Vous pouvez également rechercher une adresse ou basculer en vue orthophoto." %}
</p>
<div id="edit-building-map" style="height: 400px; width: 100%; border-radius: 8px; border: 1px solid #dee2e6;" class="mb-3"></div>
<div class="row g-3">
<div class="col-md-6">
<label for="{{ form.lat.id_for_label }}" class="form-label fw-semibold">{% translate "Latitude" %}</label>
{{ form.lat }}
{% if form.lat.errors %}<div class="text-danger small mt-1">{{ form.lat.errors.0 }}</div>{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.lon.id_for_label }}" class="form-label fw-semibold">{% translate "Longitude" %}</label>
{{ form.lon }}
{% if form.lon.errors %}<div class="text-danger small mt-1">{{ form.lon.errors.0 }}</div>{% endif %}
</div>
</div>
</div>
</div>
<!-- Form Submit & Cancel -->
<div class="d-flex justify-content-end gap-2 mb-5">
<a href="{% url 'assets:publicbuilding_detail' building.pk %}" class="btn btn-secondary px-4">
<i class="bi bi-x-lg me-1"></i>{% translate "Annuler" %}
</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-circle me-1"></i>{% translate "Enregistrer les modifications" %}
</button>
</div>
</form>
</div>
{% endblock content %}
{% block script %}
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const latInput = document.getElementById("{{ form.lat.id_for_label }}");
const lonInput = document.getElementById("{{ form.lon.id_for_label }}");
let initLat = parseFloat("{{ center_lat|default_if_none:'' }}") || parseFloat(latInput.value) || 50.8477;
let initLon = parseFloat("{{ center_lon|default_if_none:'' }}") || parseFloat(lonInput.value) || 4.3572;
let hasCoords = Boolean("{{ center_lat|default_if_none:'' }}" || (latInput && latInput.value));
const map = new maplibregl.Map({
container: 'edit-building-map',
style: window.MAP_STYLE_LIGHT || 'https://tiles.openfreemap.org/styles/positron',
center: [initLon, initLat],
zoom: hasCoords ? 17 : 12,
});
// 1. Navigation & Geolocate Controls
map.addControl(new maplibregl.NavigationControl(), 'bottom-right');
map.addControl(new maplibregl.GeolocateControl({ positionOptions: { enableHighAccuracy: true }, trackUserLocation: true }), 'bottom-right');
// 2. Custom Address Search Control
if (typeof AddressSearchControl !== 'undefined') {
const searchCtrl = new AddressSearchControl({
autocompleteUrl: "{% url 'common:autocomplete_street' %}"
});
map.addControl(searchCtrl, 'top-left');
searchCtrl.addEventListener('address-selected', function(e) {
if (e.detail && e.detail.lon && e.detail.lat) {
const lon = parseFloat(e.detail.lon);
const lat = parseFloat(e.detail.lat);
map.flyTo({ center: [lon, lat], zoom: 18 });
setMarkerPosition(lat, lon);
}
});
}
// 3. Orthophotos / Satellite LayerSwitcherControl
if (typeof LayerSwitcherControl !== 'undefined') {
const layerSwitcher = new LayerSwitcherControl({
layers: [
{
id: 'ortho',
name: 'Orthophotos',
icon: 'satellite',
group: 'ortho',
layer: {
type: 'raster',
paint: { 'raster-opacity': 1 }
},
sublayers: [
{
id: 'urbis',
name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)',
default: true,
source: {
type: 'raster',
tiles: [
window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}'
],
tileSize: 256,
attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© CIRB / Urbis'
}
},
{
id: 'vlaanderen',
name: 'Vlaanderen 2023',
source: {
type: 'raster',
tiles: ['https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'],
tileSize: 256,
attribution: '© Digitaal Vlaanderen - Orthofoto 2023'
}
}
]
}
]
});
map.addControl(layerSwitcher, 'top-right');
}
// 4. Marker logic & Form synchronization
let marker = null;
if (hasCoords) {
marker = new maplibregl.Marker({ draggable: true, color: '#2B579A' })
.setLngLat([initLon, initLat])
.addTo(map);
marker.on('dragend', function() {
const lngLat = marker.getLngLat();
updateInputValues(lngLat.lat, lngLat.lng);
});
}
map.on('click', function(e) {
setMarkerPosition(e.lngLat.lat, e.lngLat.lng);
});
function setMarkerPosition(lat, lon) {
if (!marker) {
marker = new maplibregl.Marker({ draggable: true, color: '#2B579A' })
.setLngLat([lon, lat])
.addTo(map);
marker.on('dragend', function() {
const lngLat = marker.getLngLat();
updateInputValues(lngLat.lat, lngLat.lng);
});
} else {
marker.setLngLat([lon, lat]);
}
updateInputValues(lat, lon);
}
function updateInputValues(lat, lon) {
if (latInput) latInput.value = parseFloat(lat).toFixed(7);
if (lonInput) lonInput.value = parseFloat(lon).toFixed(7);
}
function syncMarkerFromInputs() {
const lat = parseFloat(latInput.value);
const lon = parseFloat(lonInput.value);
if (!isNaN(lat) && !isNaN(lon)) {
setMarkerPosition(lat, lon);
map.panTo([lon, lat]);
}
}
if (latInput) latInput.addEventListener('change', syncMarkerFromInputs);
if (lonInput) lonInput.addEventListener('change', syncMarkerFromInputs);
});
</script>
{% endblock script %}

View file

@ -0,0 +1,161 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{% translate "Bâtiments publics" %} | StreetUp{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1"><i class="bi bi-building me-2 text-primary"></i>{% translate "Gestion des Bâtiments Publics" %}</h1>
<p class="text-muted mb-0">{% translate "Patrimoine bâti, pièces, équipements techniques et diagnostics ERP" %}</p>
</div>
<div>
<a href="{% url 'admin:assets_publicbuilding_add' %}" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouveau Bâtiment" %}
</a>
</div>
</div>
<!-- Filters Card -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label class="form-label small text-muted">{% translate "Recherche" %}</label>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="q" class="form-control" placeholder="{% translate 'Code, nom, adresse, propriétaire...' %}" value="{{ search_query }}">
</div>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">{% translate "Usage" %}</label>
<select name="usage_type" class="form-select form-select-sm">
<option value="">-- {% translate "Tous les usages" %} --</option>
{% for key, label in usage_type_choices %}
<option value="{{ key }}" {% if selected_usage == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">{% translate "Statut" %}</label>
<select name="building_status" class="form-select form-select-sm">
<option value="">-- {% translate "Tous les statuts" %} --</option>
{% for key, label in building_status_choices %}
<option value="{{ key }}" {% if selected_status == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">{% translate "Diagnostic DPE" %}</label>
<select name="dpe_rating" class="form-select form-select-sm">
<option value="">-- {% translate "Tous DPE" %} --</option>
{% for key, label in dpe_rating_choices %}
<option value="{{ key }}" {% if selected_dpe == key %}selected{% endif %}>DPE {{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3 d-flex align-items-end gap-2">
<button type="submit" class="btn btn-sm btn-primary w-100"><i class="bi bi-filter me-1"></i>{% translate "Filtrer" %}</button>
<a href="{% url 'assets:buildings_index' %}" class="btn btn-sm btn-outline-secondary w-100">{% translate "Réinitialiser" %}</a>
</div>
</form>
</div>
</div>
<!-- Table of Public Buildings -->
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% translate "Code / Nom" %}</th>
<th>{% translate "Type & Parent" %}</th>
<th>{% translate "Usage principal" %}</th>
<th>{% translate "Propriétaire & Gestionnaire" %}</th>
<th>{% translate "ERP" %}</th>
<th>{% translate "DPE / GES" %}</th>
<th>{% translate "Statut" %}</th>
<th class="text-end">{% translate "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for b in buildings %}
<tr>
<td>
<a href="{% url 'assets:publicbuilding_detail' b.pk %}" class="fw-bold text-decoration-none">
{{ b.code }}
</a>
<div class="small text-muted">{{ b.get_name }}</div>
</td>
<td>
<span class="badge bg-primary-subtle text-primary border me-1">
{{ b.get_location_type_display }}
</span>
{% if b.parent_location %}
<div class="small text-muted mt-1"><i class="bi bi-diagram-2 me-1"></i>{{ b.parent_location.code }}</div>
{% endif %}
</td>
<td>
<span class="badge bg-secondary-subtle text-dark border">
{{ b.get_usage_type_display }}
</span>
</td>
<td>
{% if b.owner %}
<div class="small"><i class="bi bi-shield-check text-muted me-1"></i><strong>{% translate "Propriétaire" %}:</strong> {{ b.owner }}</div>
{% endif %}
{% if b.manager %}
<div class="small text-muted"><i class="bi bi-person me-1"></i><strong>{% translate "Gestionnaire" %}:</strong> {{ b.manager.get_full_name|default:b.manager.username }}</div>
{% endif %}
</td>
<td>
{% if b.is_erp %}
<span class="badge bg-info-subtle text-info border">
<i class="bi bi-people me-1"></i>ERP Cat {{ b.erp_category|default:"-" }} ({{ b.erp_type|default:"-" }})
</span>
{% else %}
<span class="badge bg-light text-muted border">{% translate "Non ERP" %}</span>
{% endif %}
</td>
<td>
{% if b.dpe_rating %}
<span class="badge bg-success border me-1">DPE {{ b.dpe_rating }}</span>
{% endif %}
{% if b.ges_rating %}
<span class="badge bg-primary border">GES {{ b.ges_rating }}</span>
{% endif %}
</td>
<td>
<span class="badge {% if b.building_status == 'in_service' %}bg-success-subtle text-success{% else %}bg-warning-subtle text-warning{% endif %} border">
{{ b.get_building_status_display }}
</span>
</td>
<td class="text-end">
<a href="{% url 'assets:publicbuilding_detail' b.pk %}" class="btn btn-sm btn-outline-primary" title="{% translate 'Voir détails' %}">
<i class="bi bi-eye"></i>
</a>
{% if b.can_edit %}
<a href="{% url 'assets:publicbuilding_edit' b.pk %}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Éditer' %}">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-5 text-muted">
<i class="bi bi-building-exclamation fs-1 d-block mb-2"></i>
{% translate "Aucun bâtiment public ne correspond à vos critères." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,74 @@
{% load i18n %}
<ul class="list-group list-group-flush border-0">
{% for child_node in node.children %}
<li class="list-group-item border-0 py-1 px-0">
<!-- Row Header -->
<div class="d-flex align-items-center justify-content-between py-1 px-3">
<!-- Contenu gauche (indenté selon la profondeur) -->
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
{% if child_node.children or child_node.assets %}
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-loc-{{ child_node.loc.id }}"
aria-expanded="true">
<i class="bi bi-chevron-down small"></i>
</button>
{% else %}
<span class="px-2"></span>
{% endif %}
{% if child_node.loc.location_type == 'building' or child_node.loc.location_type == 'site' %}
<i class="bi bi-building text-primary"></i>
{% elif child_node.loc.location_type == 'level' %}
<i class="bi bi-layers text-secondary"></i>
{% elif child_node.loc.location_type == 'room' %}
<i class="bi bi-door-open text-info"></i>
{% else %}
<i class="bi bi-geo-alt text-dark"></i>
{% endif %}
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
<a href="{{ child_node.loc.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
{{ child_node.loc.code }}
</a>
<span class="small text-muted">- {{ child_node.loc.get_name }}</span>
<span class="badge bg-primary-subtle text-primary border small">{{ child_node.loc.get_location_type_display }}</span>
{% if child_node.assets %}
<button class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-assets-loc-{{ child_node.loc.id }}"
aria-expanded="false">
<i class="bi bi-tools me-1"></i>Assets ({{ child_node.assets|length }})
</button>
{% endif %}
</div>
</div>
<!-- Collapsible Assets for this location (NON VISIBLES PAR DÉFAUT) -->
{% if child_node.assets %}
<div id="tree-assets-loc-{{ child_node.loc.id }}" class="collapse mt-2" style="padding-left: calc({{ depth|default:1 }} * 1.25rem);">
<div class="card card-body bg-light border-info-subtle p-2 mb-2 me-3">
<div class="fw-bold small text-info mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ child_node.loc.code }} :</div>
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=child_node.assets depth=depth|default:1|add:1 only %}
</div>
</div>
{% endif %}
<!-- Collapsible Child Locations -->
{% if child_node.children %}
<div id="tree-loc-{{ child_node.loc.id }}" class="collapse show">
{% include "assets/buildings/publicbuilding_location_tree.html" with node=child_node depth=depth|default:1|add:1 only %}
</div>
{% endif %}
</li>
{% empty %}
{% if not node.assets %}
<li class="list-group-item border-0 text-muted small fst-italic py-2 ps-3">
{% translate "Aucune sous-localisation." %}
</li>
{% endif %}
{% endfor %}
</ul>

View file

@ -0,0 +1,70 @@
{% load i18n %}
<ul class="list-group list-group-flush bg-transparent">
{% for asset_node in asset_nodes %}
<li class="list-group-item bg-transparent border-0 py-1 px-0">
<!-- Row Header -->
<div class="d-flex align-items-center justify-content-between py-1 px-2">
<!-- Contenu gauche (indenté selon la profondeur) -->
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
{% if asset_node.children %}
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-asset-{{ asset_node.asset.id }}"
aria-expanded="false">
<i class="bi bi-chevron-right small"></i>
</button>
{% else %}
<span class="px-1"></span>
{% endif %}
{% if asset_node.asset.asset_type == 'play_structure' %}
<i class="bi bi-house-door-fill text-success small" title="{% translate 'Structure de jeu' %}"></i>
{% elif asset_node.asset.asset_type == 'swing' %}
<i class="bi bi-arrow-repeat text-success small" title="{% translate 'Balançoire / Portique' %}"></i>
{% elif asset_node.asset.asset_type == 'slide' %}
<i class="bi bi-sign-turn-right-fill text-success small" title="{% translate 'Toboggan' %}"></i>
{% elif asset_node.asset.asset_type == 'spring_rider' %}
<i class="bi bi-emoji-smile-fill text-success small" title="{% translate 'Jeu à ressort' %}"></i>
{% elif asset_node.asset.asset_type == 'zip_line' %}
<i class="bi bi-arrows-expand text-success small" title="{% translate 'Tyrolienne' %}"></i>
{% elif asset_node.asset.asset_type == 'sports_goal' %}
<i class="bi bi-dribbble text-primary small" title="{% translate 'But / Panneau basket' %}"></i>
{% elif asset_node.asset.asset_type == 'fitness_station' %}
<i class="bi bi-activity text-danger small" title="{% translate 'Agrès fitness' %}"></i>
{% elif asset_node.asset.asset_type == 'skate_module' %}
<i class="bi bi-lightning-fill text-warning small" title="{% translate 'Module skate' %}"></i>
{% elif asset_node.asset.asset_type == 'stage_platform' %}
<i class="bi bi-display text-info small" title="{% translate 'Scène / Podium' %}"></i>
{% elif asset_node.asset.asset_type == 'event_power_box' %}
<i class="bi bi-plug-fill text-warning small" title="{% translate 'Borne électrique' %}"></i>
{% elif asset_node.asset.asset_type == 'drinking_fountain' %}
<i class="bi bi-droplet-fill text-primary small" title="{% translate 'Fontaine d\'eau' %}"></i>
{% elif asset_node.asset.asset_type == 'bench_table' %}
<i class="bi bi-table text-secondary small" title="{% translate 'Banc / Table' %}"></i>
{% elif asset_node.asset.asset_type == 'fence_gate' %}
<i class="bi bi-shield-lock-fill text-secondary small" title="{% translate 'Clôture' %}"></i>
{% else %}
<i class="bi bi-gear-fill text-success small"></i>
{% endif %}
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
<a href="{{ asset_node.asset.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
{{ asset_node.asset.code }}
</a>
<span class="small">{{ asset_node.asset.get_name }}</span>
<span class="badge bg-success-subtle text-success border small ms-1">{{ asset_node.asset.get_asset_type_display }}</span>
{% if asset_node.asset.norm %}
<span class="badge bg-secondary-subtle text-dark border small ms-1">{{ asset_node.asset.get_norm_display }}</span>
{% endif %}
</div>
</div>
{% if asset_node.children %}
<div id="tree-asset-{{ asset_node.asset.id }}" class="collapse mt-1">
{% include "assets/outdoor/outdoorlocation_asset_tree.html" with asset_nodes=asset_node.children depth=depth|default:1|add:1 only %}
</div>
{% endif %}
</li>
{% endfor %}
</ul>

View file

@ -0,0 +1,490 @@
{% extends "base.html" %}
{% load static i18n documents_tags %}
{% block title %}{{ location.code }} - {{ location.get_name }} | StreetUp{% endblock %}
{% block extra_head %}
<!-- MapLibre GL CSS -->
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb & Actions -->
<div class="d-flex justify-content-between align-items-center mb-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'assets:index' %}">{% translate "Espaces & Équipements plein air" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ location.code }}</li>
</ol>
</nav>
<div class="d-flex gap-2">
<a href="{% url 'assets:index' %}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>{% translate "Retour à la liste" %}
</a>
</div>
</div>
<!-- Header Card -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h2 class="h4 mb-1">
<i class="bi bi-dribbble text-success me-2"></i>{{ location.code }} - {{ location.get_name }}
</h2>
<p class="text-muted mb-2"><i class="bi bi-geo-alt me-1"></i>{{ location.address|default:_("Adresse non renseignée") }}</p>
<div class="d-flex gap-2 flex-wrap">
<span class="badge bg-success-subtle text-success border">{{ location.get_site_type_display }}</span>
{% if location.pmr_accessible %}
<span class="badge bg-info-subtle text-info border"><i class="bi bi-person-wheelchair me-1"></i>PMR Accessible</span>
{% endif %}
{% if location.has_water_point %}
<span class="badge bg-primary-subtle text-primary border"><i class="bi bi-droplet me-1"></i>Eau potable</span>
{% endif %}
{% if location.has_power_supply %}
<span class="badge bg-warning-subtle text-warning border"><i class="bi bi-lightning-charge me-1"></i>Électricité</span>
{% endif %}
{% if location.is_fenced %}
<span class="badge bg-secondary-subtle text-dark border"><i class="bi bi-shield-check me-1"></i>Clôturé</span>
{% endif %}
</div>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
{% if location.surface_m2 %}
<div class="d-inline-block text-center me-3">
<span class="d-block text-muted small">{% translate "Surface" %}</span>
<span class="badge bg-success fs-5 px-3 py-2">{{ location.surface_m2|floatformat:0 }} m²</span>
</div>
{% endif %}
{% if location.max_capacity %}
<div class="d-inline-block text-center">
<span class="d-block text-muted small">{% translate "Jauge max" %}</span>
<span class="badge bg-primary fs-5 px-3 py-2">{{ location.max_capacity }} pers.</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 3 Core Tabs Navigation -->
<ul class="nav nav-tabs mb-4" id="outdoorTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab">
<i class="bi bi-info-circle me-1"></i>{% translate "Vue d'ensemble" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments" type="button" role="tab">
<i class="bi bi-gear me-1"></i>{% translate "Assets & Équipements" %} ({{ equipments|length }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="interventions-tab" data-bs-toggle="tab" data-bs-target="#interventions" type="button" role="tab">
<i class="bi bi-tools me-1"></i>{% translate "Interventions" %} ({{ interventions.count }})
</button>
</li>
</ul>
<!-- Tabs Content -->
<div class="tab-content" id="outdoorTabsContent">
<!-- 1. Vue d'ensemble -->
<div class="tab-pane fade show active" id="overview" role="tabpanel">
<div class="row g-4">
<!-- Left Column: Propriété, Caractéristiques & Arborescence -->
<div class="col-md-7">
<!-- Card 1: Propriété & Responsables -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold"><i class="bi bi-person-badge me-2"></i>{% translate "Propriété & Responsables" %}</div>
<div class="card-body">
<table class="table table-borderless mb-0">
<tr>
<th class="w-50 text-muted">{% translate "Propriétaire" %} :</th>
<td>{{ location.owner|default:"-" }}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Gestionnaire référent" %} :</th>
<td>{% if location.manager %}{{ location.manager.get_full_name|default:location.manager.username }}{% else %}-{% endif %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Service gestionnaire" %} :</th>
<td>{{ location.occupant_entity|default:"-" }}</td>
</tr>
</table>
</div>
</div>
<!-- Card 2: Caractéristiques physiques & Aménagements -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold"><i class="bi bi-rulers me-2"></i>{% translate "Caractéristiques physiques & Aménagements" %}</div>
<div class="card-body">
<table class="table table-borderless mb-0">
<tr>
<th class="w-50 text-muted">{% translate "Surface totale" %} :</th>
<td>{{ location.surface_m2|default:"-" }} m²</td>
</tr>
<tr>
<th class="text-muted">{% translate "Jauge / Capacité d'accueil" %} :</th>
<td>{% if location.max_capacity %}{{ location.max_capacity }} {% translate "personnes" %}{% else %}-{% endif %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Accessibilité PMR" %} :</th>
<td>{% if location.pmr_accessible %}<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>{% translate "Oui" %}</span>{% else %}<span class="text-muted">{% translate "Non" %}</span>{% endif %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Point d'eau potable" %} :</th>
<td>{% if location.has_water_point %}<span class="text-primary"><i class="bi bi-check-circle-fill me-1"></i>{% translate "Oui" %}</span>{% else %}<span class="text-muted">{% translate "Non" %}</span>{% endif %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Alimentation électrique" %} :</th>
<td>{% if location.has_power_supply %}<span class="text-warning"><i class="bi bi-check-circle-fill me-1"></i>{% translate "Disponible" %}</span>{% else %}<span class="text-muted">{% translate "Non" %}</span>{% endif %}</td>
</tr>
<tr>
<th class="text-muted">{% translate "Clôture & Sécurité" %} :</th>
<td>{% if location.is_fenced %}<span class="text-secondary"><i class="bi bi-shield-check me-1"></i>{% translate "Site clôturé" %}</span>{% else %}<span class="text-muted">{% translate "Accès libre" %}</span>{% endif %}</td>
</tr>
</table>
</div>
</div>
<!-- Card 3: Arborescence Unifiée des Localisations & Assets -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence Hiérarchique" %}</span>
<span class="badge bg-success-subtle text-success border">{{ location_tree.children|length }} {% translate "sous-localisation(s)" %}</span>
</div>
<div class="card-body p-3">
<!-- Localisation Parente (si existante) -->
{% if parent_location %}
<div class="card bg-light border-secondary-subtle p-2 mb-3">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-geo-alt-fill text-danger fs-5"></i>
<div>
<div class="small text-muted fw-semibold">{% translate "Localisation parente" %} :</div>
<a href="{{ parent_location.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline">
{{ parent_location.code }} - {{ parent_location.get_name }}
</a>
<span class="badge bg-secondary-subtle text-dark border small ms-1">{{ parent_location.get_site_type_display }}</span>
</div>
</div>
<a href="{{ parent_location.get_absolute_url }}" class="btn btn-sm btn-outline-secondary py-0 px-2 small">
<i class="bi bi-arrow-up me-1"></i>{% translate "Voir la parente" %}
</a>
</div>
</div>
{% endif %}
<!-- Localisation Courante -->
<div class="p-2 border rounded bg-white mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
{% if location.site_type == 'playground' %}
<i class="bi bi-balloon-fill text-success fs-5"></i>
{% elif location.site_type == 'sports_ground' %}
<i class="bi bi-trophy-fill text-primary fs-5"></i>
{% elif location.site_type == 'multisports_agora' %}
<i class="bi bi-dribbble text-primary fs-5"></i>
{% elif location.site_type == 'outdoor_fitness' %}
<i class="bi bi-activity text-danger fs-5"></i>
{% elif location.site_type == 'skatepark_pumptrack' %}
<i class="bi bi-scooter text-warning fs-5"></i>
{% elif location.site_type == 'open_air_theater' %}
<i class="bi bi-ticket-detailed-fill text-info fs-5"></i>
{% elif location.site_type == 'park_recreation' %}
<i class="bi bi-tree-fill text-success fs-5"></i>
{% elif location.site_type == 'dog_park' %}
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#fd7e14" class="bi" viewBox="0 0 512 512"><path d="M226.5 92.9c14.3 42.9-.3 86.2-32.6 96.8s-70.1-15.6-84.4-58.5s.3-86.2 32.6-96.8s70.1 15.6 84.4 58.5zM100.4 198.6c18.9 32.4 14.3 70.1-10.2 84.1s-59.7-.9-78.5-33.3S-2.7 179.3 21.8 165.3s59.7.9 78.6 33.3zM69.2 401.2c-7.1-17.4-2.8-37.1 11.6-47.5c22.7-16.4 57.5-12.7 87.2 8.7c31.1 22.3 53.6 56.4 46.5 73.8c-7.1 17.4-32.8 17.3-64 -5c-31.1-22.4-74.2-12.6-81.3 4.8zm342.4-202.6c18.9-32.4 54.1-47.3 78.6-33.3s29.1 51.7 10.2 84.1s-54.1 47.3-78.6 33.3s-29.1-51.7-10.2-84.1zm-56.5-105.7c14.3-42.9 52.1-69.1 84.4-58.5s46.9 53.9 32.6 96.8s-52.1 69.1-84.4 58.5s-46.9-53.9-32.6-96.8zM442.8 401.2c-7.1-17.4-50.2-27.2-81.3-4.8c-31.2 22.3-56.9 22.4-64 5c-7.1-17.4 15.4-51.5 46.5-73.8c29.7-21.4 64.5-25.1 87.2-8.7c14.4 10.4 18.7 30.1 11.6 47.5zM256 224c-53 0-103 34.6-122.6 88.8c-17.7 49 1.1 103.8 44.5 131.7c35.6 22.9 80 23.5 116.2 0c43.4-27.9 62.2-82.7 44.5-131.7C359 258.6 309 224 256 224z"/></svg>
{% elif location.site_type == 'cemetery' %}
<i class="bi bi-flower2 text-secondary fs-5"></i>
{% else %}
<i class="bi bi-geo-alt-fill text-success fs-5"></i>
{% endif %}
<span class="fw-bold fs-6 text-success">{{ location.code }}</span>
<span class="fw-bold fs-6">- {{ location.get_name }}</span>
<span class="badge bg-success text-white ms-1">{{ location.get_site_type_display }}</span>
{% if location_tree.assets %}
<button class="btn btn-sm btn-outline-success py-0 px-2 small ms-2 text-nowrap"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-assets-root"
aria-expanded="false">
<i class="bi bi-tools me-1"></i>Assets ({{ location_tree.assets|length }})
</button>
{% endif %}
</div>
</div>
<!-- Collapsible Assets de la localisation courante -->
{% if location_tree.assets %}
<div id="tree-assets-root" class="collapse mt-2 ms-4">
<div class="card card-body bg-light border-success-subtle p-2 mb-2">
<div class="fw-bold small text-success mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ location.code }} :</div>
{% include "assets/outdoor/outdoorlocation_asset_tree.html" with asset_nodes=location_tree.assets only %}
</div>
</div>
{% endif %}
<!-- Sous-localisations & leurs assets -->
<div class="mt-2">
{% include "assets/outdoor/outdoorlocation_tree.html" with node=location_tree depth=1 only %}
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Localisation & Documents -->
<div class="col-md-5">
<!-- Card Localisation & Carte interactive -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
<span><i class="bi bi-geo-alt me-2"></i>{% translate "Localisation & Carte" %}</span>
{% if center_lat and center_lon %}
<a href="https://www.google.com/maps?cbll={{ center_lat|floatformat:'6u'}},{{ center_lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid="
target="_blank" class="btn btn-sm btn-outline-secondary py-0 px-2">
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="14" class="me-1">
Street View
</a>
{% endif %}
</div>
<div class="card-body p-0">
<div id="outdoor-map" style="height: 340px; width: 100%; border-radius: 0 0 4px 4px;"></div>
</div>
<div class="card-footer bg-white p-3">
<p class="mb-2 small">
<strong>{% translate "Adresse" %} :</strong> {{ location.address|default:_("Non renseignée") }}
</p>
{% if center_lat and center_lon %}
<div class="small text-muted font-monospace">
WGS84: {{ center_lat|floatformat:6 }}, {{ center_lon|floatformat:6 }}
</div>
{% endif %}
</div>
</div>
<!-- Card Documents associés -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<span class="fw-bold"><i class="bi bi-file-earmark-text me-2"></i>{% translate "Documents associés" %} ({{ documents.count }})</span>
<a href="{% url 'admin:assets_outdoorlocationdocument_add' %}?site={{ location.pk }}" class="btn btn-sm btn-outline-primary py-0 px-2">
<i class="bi bi-plus-lg me-1"></i>{% translate "Ajouter" %}
</a>
</div>
<div class="card-body">
<div class="row row-cols-1 row-cols-sm-2 g-3">
{% for doc in documents %}
<div class="col">
<div class="card h-100 border text-center p-2">
{% if doc.document.file %}
<i class="bi bi-file-earmark-pdf text-danger fs-2 mb-1"></i>
<div class="small text-truncate w-100 fw-bold" title="{{ doc.document.file.name }}">{{ doc.document.file.name }}</div>
<a href="{{ doc.document.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary mt-2">
<i class="bi bi-download me-1"></i>{% translate "Télécharger" %}
</a>
{% else %}
<i class="bi bi-file-earmark text-secondary fs-2 mb-1"></i>
<span class="text-muted small">{% translate "Fichier indisponible" %}</span>
{% endif %}
</div>
</div>
{% empty %}
<div class="col-12 text-center text-muted py-3">
<i class="bi bi-folder-x fs-4 d-block mb-1"></i>
{% translate "Aucun document associé." %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. Assets & Équipements Tab -->
<div class="tab-pane fade" id="equipments" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title h6 mb-0"><i class="bi bi-tools me-2"></i>{% translate "Assets Physiques & Équipements du Site et de ses sous-localisations" %}</h5>
<span class="badge bg-success-subtle text-success border">{{ equipments|length }} {% translate "asset(s)" %}</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% translate "Code" %}</th>
<th>{% translate "Nom" %}</th>
<th>{% translate "Type d'asset" %}</th>
<th>{% translate "Localisation spécifique" %}</th>
<th>{% translate "Norme" %}</th>
<th>{% translate "Statut" %}</th>
<th class="text-end">{% translate "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for eq in equipments %}
<tr>
<td class="fw-bold">
<a href="{{ eq.get_absolute_url }}">{{ eq.code }}</a>
</td>
<td>{{ eq.get_name }}</td>
<td>
<span class="badge bg-success-subtle text-success border">{{ eq.get_asset_type_display }}</span>
</td>
<td>
{% if eq.location %}
<a href="{{ eq.location.get_absolute_url }}">{{ eq.location.code }} - {{ eq.location.get_name }}</a>
{% else %}
<span class="text-muted">{% translate "Site principal" %}</span>
{% endif %}
</td>
<td>{{ eq.get_norm_display|default:"-" }}</td>
<td>
<span class="badge bg-secondary">{{ eq.get_status_display }}</span>
</td>
<td class="text-end">
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center text-muted py-4">
{% translate "Aucun asset physique répertorié pour ce site." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 3. Interventions Tab -->
<div class="tab-pane fade" id="interventions" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title h6 mb-0"><i class="bi bi-tools me-2"></i>{% translate "Historique des Interventions" %}</h5>
<span class="badge bg-primary-subtle text-primary border">{{ interventions.count }} {% translate "intervention(s)" %}</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% translate "N° Ticket" %}</th>
<th>{% translate "Titre" %}</th>
<th>{% translate "Statut" %}</th>
<th>{% translate "Priorité" %}</th>
<th>{% translate "Créée le" %}</th>
<th class="text-end">{% translate "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for intervention in interventions %}
<tr>
<td class="fw-bold">#{{ intervention.pk }}</td>
<td>{{ intervention.title }}</td>
<td><span class="badge bg-info">{{ intervention.get_status_display }}</span></td>
<td><span class="badge bg-warning text-dark">{{ intervention.get_priority_display }}</span></td>
<td>{{ intervention.creation_time|date:"d/m/Y H:i" }}</td>
<td class="text-end">
<a href="{% url 'interventions:intervention_detail' intervention.pk %}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center text-muted py-4">
{% translate "Aucune intervention enregistrée sur cet espace." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const centerLat = {{ center_lat|default:"null" }};
const centerLon = {{ center_lon|default:"null" }};
const locationGeojson = {{ location_geojson_json|safe }};
if (centerLat !== null && centerLon !== null) {
const map = new maplibregl.Map({
container: 'outdoor-map',
style: '{{ MAP_STYLE_LIGHT|default:"https://api.maptiler.com/maps/streets-v2-light/style.json" }}',
center: [centerLon, centerLat],
zoom: 16
});
map.addControl(new maplibregl.NavigationControl());
map.on('load', function() {
if (locationGeojson) {
map.addSource('location-shape', {
'type': 'geojson',
'data': {
'type': 'Feature',
'geometry': locationGeojson
}
});
if (locationGeojson.type === 'Polygon' || locationGeojson.type === 'MultiPolygon') {
map.addLayer({
'id': 'location-fill',
'type': 'fill',
'source': 'location-shape',
'paint': {
'fill-color': '#198754',
'fill-opacity': 0.3
}
});
map.addLayer({
'id': 'location-outline',
'type': 'line',
'source': 'location-shape',
'paint': {
'line-color': '#198754',
'line-width': 3
}
});
} else {
new maplibregl.Marker({ color: '#198754' })
.setLngLat([centerLon, centerLat])
.addTo(map);
}
} else {
new maplibregl.Marker({ color: '#198754' })
.setLngLat([centerLon, centerLat])
.addTo(map);
}
});
} else {
document.getElementById('outdoor-map').innerHTML = '<div class="d-flex align-items-center justify-content-center h-100 text-muted"><i class="bi bi-geo-alt-fill me-2"></i>{% translate "Géométrie non disponible" %}</div>';
}
});
</script>
{% endblock content %}

View file

@ -0,0 +1,88 @@
{% load i18n %}
<ul class="list-group list-group-flush border-0">
{% for child_node in node.children %}
<li class="list-group-item border-0 py-1 px-0">
<!-- Row Header -->
<div class="d-flex align-items-center justify-content-between py-1 px-3">
<!-- Contenu gauche (indenté selon la profondeur) -->
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
{% if child_node.children or child_node.assets %}
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-loc-{{ child_node.loc.id }}"
aria-expanded="true">
<i class="bi bi-chevron-down small"></i>
</button>
{% else %}
<span class="px-2"></span>
{% endif %}
{% if child_node.loc.site_type == 'playground' %}
<i class="bi bi-balloon-fill text-success fs-6" title="{% translate 'Aire de jeux' %}"></i>
{% elif child_node.loc.site_type == 'sports_ground' %}
<i class="bi bi-trophy-fill text-primary fs-6" title="{% translate 'Terrain de sport' %}"></i>
{% elif child_node.loc.site_type == 'multisports_agora' %}
<i class="bi bi-dribbble text-primary fs-6" title="{% translate 'City-stadium / Agora' %}"></i>
{% elif child_node.loc.site_type == 'outdoor_fitness' %}
<i class="bi bi-activity text-danger fs-6" title="{% translate 'Fitness plein air' %}"></i>
{% elif child_node.loc.site_type == 'skatepark_pumptrack' %}
<i class="bi bi-scooter text-warning fs-6" title="{% translate 'Skatepark & Pumptrack' %}"></i>
{% elif child_node.loc.site_type == 'open_air_theater' %}
<i class="bi bi-ticket-detailed-fill text-info fs-6" title="{% translate 'Théâtre en plein air' %}"></i>
{% elif child_node.loc.site_type == 'park_recreation' %}
<i class="bi bi-tree-fill text-success fs-6" title="{% translate 'Parc récréatif' %}"></i>
{% elif child_node.loc.site_type == 'dog_park' %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#fd7e14" class="bi" viewBox="0 0 512 512" title="{% translate 'Espace canin / Canisite' %}"><path d="M226.5 92.9c14.3 42.9-.3 86.2-32.6 96.8s-70.1-15.6-84.4-58.5s.3-86.2 32.6-96.8s70.1 15.6 84.4 58.5zM100.4 198.6c18.9 32.4 14.3 70.1-10.2 84.1s-59.7-.9-78.5-33.3S-2.7 179.3 21.8 165.3s59.7.9 78.6 33.3zM69.2 401.2c-7.1-17.4-2.8-37.1 11.6-47.5c22.7-16.4 57.5-12.7 87.2 8.7c31.1 22.3 53.6 56.4 46.5 73.8c-7.1 17.4-32.8 17.3-64 -5c-31.1-22.4-74.2-12.6-81.3 4.8zm342.4-202.6c18.9-32.4 54.1-47.3 78.6-33.3s29.1 51.7 10.2 84.1s-54.1 47.3-78.6 33.3s-29.1-51.7-10.2-84.1zm-56.5-105.7c14.3-42.9 52.1-69.1 84.4-58.5s46.9 53.9 32.6 96.8s-52.1 69.1-84.4 58.5s-46.9-53.9-32.6-96.8zM442.8 401.2c-7.1-17.4-50.2-27.2-81.3-4.8c-31.2 22.3-56.9 22.4-64 5c-7.1-17.4 15.4-51.5 46.5-73.8c29.7-21.4 64.5-25.1 87.2-8.7c14.4 10.4 18.7 30.1 11.6 47.5zM256 224c-53 0-103 34.6-122.6 88.8c-17.7 49 1.1 103.8 44.5 131.7c35.6 22.9 80 23.5 116.2 0c43.4-27.9 62.2-82.7 44.5-131.7C359 258.6 309 224 256 224z"/></svg>
{% elif child_node.loc.site_type == 'cemetery' %}
<i class="bi bi-flower2 text-secondary fs-6" title="{% translate 'Cimetière' %}"></i>
{% elif child_node.loc.site_type == 'event_plaza' %}
<i class="bi bi-flag-fill text-primary fs-6" title="{% translate 'Place publique' %}"></i>
{% else %}
<i class="bi bi-geo-alt-fill text-success fs-6"></i>
{% endif %}
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
<a href="{{ child_node.loc.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
{{ child_node.loc.code }}
</a>
<span class="small text-muted">- {{ child_node.loc.get_name }}</span>
<span class="badge bg-success-subtle text-success border small">{{ child_node.loc.get_site_type_display }}</span>
{% if child_node.assets %}
<button class="btn btn-sm btn-outline-success py-0 px-2 small text-nowrap"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tree-assets-loc-{{ child_node.loc.id }}"
aria-expanded="false">
<i class="bi bi-tools me-1"></i>Assets ({{ child_node.assets|length }})
</button>
{% endif %}
</div>
</div>
<!-- Collapsible Assets pour cette localisation (NON VISIBLES PAR DÉFAUT) -->
{% if child_node.assets %}
<div id="tree-assets-loc-{{ child_node.loc.id }}" class="collapse mt-2" style="padding-left: calc({{ depth|default:1 }} * 1.25rem);">
<div class="card card-body bg-light border-success-subtle p-2 mb-2 me-3">
<div class="fw-bold small text-success mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ child_node.loc.code }} :</div>
{% include "assets/outdoor/outdoorlocation_asset_tree.html" with asset_nodes=child_node.assets depth=depth|default:1|add:1 only %}
</div>
</div>
{% endif %}
<!-- Collapsible Sous-localisations -->
{% if child_node.children %}
<div id="tree-loc-{{ child_node.loc.id }}" class="collapse show">
{% include "assets/outdoor/outdoorlocation_tree.html" with node=child_node depth=depth|default:1|add:1 only %}
</div>
{% endif %}
</li>
{% empty %}
{% if not node.assets %}
<li class="list-group-item border-0 text-muted small fst-italic py-2 ps-3">
{% translate "Aucune sous-localisation." %}
</li>
{% endif %}
{% endfor %}
</ul>

View file

@ -416,7 +416,33 @@
</div> </div>
{# Layout selection buttons - aligned to the right #} {# Layout selection buttons - aligned to the right #}
<div class="ms-auto" style="z-index: 10;"> <div class="ms-auto d-flex align-items-center gap-2" style="z-index: 10;">
{# Dropdown Tableaux spécifiques (générique selon les thématiques accessibles) #}
{% if specific_tables %}
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle rounded-pill px-3 border shadow-sm d-inline-flex align-items-center"
type="button" id="specificTablesDropdown" data-bs-toggle="dropdown" aria-expanded="false">
{% translate "Répertoires" %}
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-1" aria-labelledby="specificTablesDropdown" style="min-width: 270px;">
<li><h6 class="dropdown-header text-uppercase small text-muted">{% translate "Répertoires spécifiques" %}</h6></li>
{% for table in specific_tables %}
<li>
<a class="dropdown-item d-flex align-items-center gap-3 py-2" href="{{ table.url }}">
<i class="bi {{ table.icon }} text-primary fs-5"></i>
<div>
<div class="fw-bold small text-dark">{{ table.title }}</div>
{% if table.description %}
<div class="text-muted" style="font-size: 0.75rem;">{{ table.description }}</div>
{% endif %}
</div>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button id="layout-btn-table" class="btn btn-light" type="button" title="{% translate 'Vue tableau uniquement' %}"> <button id="layout-btn-table" class="btn btn-light" type="button" title="{% translate 'Vue tableau uniquement' %}">
<i class="bi bi-table"></i> <i class="bi bi-table"></i>
@ -692,6 +718,7 @@
const THEMATIC_CATEGORIES = {{ thematic_categories_json|safe }}; const THEMATIC_CATEGORIES = {{ thematic_categories_json|safe }};
const ADD_MODAL_CONFIG = {{ add_modal_config_json|safe }}; const ADD_MODAL_CONFIG = {{ add_modal_config_json|safe }};
const SELECTED_THEMATICS = {{ selected_thematics|safe }}; const SELECTED_THEMATICS = {{ selected_thematics|safe }};
const HAS_URL_THEMATICS = {% if has_url_thematics %}true{% else %}false{% endif %};
const SELECTED_CATEGORIES = {{ selected_categories_json|safe }}; const SELECTED_CATEGORIES = {{ selected_categories_json|safe }};
const PREVENTIVE_DRAFT_ID = "{{ preventive_draft_id|default:'' }}"; const PREVENTIVE_DRAFT_ID = "{{ preventive_draft_id|default:'' }}";
const LOCKED_THEMATIC_CODE = "{{ locked_thematic_code|default:'' }}"; const LOCKED_THEMATIC_CODE = "{{ locked_thematic_code|default:'' }}";
@ -728,6 +755,6 @@
<script src="{% static 'common/base_script.js' %}"></script> <script src="{% static 'common/base_script.js' %}"></script>
<script src="{% static 'common/base_map.js' %}"></script> <script src="{% static 'common/base_map.js' %}"></script>
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script> <script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
<script src="{% static 'assets/unified_map_layers.js' %}?v=20260703c"></script> <script src="{% static 'assets/unified_map_layers.js' %}?v=20260726_pb"></script>
<script src="{% static 'assets/unified_index.js' %}?v=20260703c"></script> <script src="{% static 'assets/unified_index.js' %}?v=20260726_pb"></script>
{% endblock script %} {% endblock script %}

View file

@ -10,6 +10,9 @@ RUN_DXF_TESTS = os.getenv('RUN_DXF_TESTS', '0') == '1'
from common.models import Thematic, UserConfig, UserThematics from common.models import Thematic, UserConfig, UserThematics
from assets.models import NatureLocation, TrafficLightIntersection, TrafficLightRadarModel, TrafficLightRadar, AssetCategory from assets.models import NatureLocation, TrafficLightIntersection, TrafficLightRadarModel, TrafficLightRadar, AssetCategory
from assets.models.buildings import PublicBuilding, BuildingAsset, BuildingEquipment
class NatureLocationsGeoJSONTest(TestCase): class NatureLocationsGeoJSONTest(TestCase):
def setUp(self): def setUp(self):
@ -2376,8 +2379,152 @@ class TrafficLightPoleSignPanelsTest(TestCase):
model=radar_pole_model model=radar_pole_model
) )
# Verify sorted_signpanels returns an empty list for radar poles
self.assertEqual(list(radar_pole.sorted_signpanels), []) from assets.models.buildings import BuildingAsset
class PublicBuildingTestCase(TestCase):
def setUp(self):
# Complexe sportif (Localisation Racine)
self.complex = PublicBuilding.objects.create(
code="CMP-001",
name_fr="Complexe Sportif Municipal",
name_nl="Gemeentelijk Sportcomplex",
address="Rue du Sport 1, 1000 Bruxelles",
owner="Ville de Bruxelles",
usage_type="sports",
location_type="site",
building_status="in_service",
is_erp=True,
erp_category="1",
erp_type="X",
geom=MultiPolygon(Polygon(((0, 0), (0, 10), (10, 10), (10, 0), (0, 0)))),
)
# Bloc A (Localisation Enfant)
self.bloc_a = PublicBuilding.objects.create(
code="BLOC-A",
name_fr="Bloc A",
parent_location=self.complex,
location_type="building",
)
# Étage 0 (Localisation Enfant du Bloc A)
self.etage_0 = PublicBuilding.objects.create(
code="ETG-0",
name_fr="Étage 0",
parent_location=self.bloc_a,
location_type="level",
level_number=0,
)
def test_public_building_location_hierarchy(self):
self.assertEqual(self.complex.code, "CMP-001")
self.assertEqual(self.complex.location_type, "site")
self.assertEqual(self.bloc_a.parent_location, self.complex)
self.assertEqual(self.etage_0.parent_location, self.bloc_a)
self.assertIn(self.bloc_a, self.complex.get_child_locations())
def test_building_asset_tree_with_civil_engineering(self):
# Génie civil / Bâti A (Asset Racine)
civil_asset = BuildingAsset.objects.create(
building=self.complex,
location=self.bloc_a,
code="BATI-A",
name_fr="Bâti Bloc A",
asset_type="civil_engineering"
)
# Asset Enfant: Façade Nord
facade_asset = BuildingAsset.objects.create(
building=self.complex,
location=self.bloc_a,
parent_asset=civil_asset,
code="FAC-N",
name_fr="Façade Nord",
asset_type="facade"
)
# Asset Enfant: Chaudière
hvac_asset = BuildingAsset.objects.create(
building=self.complex,
location=self.etage_0,
parent_asset=civil_asset,
code="CVC-01",
name_fr="Chaudière",
asset_type="hvac"
)
self.assertEqual(civil_asset.asset_type, "civil_engineering")
self.assertEqual(facade_asset.parent_asset, civil_asset)
self.assertEqual(hvac_asset.parent_asset, civil_asset)
self.assertIn(facade_asset, civil_asset.get_child_assets())
self.assertIn(hvac_asset, civil_asset.get_child_assets())
class PublicBuildingEditViewTest(TestCase):
def setUp(self):
self.thematic = Thematic.objects.create(
code="publicbuildings",
name_fr="Bâtiments Publics",
name_nl="Openbare Gebouwen"
)
self.building = PublicBuilding.objects.create(
code="BAT-999",
name_fr="Bâtiment de Test",
owner="Commune Test"
)
# User without edit permissions
self.viewer_user = User.objects.create_user(username="viewer", password="password123")
self.viewer_config = UserConfig.objects.create(user=self.viewer_user)
UserThematics.objects.create(
user_config=self.viewer_config,
thematic=self.thematic,
can_view_assets=True,
can_edit_assets=False
)
# User with edit permissions
self.editor_user = User.objects.create_user(username="editor", password="password123")
self.editor_config = UserConfig.objects.create(user=self.editor_user)
UserThematics.objects.create(
user_config=self.editor_config,
thematic=self.thematic,
can_view_assets=True,
can_edit_assets=True
)
def test_publicbuilding_edit_permission_denied_for_viewer(self):
self.client.login(username="viewer", password="password123")
url = reverse('assets:publicbuilding_edit', kwargs={'pk': self.building.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
def test_publicbuilding_edit_allowed_for_editor(self):
self.client.login(username="editor", password="password123")
url = reverse('assets:publicbuilding_edit', kwargs={'pk': self.building.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "BAT-999")
def test_publicbuilding_edit_post_updates_building(self):
self.client.login(username="editor", password="password123")
url = reverse('assets:publicbuilding_edit', kwargs={'pk': self.building.pk})
post_data = {
'code': 'BAT-999',
'location_type': 'building',
'name_fr': 'Bâtiment Modifié',
'name_nl': 'Gewijzigd Gebouw',
'address': 'Rue du Test 123',
'owner': 'Nouveau Propriétaire',
'usage_type': 'administrative',
'building_status': 'in_service',
'is_erp': True,
}
response = self.client.post(url, post_data)
self.assertRedirects(response, reverse('assets:publicbuilding_detail', kwargs={'pk': self.building.pk}))
self.building.refresh_from_db()
self.assertEqual(self.building.name_fr, 'Bâtiment Modifié')
self.assertEqual(self.building.address, 'Rue du Test 123')
self.assertEqual(self.building.owner, 'Nouveau Propriétaire')

View file

@ -1,12 +1,17 @@
from django.urls import path from django.urls import path
from .views import core, locations, nature, publiclighting, trafficlights, artworks, structures, controlcenters, sign, roads, its, clean, asset_editing, history, location_editing, export, asset_permissions, water from .views import core, locations, nature, publiclighting, trafficlights, artworks, structures, controlcenters, sign, roads, its, clean, asset_editing, history, location_editing, export, asset_permissions, water, buildings, outdoor
app_name = "assets" app_name = "assets"
urlpatterns = [ urlpatterns = [
path("", core.unified_assets_index, name="index"), path("", core.unified_assets_index, name="index"),
path("buildings/", buildings.buildings_index, name="buildings_index"),
path("buildings/<int:pk>/", buildings.building_detail, name="publicbuilding_detail"),
path("buildings/<int:pk>/edit/", buildings.publicbuilding_edit, name="publicbuilding_edit"),
path("buildings/<str:asset_model>/<int:asset_id>/", buildings.publicbuildings_assets_detail, name="publicbuildings_assets_detail"),
path("export/", export.export_assets, name="export_assets"), path("export/", export.export_assets, name="export_assets"),
# ------------------------------------------------ # ------------------------------------------------
# Per-instance & per-type permission management # Per-instance & per-type permission management
# ------------------------------------------------ # ------------------------------------------------
@ -217,6 +222,17 @@ urlpatterns = [
path('water/assets/autocomplete/', water.water_assets_autocomplete, name='water_autocomplete_assets'), path('water/assets/autocomplete/', water.water_assets_autocomplete, name='water_autocomplete_assets'),
path('water/<str:asset_model>/<int:asset_id>/', water.water_assets_detail, name='water_assets_detail'), path('water/<str:asset_model>/<int:asset_id>/', water.water_assets_detail, name='water_assets_detail'),
# Public buildings GeoJSON URLs
path('publicbuildings/locations/geojson/', buildings.publicbuildings_locations_geojson, name='publicbuildings_locations_geojson'),
path('publicbuildings/assets/geojson/', buildings.publicbuildings_assets_geojson, name='publicbuildings_assets_geojson'),
# Outdoor facilities GeoJSON & Detail URLs
path('outdoor/locations/geojson/', outdoor.outdoor_locations_geojson, name='outdoor_locations_geojson'),
path('outdoor/locations/<int:pk>/', outdoor.outdoor_location_detail, name='outdoor_location_detail'),
path('outdoor/assets/geojson/', outdoor.outdoor_assets_geojson, name='outdoor_assets_geojson'),
path('outdoor/<str:asset_model>/<int:asset_id>/', outdoor.outdoor_assets_detail, name='outdoor_assets_detail'),
# DXF import URLs # DXF import URLs
path('trafficlights/intersections/<int:intersection_id>/import-dxf/', trafficlights.import_dxf, name='import_dxf'), path('trafficlights/intersections/<int:intersection_id>/import-dxf/', trafficlights.import_dxf, name='import_dxf'),
path('trafficlights/intersections/<int:intersection_id>/stage-dxf/', trafficlights.stage_dxf, name='stage_dxf'), path('trafficlights/intersections/<int:intersection_id>/stage-dxf/', trafficlights.stage_dxf, name='stage_dxf'),

View file

@ -0,0 +1,461 @@
import json
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Q, F
from django.http import JsonResponse, HttpResponseForbidden
from django.urls import reverse
from django.utils.translation import gettext as _, get_language
from django.contrib.gis.geos import GEOSGeometry
from common.models import UserThematics
from common.utils import check_thematic_access, get_regional_terms
from assets.permissions import can_edit_asset
from assets.forms import PublicBuildingForm, BuildingAssetForm
from assets.models.buildings import (
PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument,
USAGE_TYPE_CHOICES, BUILDING_STATUS_CHOICES, DPE_RATING_CHOICES, ERP_CATEGORY_CHOICES,
LOCATION_TYPE_CHOICES, BUILDING_ASSET_TYPE_CHOICES
)
def publicbuildings_locations_geojson(request):
"""
Vue GeoJSON renvoyant la collection des localisations de bâtiments publics pour la carte unifiée.
"""
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
statuses = data.get('asset_status', [])
location_types = data.get('location_type') or []
if isinstance(location_types, str):
location_types = [location_types]
usage_types = data.get('usage_type') or []
if isinstance(usage_types, str):
usage_types = [usage_types]
is_main_loc = data.get('is_main_location')
is_erp_val = data.get('is_erp')
filter_geom_raw = data.get("filter_geom")
if filter_geom_raw:
try:
filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326)
filter_geom.transform(3812)
except Exception as e:
return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400)
else:
filter_geom = None
search_q = (data.get('q') or '').strip()
else:
statuses = request.GET.getlist('asset_status')
location_types = request.GET.getlist('location_type', [])
usage_types = request.GET.getlist('usage_type', [])
is_main_loc = request.GET.get('is_main_location')
is_erp_val = request.GET.get('is_erp')
filter_geom = None
search_q = request.GET.get('q', '').strip()
qs = PublicBuilding.objects.all()
if location_types:
qs = qs.filter(location_type__in=location_types)
if usage_types:
qs = qs.filter(usage_type__in=usage_types)
if is_main_loc in ['true', '1', True]:
qs = qs.filter(is_main_location=True)
elif is_main_loc in ['false', '0', False]:
qs = qs.filter(is_main_location=False)
if is_erp_val in ['true', '1', True]:
qs = qs.filter(is_erp=True)
elif is_erp_val in ['false', '0', False]:
qs = qs.filter(is_erp=False)
if statuses:
qs = qs.filter(building_status__in=statuses)
if filter_geom:
qs = qs.filter(geom__intersects=filter_geom)
if search_q:
qs = qs.filter(
Q(code__icontains=search_q) |
Q(name_fr__icontains=search_q) |
Q(name_nl__icontains=search_q) |
Q(address__icontains=search_q) |
Q(owner__icontains=search_q)
)
features = []
from assets.views.core import get_element_type_label
for b in qs:
geometry = None
if b.geojson:
try:
geometry = json.loads(b.geojson)
except Exception:
pass
if not geometry and b.geom:
geom_4326 = b.geom.clone()
geom_4326.transform(4326)
centroid = geom_4326.centroid
geometry = {"type": "Point", "coordinates": [centroid.x, centroid.y]}
elif not geometry and b.lon is not None and b.lat is not None:
geometry = {"type": "Point", "coordinates": [b.lon, b.lat]}
detail_url = reverse('assets:publicbuilding_detail', kwargs={'pk': b.pk})
features.append({
"type": "Feature",
"id": b.id,
"geometry": geometry,
"properties": {
"code": b.code,
"name": b.get_name(),
"location_type": b.location_type,
"location_type_display": b.get_location_type_display(),
"status": b.building_status,
"building_status": b.building_status,
"building_status_display": b.get_building_status_display(),
"usage_type": b.usage_type,
"usage_type_display": b.get_usage_type_display(),
"owner": b.owner or '',
"is_erp": b.is_erp,
"erp_category": b.erp_category or '',
"dpe_rating": b.dpe_rating or '',
"lon": b.lon,
"lat": b.lat,
"color": "#2B579A",
"display_color": "#2B579A",
"asset_type": "publicbuilding",
"detail_url": detail_url,
"element_type": get_element_type_label("publicbuilding"),
"thematic": "publicbuildings",
"is_location": True,
}
})
return JsonResponse({"type": "FeatureCollection", "features": features})
def publicbuildings_assets_geojson(request):
"""
Vue GeoJSON renvoyant la collection des assets physiques de bâtiments (Génie civil, Façades, CVC, Ascenseurs...) pour la carte unifiée.
"""
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
statuses = data.get('asset_status', [])
categories = data.get('category', []) or data.get('categories', [])
if isinstance(categories, (str, int)):
categories = [categories]
asset_types = data.get('asset_type') or []
if isinstance(asset_types, str):
asset_types = [asset_types]
search_q = (data.get('q') or '').strip()
else:
statuses = request.GET.getlist('asset_status')
categories = request.GET.getlist('category')
asset_types = request.GET.getlist('asset_type', [])
search_q = request.GET.get('q', '').strip()
qs = BuildingAsset.objects.select_related('building', 'location', 'parent_asset', 'category').all()
if categories:
qs = qs.filter(category_id__in=categories)
if asset_types:
qs = qs.filter(asset_type__in=asset_types)
if statuses:
qs = qs.filter(status__in=statuses)
if search_q:
qs = qs.filter(
Q(code__icontains=search_q) |
Q(name_fr__icontains=search_q) |
Q(name_nl__icontains=search_q)
)
features = []
from assets.views.core import get_element_type_label
for a in qs:
geometry = None
if a.geojson:
try:
geometry = json.loads(a.geojson)
except Exception:
pass
if not geometry and a.geom:
geom_4326 = a.geom.clone()
geom_4326.transform(4326)
geometry = {"type": "Point", "coordinates": [geom_4326.x, geom_4326.y]}
elif not geometry and a.lon is not None and a.lat is not None:
geometry = {"type": "Point", "coordinates": [a.lon, a.lat]}
elif not geometry and a.building and a.building.lon is not None and a.building.lat is not None:
geometry = {"type": "Point", "coordinates": [a.building.lon, a.building.lat]}
detail_url = reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': a.pk})
features.append({
"type": "Feature",
"id": a.id,
"geometry": geometry,
"properties": {
"code": a.code,
"name": a.get_name(),
"asset_type": a.asset_type,
"asset_type_display": a.get_asset_type_display(),
"category_id": a.category_id,
"category_name": a.category.get_name() if a.category else '',
"status": a.status,
"status_display": a.get_status_display(),
"building_code": a.building.code if a.building else '',
"building_name": a.building.get_name() if a.building else '',
"parent_asset_id": a.parent_asset_id,
"color": "#17a2b8",
"display_color": "#17a2b8",
"detail_url": detail_url,
"element_type": get_element_type_label("buildingasset"),
"thematic": "publicbuildings",
"is_location": False,
}
})
return JsonResponse({"type": "FeatureCollection", "features": features})
@login_required
def buildings_index(request):
"""
Vue principale (Index / Liste) des Localisations Bâtiments Publics.
"""
lang = get_language()
buildings = PublicBuilding.objects.select_related('parent_location', 'manager').all()
# Search filter
search_query = request.GET.get('q', '').strip()
if search_query:
buildings = buildings.filter(
Q(code__icontains=search_query) |
Q(name_fr__icontains=search_query) |
Q(name_nl__icontains=search_query) |
Q(address__icontains=search_query) |
Q(owner__icontains=search_query)
)
# Usage filter
usage_filter = request.GET.get('usage_type')
if usage_filter:
buildings = buildings.filter(usage_type=usage_filter)
# Status filter
status_filter = request.GET.get('building_status')
if status_filter:
buildings = buildings.filter(building_status=status_filter)
# Location type filter
location_type_filter = request.GET.get('location_type')
if location_type_filter:
buildings = buildings.filter(location_type=location_type_filter)
# ERP filter
erp_filter = request.GET.get('is_erp')
if erp_filter in ['true', '1']:
buildings = buildings.filter(is_erp=True)
elif erp_filter in ['false', '0']:
buildings = buildings.filter(is_erp=False)
# DPE rating filter
dpe_filter = request.GET.get('dpe_rating')
if dpe_filter:
buildings = buildings.filter(dpe_rating=dpe_filter)
building_list = []
for b in buildings:
b.can_edit = can_edit_asset(request.user, b)
building_list.append(b)
context = {
'buildings': building_list,
'search_query': search_query,
'usage_type_choices': USAGE_TYPE_CHOICES,
'building_status_choices': BUILDING_STATUS_CHOICES,
'location_type_choices': LOCATION_TYPE_CHOICES,
'dpe_rating_choices': DPE_RATING_CHOICES,
'selected_usage': usage_filter,
'selected_status': status_filter,
'selected_location_type': location_type_filter,
'selected_erp': erp_filter,
'selected_dpe': dpe_filter,
'active_tab': 'buildings',
}
return render(request, 'assets/buildings/publicbuilding_list.html', context)
def build_asset_node(asset):
return {
'asset': asset,
'children': [build_asset_node(c) for c in asset.child_assets.all()]
}
def build_location_node(loc):
if loc.parent_location is None:
loc_assets = loc.location_assets.filter(parent_asset__isnull=True) | loc.all_building_assets.filter(location__isnull=True, parent_asset__isnull=True)
loc_assets = loc_assets.distinct().prefetch_related('child_assets')
else:
loc_assets = loc.location_assets.filter(parent_asset__isnull=True).prefetch_related('child_assets')
return {
'loc': loc,
'children': [build_location_node(c) for c in loc.child_locations.all().prefetch_related('child_locations', 'location_assets')],
'assets': [build_asset_node(a) for a in loc_assets],
}
@login_required
def building_detail(request, pk):
"""
Vue détaillée d'une Localisation Bâtiment Public avec son arborescence unifiée de localisations et d'assets physiques.
"""
from django.contrib.contenttypes.models import ContentType
from interventions.models import Intervention
from interventions.permissions import filter_viewable_interventions_for_user
building = get_object_or_404(PublicBuilding.objects.select_related('parent_location', 'manager'), pk=pk)
can_edit = can_edit_asset(request.user, building)
regional_terms = get_regional_terms(getattr(building, '_regional_standard', 'BE'))
# Arborescence unifiée (Localisation courante + sous-localisations + assets rattachés)
location_tree = build_location_node(building)
parent_location = building.parent_location
documents = building.documents.all()
content_type = ContentType.objects.get_for_model(PublicBuilding)
# Récupération des interventions associées au bâtiment
base_interventions = filter_viewable_interventions_for_user(request.user)
interventions = base_interventions.filter(
interventionasset__object_id=building.id,
interventionasset__content_type=content_type
).distinct().order_by('-creation_time')
# GeoJSON pour la carte de localisation
building_geojson = None
center_lon, center_lat = None, None
if building.geom:
try:
geom_4326 = building.geom.clone()
geom_4326.transform(4326)
building_geojson = json.loads(geom_4326.json)
centroid = geom_4326.centroid
center_lon, center_lat = centroid.x, centroid.y
except Exception:
pass
elif building.lon is not None and building.lat is not None:
center_lon, center_lat = building.lon, building.lat
context = {
'building': building,
'can_edit': can_edit,
'regional_terms': regional_terms,
'location_tree': location_tree,
'parent_location': parent_location,
'equipments': building.get_all_assets(),
'documents': documents,
'interventions': interventions,
'content_type': content_type,
'building_geojson_json': json.dumps(building_geojson) if building_geojson else 'null',
'center_lon': center_lon,
'center_lat': center_lat,
}
return render(request, 'assets/buildings/publicbuilding_detail.html', context)
@login_required
def publicbuilding_edit(request, pk):
"""
Vue d'édition d'une Localisation Bâtiment Public réservée aux utilisateurs autorisés.
"""
building = get_object_or_404(PublicBuilding, pk=pk)
if not can_edit_asset(request.user, building):
return HttpResponseForbidden(_("Vous n'avez pas la permission de modifier ce bâtiment."))
if request.method == 'POST':
form = PublicBuildingForm(request.POST, instance=building)
if form.is_valid():
saved_building = form.save()
messages.success(request, _("La localisation '%s' a été mise à jour avec succès.") % saved_building.code)
return redirect('assets:publicbuilding_detail', pk=saved_building.pk)
else:
form = PublicBuildingForm(instance=building)
regional_terms = get_regional_terms(getattr(building, '_regional_standard', 'BE'))
center_lon, center_lat = None, None
if building.lon is not None and building.lat is not None:
center_lon, center_lat = building.lon, building.lat
elif building.geom:
try:
geom_4326 = building.geom.clone()
geom_4326.transform(4326)
centroid = geom_4326.centroid
center_lon, center_lat = centroid.x, centroid.y
except Exception:
pass
context = {
'building': building,
'form': form,
'regional_terms': regional_terms,
'center_lon': center_lon,
'center_lat': center_lat,
}
return render(request, 'assets/buildings/publicbuilding_form.html', context)
@login_required
def publicbuildings_assets_detail(request, asset_model, asset_id):
"""
Vue générique pour le détail d'un asset/équipement physique d'un bâtiment public.
"""
from django.contrib.contenttypes.models import ContentType
from assets.views.core import get_asset_edit_context
from interventions.models import Intervention
from interventions.permissions import filter_viewable_interventions_for_user
content_type = get_object_or_404(ContentType, app_label='assets', model=asset_model.lower())
model_class = content_type.model_class()
asset = get_object_or_404(model_class, pk=asset_id)
base_interventions = filter_viewable_interventions_for_user(request.user)
interventions = base_interventions.filter(
interventionasset__object_id=asset.id,
interventionasset__content_type=content_type
).distinct().order_by('-creation_time')
context = {
'asset': asset,
'content_type': content_type,
'interventions': interventions,
}
context.update(get_asset_edit_context(request, asset, content_type))
return render(request, "assets/detail_generic.html", context)

View file

@ -9,6 +9,7 @@ from django.template.loader import select_template
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.contrib.auth.decorators import login_required
from common.models import UserConfig, Thematic from common.models import UserConfig, Thematic
from common.utils import check_thematic_access from common.utils import check_thematic_access
from assets.models import AssetCategory, ASSET_STATUS_CHOICES from assets.models import AssetCategory, ASSET_STATUS_CHOICES
@ -358,64 +359,66 @@ ELEMENT_TYPE_LABELS = {
'publiclightingstreet': 'Rue EP', 'publiclightingstreet': 'Rue EP',
'trafficlightintersection': 'Carrefour', 'trafficlightintersection': 'Carrefour',
'naturelocation': 'Section verte', 'naturelocation': 'Section verte',
'naturerwiz': 'Zone RWIZ', # --- Voirie ---
'structurelocation': "Ouvrage d'art", 'street': _('Rue / Voie'),
'controlcenterlocation': 'Centre de contrôle', 'roadway': _('Chaussée'),
'itslocation': 'Zone ITS', 'sidewalk': _('Trottoir'),
'cleanlocation': 'Zone propreté', 'roadfurniture': _('Mobilier voirie'),
'artworkstreet': 'Voirie œuvres', 'roaddrain': _('Drain'),
# --- Voiries --- 'roadconcreteblockset': _('Blocs béton'),
'roadway': 'Chaussée',
'roadsidewalk': 'Trottoir',
'roadfurniture': 'Mobilier voirie',
'roaddrain': 'Drain',
'roadconcreteblockset': 'Blocs béton',
# --- Éclairage public --- # --- Éclairage public ---
'publiclightingpole': 'Candélabre', 'publiclightingpole': _('Candélabre'),
'publiclightinglightfixture': 'Luminaire', 'publiclightinglightfixture': _('Luminaire'),
'publiclightingelectricalcabinet': 'Armoire électrique EP', 'publiclightingelectricalcabinet': _('Armoire électrique EP'),
# --- Signalisation --- # --- Signalisation ---
'signpole': 'Poteau signalisation', 'signpole': _('Poteau signalisation'),
'signpanel': 'Panneau', 'signpanel': _('Panneau'),
'signparkingspot': 'Place de parking', 'signparkingspot': _('Place de parking'),
# --- Tricolores --- # --- Tricolores ---
'trafficlightpole': 'Poteau tricolore', 'trafficlightpole': _('Poteau tricolore'),
'trafficlightcable': 'Câble TL', 'trafficlightcable': _('Câble TL'),
'trafficlightlantern': 'Lanterne TL', 'trafficlightlantern': _('Lanterne TL'),
'trafficlightcontroller': 'Contrôleur TL', 'trafficlightcontroller': _('Contrôleur TL'),
'trafficlightcontrollerhardware': 'Hardware contrôleur', 'trafficlightcontrollerhardware': _('Hardware contrôleur'),
'trafficlightdetector': 'Détecteur TL', 'trafficlightdetector': _('Détecteur TL'),
'trafficlightnetworkhardware': 'Hardware réseau TL', 'trafficlightnetworkhardware': _('Hardware réseau TL'),
'trafficlightvoltagehardware': 'Hardware tension TL', 'trafficlightvoltagehardware': _('Hardware tension TL'),
'trafficlightelectricalcabinet': 'Armoire électrique TL', 'trafficlightelectricalcabinet': _('Armoire électrique TL'),
'trafficlightelectricalcabinetcontent': 'Contenu armoire TL', 'trafficlightelectricalcabinetcontent': _('Contenu armoire TL'),
'trafficlightaccessory': 'Accessoire TL', 'trafficlightaccessory': _('Accessoire TL'),
# --- Nature --- # --- Nature ---
'naturegreensurface': 'Surface verte', 'naturegreensurface': _('Surface verte'),
'naturetree': 'Arbre', 'naturetree': _('Arbre'),
'natureshrub': 'Arbuste', 'natureshrub': _('Arbuste'),
'naturebench': 'Banc', 'naturebench': _('Banc'),
'naturerwiasset': 'Ouvrage RWI', 'naturerwiasset': _('Ouvrage RWI'),
# --- Ouvrages d'art --- # --- Ouvrages d'art ---
'structuregeoasset': "Composant d'ouvrage", 'structuregeoasset': _("Composant d'ouvrage"),
# --- Œuvres d'art --- # --- Œuvres d'art ---
'artworkmonument': 'Monument', 'artworkmonument': _('Monument'),
'artworkfountain': 'Fontaine', 'artworkfountain': _('Fontaine'),
# --- Centres de contrôle --- # --- Centres de contrôle ---
'controlcentergeoasset': 'Équipement CdC', 'controlcentergeoasset': _('Équipement CdC'),
# --- ITS --- # --- ITS ---
'itsgeoasset': 'Équipement ITS', 'itsgeoasset': _('Équipement ITS'),
# --- Propreté --- # --- Propreté ---
'cleanlitterbin': 'Corbeille', 'cleanlitterbin': _('Corbeille'),
'cleanglasscontainer': 'Conteneur verre', 'cleanglasscontainer': _('Conteneur verre'),
# --- Bâtiments publics ---
'publicbuilding': _('Bâtiment public'),
'buildingequipment': _('Équipement bâtiment'),
# --- Espaces plein air ---
'outdoorlocation': _('Espace public plein air'),
'outdoorasset': _('Équipement plein air'),
} }
def get_element_type_label(model_name): def get_element_type_label(model_name):
"""Returns the French display label for an asset/location model name (lowercase).""" """Returns the translated display label for an asset/location model name (lowercase)."""
if not model_name: if not model_name:
return '' return ''
return ELEMENT_TYPE_LABELS.get((model_name or '').lower(), model_name or '') label = ELEMENT_TYPE_LABELS.get((model_name or '').lower(), model_name or '')
return str(_(label)) if label else ''
def build_thematic_config(thematics): def build_thematic_config(thematics):
@ -468,6 +471,22 @@ def build_thematic_config(thematics):
'locations_geojson_url': reverse('assets:clean_locations_geojson'), 'locations_geojson_url': reverse('assets:clean_locations_geojson'),
'assets_geojson_url': reverse('assets:clean_assets_geojson'), 'assets_geojson_url': reverse('assets:clean_assets_geojson'),
}, },
'publicbuildings': {
'locations_geojson_url': reverse('assets:publicbuildings_locations_geojson'),
'assets_geojson_url': reverse('assets:publicbuildings_assets_geojson'),
},
'buildings': {
'locations_geojson_url': reverse('assets:publicbuildings_locations_geojson'),
'assets_geojson_url': reverse('assets:publicbuildings_assets_geojson'),
},
'outdoorfacilities': {
'locations_geojson_url': reverse('assets:outdoor_locations_geojson'),
'assets_geojson_url': reverse('assets:outdoor_assets_geojson'),
},
'outdoor': {
'locations_geojson_url': reverse('assets:outdoor_locations_geojson'),
'assets_geojson_url': reverse('assets:outdoor_assets_geojson'),
},
} }
result = {} result = {}
for thematic in thematics: for thematic in thematics:
@ -702,6 +721,80 @@ def build_conditional_filter_config(accessible_thematics, user=None):
'options': quality_level_options, 'options': quality_level_options,
}) })
# Public buildings specific choices
from assets.models.buildings import USAGE_TYPE_CHOICES
building_usage_options = [
{'value': str(key), 'label': str(value)}
for key, value in USAGE_TYPE_CHOICES
]
# Public buildings specific filters
for thematic_code in ['publicbuildings', 'buildings']:
if thematic_code in accessible_codes:
config[thematic_code] = [
{
'key': 'usage_type',
'label': _("Type d'usage (Localisation)"),
'type': 'multi',
'options': building_usage_options,
},
{
'key': 'is_main_location',
'label': _('Localisation principale'),
'type': 'single',
'options': [
{'value': 'true', 'label': _('Oui')},
{'value': 'false', 'label': _('Non')},
],
},
{
'key': 'is_erp',
'label': _('Accueil ERP / PMR'),
'type': 'single',
'options': [
{'value': 'true', 'label': _('Oui')},
{'value': 'false', 'label': _('Non')},
],
},
]
# Outdoor facilities specific choices
from assets.models.outdoor import SITE_TYPE_CHOICES, NORM_CHOICES
outdoor_site_type_options = [
{'value': str(key), 'label': str(value)}
for key, value in SITE_TYPE_CHOICES
]
outdoor_norm_options = [
{'value': str(key), 'label': str(value)}
for key, value in NORM_CHOICES
]
for thematic_code in ['outdoorfacilities', 'outdoor']:
if thematic_code in accessible_codes:
config[thematic_code] = [
{
'key': 'site_type',
'label': _("Type de site (Localisation)"),
'type': 'multi',
'options': outdoor_site_type_options,
},
{
'key': 'norm',
'label': _("Norme de sécurité (Asset)"),
'type': 'multi',
'options': outdoor_norm_options,
},
{
'key': 'pmr_accessible',
'label': _('Accessible PMR'),
'type': 'single',
'options': [
{'value': 'true', 'label': _('Oui')},
{'value': 'false', 'label': _('Non')},
],
},
]
# Sign assets specific filter # Sign assets specific filter
if 'sign' in accessible_codes: if 'sign' in accessible_codes:
config.setdefault('sign', []).append({ config.setdefault('sign', []).append({
@ -769,6 +862,41 @@ def build_unified_add_modal_config(accessible_thematics, user=None):
} }
SPECIFIC_TABLES_REGISTRY = [
{
'thematic_code': 'publicbuildings',
'title': _("Bâtiments publics"),
'description': _("Gestion du patrimoine bâti & équipements"),
'icon': 'bi-building',
'url_name': 'assets:buildings_index',
},
]
def get_user_specific_tables(user, accessible_thematics):
"""
Retourne la liste générique des répertoires/tableaux spécifiques accessibles par l'utilisateur.
"""
from django.urls import reverse
accessible_codes = {t.code for t in accessible_thematics}
specific_tables = []
for item in SPECIFIC_TABLES_REGISTRY:
if user.is_superuser or item['thematic_code'] in accessible_codes:
try:
url = reverse(item['url_name'])
specific_tables.append({
'title': item['title'],
'description': item['description'],
'icon': item['icon'],
'url': url,
})
except Exception:
pass
return specific_tables
@login_required
def unified_assets_index(request): def unified_assets_index(request):
"""Unified asset/location index page combining all accessible thematics.""" """Unified asset/location index page combining all accessible thematics."""
from django.utils.translation import get_language from django.utils.translation import get_language
@ -776,16 +904,22 @@ def unified_assets_index(request):
from interventions.views.drafts import InterventionDraftManager from interventions.views.drafts import InterventionDraftManager
lang = get_language() or 'fr' lang = get_language() or 'fr'
user_config, _ = UserConfig.objects.get_or_create(user=request.user)
# Thématiques accessibles à l'utilisateur # Thématiques accessibles à l'utilisateur
if getattr(request.user, 'is_superuser', False):
accessible_thematics = list(Thematic.objects.order_by(f'{"name_nl" if lang == "nl" else "name_fr"}'))
else:
thematic_qs = ( thematic_qs = (
UserThematics.objects UserThematics.objects
.filter(user_config=request.user.config, can_view_assets=True) .filter(user_config=user_config, can_view_assets=True)
.select_related('thematic') .select_related('thematic')
.order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}')
) )
accessible_thematics = [tc.thematic for tc in thematic_qs] accessible_thematics = [tc.thematic for tc in thematic_qs]
# Mode intervention préventive # Mode intervention préventive
preventive_draft_id = request.GET.get('preventive_draft_id') preventive_draft_id = request.GET.get('preventive_draft_id')
locked_thematic_code = request.GET.get('thematic_code', '') locked_thematic_code = request.GET.get('thematic_code', '')
@ -803,10 +937,24 @@ def unified_assets_index(request):
else: else:
selected_locations_ids = draft_data.get('locations', []) or [] selected_locations_ids = draft_data.get('locations', []) or []
# Thématiques présélectionnées (GET ou fixées par le mode intervention) # Thématiques présélectionnées (GET, intervention, thématique par défaut ou toutes les thématiques accessibles)
selected_thematics = request.GET.getlist('thematic') or ( has_url_thematics = 'thematic' in request.GET
[locked_thematic_code] if locked_thematic_code else [] if has_url_thematics:
) selected_thematics = request.GET.getlist('thematic')
elif locked_thematic_code:
selected_thematics = [locked_thematic_code]
elif user_config and user_config.assets_index_preferences and user_config.assets_index_preferences.get('thematics'):
saved_thematics = user_config.assets_index_preferences.get('thematics')
accessible_codes = {t.code for t in accessible_thematics}
selected_thematics = [code for code in saved_thematics if code in accessible_codes]
if not selected_thematics and accessible_thematics:
selected_thematics = [t.code for t in accessible_thematics]
elif user_config and user_config.default_thematic and user_config.default_thematic in accessible_thematics:
selected_thematics = [user_config.default_thematic.code]
elif accessible_thematics:
selected_thematics = [t.code for t in accessible_thematics]
else:
selected_thematics = []
selected_categories = request.GET.getlist('category') selected_categories = request.GET.getlist('category')
# Config JS par thématique (URLs GeoJSON, nom, icône) # Config JS par thématique (URLs GeoJSON, nom, icône)
@ -830,13 +978,17 @@ def unified_assets_index(request):
if locked_thematic_code else '' if locked_thematic_code else ''
) )
user_roles = request.user.config.roles.values_list('name', flat=True) if hasattr(request.user, 'config') else [] user_roles = user_config.roles.values_list('name', flat=True) if user_config else []
is_admin = 'admin' in user_roles or request.user.is_superuser is_admin = 'admin' in user_roles or request.user.is_superuser
has_bulk_edit_status_permission = is_admin or UserThematics.objects.filter(user_config=request.user.config, can_edit_assets=True).exists() has_bulk_edit_status_permission = is_admin or UserThematics.objects.filter(user_config=user_config, can_edit_assets=True).exists()
has_bulk_edit_validation_status_permission = is_admin or UserThematics.objects.filter(user_config=request.user.config, can_validate_assets=True).exists() has_bulk_edit_validation_status_permission = is_admin or UserThematics.objects.filter(user_config=user_config, can_validate_assets=True).exists()
specific_tables = get_user_specific_tables(request.user, accessible_thematics)
context = { context = {
'accessible_thematics': accessible_thematics, 'accessible_thematics': accessible_thematics,
'accessible_thematic_codes': [t.code for t in accessible_thematics],
'specific_tables': specific_tables,
'has_bulk_edit_status_permission': has_bulk_edit_status_permission, 'has_bulk_edit_status_permission': has_bulk_edit_status_permission,
'has_bulk_edit_validation_status_permission': has_bulk_edit_validation_status_permission, 'has_bulk_edit_validation_status_permission': has_bulk_edit_validation_status_permission,
'thematic_config_json': json.dumps(thematic_config), 'thematic_config_json': json.dumps(thematic_config),
@ -846,6 +998,7 @@ def unified_assets_index(request):
'asset_validation_status_choices': list(ASSET_VALIDATION_STATUS_CHOICES.items()), 'asset_validation_status_choices': list(ASSET_VALIDATION_STATUS_CHOICES.items()),
'add_modal_config_json': json.dumps(add_modal_config), 'add_modal_config_json': json.dumps(add_modal_config),
'selected_thematics': selected_thematics, 'selected_thematics': selected_thematics,
'has_url_thematics': has_url_thematics,
'selected_categories_json': json.dumps(selected_categories), 'selected_categories_json': json.dumps(selected_categories),
'preventive_draft_id': preventive_draft_id, 'preventive_draft_id': preventive_draft_id,
'locked_thematic_code': locked_thematic_code, 'locked_thematic_code': locked_thematic_code,
@ -853,7 +1006,7 @@ def unified_assets_index(request):
'selected_assets_json': json.dumps(selected_assets), 'selected_assets_json': json.dumps(selected_assets),
'selected_locations_ids': selected_locations_ids, 'selected_locations_ids': selected_locations_ids,
'link_assets_url': link_assets_url, 'link_assets_url': link_assets_url,
'assets_index_prefs_json': json.dumps(request.user.config.assets_index_preferences or {}), 'assets_index_prefs_json': json.dumps(user_config.assets_index_preferences or {}),
'default_thematic_code': (request.user.config.default_thematic.code if request.user.config.default_thematic_id else ''), 'default_thematic_code': (user_config.default_thematic.code if user_config.default_thematic_id else ''),
} }
return render(request, 'assets/unified_index.html', context) return render(request, 'assets/unified_index.html', context)

View file

@ -197,8 +197,8 @@ def its_locations_geojson(request):
# Ne garder que les locations dont le centre de contrôle a un contrat actif lié à ces contrats # Ne garder que les locations dont le centre de contrôle a un contrat actif lié à ces contrats
its_locations = its_locations.filter( its_locations = its_locations.filter(
its_contracts__status='active', # ITSContract.status its_locations_contracts__status='active', # ITSLocationContract.status
its_contracts__contract_id__in=accessible_contract_ids # FK contract its_locations_contracts__contract_id__in=accessible_contract_ids # FK contract
).distinct() ).distinct()
else: else:
# Non authentifié : ne rien exposer (option sûre) # Non authentifié : ne rien exposer (option sûre)

View file

@ -0,0 +1,349 @@
import json
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import JsonResponse, HttpResponseForbidden
from django.urls import reverse
from django.utils.translation import gettext as _, get_language
from django.contrib.gis.geos import GEOSGeometry
from assets.models.outdoor import (
OutdoorLocation, OutdoorAsset, OutdoorLocationDocument,
SITE_TYPE_CHOICES, OUTDOOR_ASSET_TYPE_CHOICES, NORM_CHOICES
)
from assets.permissions import can_edit_asset
def outdoor_locations_geojson(request):
"""
Vue GeoJSON renvoyant la collection des localisations d'espaces publics d'extérieur pour la carte unifiée.
"""
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
site_types = data.get('site_type') or []
if isinstance(site_types, str):
site_types = [site_types]
is_main_loc = data.get('is_main_location')
pmr = data.get('pmr_accessible')
water = data.get('has_water_point')
power = data.get('has_power_supply')
fenced = data.get('is_fenced')
filter_geom_raw = data.get("filter_geom")
if filter_geom_raw:
try:
filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326)
filter_geom.transform(3812)
except Exception as e:
return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400)
else:
filter_geom = None
search_q = (data.get('q') or '').strip()
else:
site_types = request.GET.getlist('site_type', [])
is_main_loc = request.GET.get('is_main_location')
pmr = request.GET.get('pmr_accessible')
water = request.GET.get('has_water_point')
power = request.GET.get('has_power_supply')
fenced = request.GET.get('is_fenced')
filter_geom = None
search_q = request.GET.get('q', '').strip()
qs = OutdoorLocation.objects.all()
if site_types:
qs = qs.filter(site_type__in=site_types)
if is_main_loc in ['true', '1', True]:
qs = qs.filter(is_main_location=True)
elif is_main_loc in ['false', '0', False]:
qs = qs.filter(is_main_location=False)
if pmr in ['true', '1', True]:
qs = qs.filter(pmr_accessible=True)
if water in ['true', '1', True]:
qs = qs.filter(has_water_point=True)
if power in ['true', '1', True]:
qs = qs.filter(has_power_supply=True)
if fenced in ['true', '1', True]:
qs = qs.filter(is_fenced=True)
if filter_geom:
qs = qs.filter(geom__intersects=filter_geom)
if search_q:
qs = qs.filter(
Q(code__icontains=search_q) |
Q(name_fr__icontains=search_q) |
Q(name_nl__icontains=search_q) |
Q(address__icontains=search_q) |
Q(owner__icontains=search_q)
)
features = []
from assets.views.core import get_element_type_label
for loc in qs:
geometry = None
if loc.geojson:
try:
geometry = json.loads(loc.geojson)
except Exception:
pass
if not geometry and loc.geom:
geom_4326 = loc.geom.clone()
geom_4326.transform(4326)
centroid = geom_4326.centroid
geometry = {"type": "Point", "coordinates": [centroid.x, centroid.y]}
elif not geometry and loc.lon is not None and loc.lat is not None:
geometry = {"type": "Point", "coordinates": [loc.lon, loc.lat]}
detail_url = reverse('assets:outdoor_location_detail', kwargs={'pk': loc.pk})
features.append({
"type": "Feature",
"id": loc.id,
"geometry": geometry,
"properties": {
"code": loc.code,
"name": loc.get_name(),
"site_type": loc.site_type,
"site_type_display": loc.get_site_type_display(),
"status": "active",
"owner": loc.owner or '',
"pmr_accessible": loc.pmr_accessible,
"has_water_point": loc.has_water_point,
"has_power_supply": loc.has_power_supply,
"is_fenced": loc.is_fenced,
"lon": loc.lon,
"lat": loc.lat,
"color": "#198754",
"display_color": "#198754",
"asset_type": "outdoorlocation",
"detail_url": detail_url,
"element_type": get_element_type_label("outdoorlocation"),
"thematic": "outdoorfacilities",
"is_location": True,
}
})
return JsonResponse({"type": "FeatureCollection", "features": features})
def outdoor_assets_geojson(request):
"""
Vue GeoJSON renvoyant la collection des assets d'extérieur (structures de jeu, fitness, skate modules...) pour la carte unifiée.
"""
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
statuses = data.get('asset_status', [])
categories = data.get('category', []) or data.get('categories', [])
if isinstance(categories, (str, int)):
categories = [categories]
asset_types = data.get('asset_type') or []
if isinstance(asset_types, str):
asset_types = [asset_types]
norms = data.get('norm') or []
if isinstance(norms, str):
norms = [norms]
search_q = (data.get('q') or '').strip()
else:
statuses = request.GET.getlist('asset_status')
categories = request.GET.getlist('category')
asset_types = request.GET.getlist('asset_type', [])
norms = request.GET.getlist('norm', [])
search_q = request.GET.get('q', '').strip()
qs = OutdoorAsset.objects.select_related('site', 'location', 'parent_asset', 'category').all()
if categories:
qs = qs.filter(category_id__in=categories)
if asset_types:
qs = qs.filter(asset_type__in=asset_types)
if norms:
qs = qs.filter(norm__in=norms)
if statuses:
qs = qs.filter(status__in=statuses)
if search_q:
qs = qs.filter(
Q(code__icontains=search_q) |
Q(name_fr__icontains=search_q) |
Q(name_nl__icontains=search_q)
)
features = []
from assets.views.core import get_element_type_label
for a in qs:
geometry = None
if a.geojson:
try:
geometry = json.loads(a.geojson)
except Exception:
pass
if not geometry and a.geom:
geom_4326 = a.geom.clone()
geom_4326.transform(4326)
geometry = {"type": "Point", "coordinates": [geom_4326.x, geom_4326.y]}
elif not geometry and a.lon is not None and a.lat is not None:
geometry = {"type": "Point", "coordinates": [a.lon, a.lat]}
elif not geometry and a.location and a.location.lon is not None and a.location.lat is not None:
geometry = {"type": "Point", "coordinates": [a.location.lon, a.location.lat]}
elif not geometry and a.site and a.site.lon is not None and a.site.lat is not None:
geometry = {"type": "Point", "coordinates": [a.site.lon, a.site.lat]}
detail_url = reverse('assets:outdoor_assets_detail', kwargs={'asset_model': 'outdoorasset', 'asset_id': a.pk})
features.append({
"type": "Feature",
"id": a.id,
"geometry": geometry,
"properties": {
"code": a.code,
"name": a.get_name(),
"asset_type": a.asset_type,
"asset_type_display": a.get_asset_type_display(),
"category_id": a.category_id,
"category_name": a.category.get_name() if a.category else '',
"status": a.status,
"status_display": a.get_status_display(),
"obsolescence": a.obsolescence or '',
"obsolescence_display": a.get_obsolescence_display() if hasattr(a, 'get_obsolescence_display') else '',
"norm": a.norm or '',
"norm_display": a.get_norm_display() if a.norm else '',
"site_code": a.site.code if a.site else '',
"site_name": a.site.get_name() if a.site else '',
"location_code": a.location.code if a.location else '',
"location_name": a.location.get_name() if a.location else '',
"lon": a.lon,
"lat": a.lat,
"color": "#198754",
"display_color": "#198754",
"detail_url": detail_url,
"element_type": get_element_type_label("outdoorasset"),
"thematic": "outdoorfacilities",
"is_location": False,
}
})
return JsonResponse({"type": "FeatureCollection", "features": features})
def build_outdoor_asset_node(asset):
return {
'asset': asset,
'children': [build_outdoor_asset_node(c) for c in asset.child_assets.all()]
}
def build_outdoor_location_node(loc):
if loc.parent_location is None:
loc_assets = loc.location_assets.filter(parent_asset__isnull=True) | loc.all_outdoor_assets.filter(location__isnull=True, parent_asset__isnull=True)
loc_assets = loc_assets.distinct().prefetch_related('child_assets')
else:
loc_assets = loc.location_assets.filter(parent_asset__isnull=True).prefetch_related('child_assets')
return {
'loc': loc,
'children': [build_outdoor_location_node(c) for c in loc.child_locations.all().prefetch_related('child_locations', 'location_assets')],
'assets': [build_outdoor_asset_node(a) for a in loc_assets],
}
@login_required
def outdoor_location_detail(request, pk):
"""
Vue détaillée d'une Localisation Espace Public Extérieur avec son arborescence unifiée de localisations et d'assets physiques.
"""
from django.contrib.contenttypes.models import ContentType
from interventions.models import Intervention
from interventions.permissions import filter_viewable_interventions_for_user
location = get_object_or_404(OutdoorLocation.objects.select_related('parent_location', 'manager'), pk=pk)
can_edit = can_edit_asset(request.user, location)
location_tree = build_outdoor_location_node(location)
parent_location = location.parent_location
documents = location.documents.all()
content_type = ContentType.objects.get_for_model(OutdoorLocation)
base_interventions = filter_viewable_interventions_for_user(request.user)
interventions = base_interventions.filter(
interventionasset__object_id=location.id,
interventionasset__content_type=content_type
).distinct().order_by('-creation_time')
location_geojson = None
center_lon, center_lat = None, None
if location.geom:
try:
geom_4326 = location.geom.clone()
geom_4326.transform(4326)
location_geojson = json.loads(geom_4326.json)
centroid = geom_4326.centroid
center_lon, center_lat = centroid.x, centroid.y
except Exception:
pass
elif location.lon is not None and location.lat is not None:
center_lon, center_lat = location.lon, location.lat
context = {
'location': location,
'can_edit': can_edit,
'location_tree': location_tree,
'parent_location': parent_location,
'equipments': location.get_all_assets(),
'documents': documents,
'interventions': interventions,
'content_type': content_type,
'location_geojson_json': json.dumps(location_geojson) if location_geojson else 'null',
'center_lon': center_lon,
'center_lat': center_lat,
}
return render(request, 'assets/outdoor/outdoorlocation_detail.html', context)
@login_required
def outdoor_assets_detail(request, asset_model, asset_id):
from django.contrib.contenttypes.models import ContentType
from assets.views.core import get_asset_edit_context
from interventions.models import Intervention
from interventions.permissions import filter_viewable_interventions_for_user
content_type = get_object_or_404(ContentType, app_label='assets', model=asset_model.lower())
model_class = content_type.model_class()
asset = get_object_or_404(model_class, pk=asset_id)
base_interventions = filter_viewable_interventions_for_user(request.user)
interventions = base_interventions.filter(
interventionasset__object_id=asset.id,
interventionasset__content_type=content_type
).distinct().order_by('-creation_time')
context = {
'asset': asset,
'content_type': content_type,
'interventions': interventions,
}
context.update(get_asset_edit_context(request, asset, content_type))
return render(request, "assets/detail_generic.html", context)

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 10:57+0200\n" "POT-Creation-Date: 2026-07-27 09:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,400 +18,392 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: authorizations/admin.py:28 #: streetup/authorizations/admin.py:28
#: authorizations/templates/authorizations/authorization_detail.html:73 #: streetup/authorizations/templates/authorizations/authorization_detail.html:73
#: authorizations/templates/authorizations/authorization_list.html:73 #: streetup/authorizations/templates/authorizations/authorization_list.html:73
#: authorizations/templates/authorizations/authorization_list.html:139 #: streetup/authorizations/templates/authorizations/authorization_list.html:139
msgid "Type" msgid "Type"
msgstr "Type" msgstr "Type"
#: authorizations/admin.py:46 #: streetup/authorizations/admin.py:46
msgid "Expiré" msgid "Expiré"
msgstr "Expired" msgstr "Expired"
#: authorizations/forms.py:62 #: streetup/authorizations/forms.py:62
msgid "Veuillez dessiner la zone concernée sur la carte." msgid "Veuillez dessiner la zone concernée sur la carte."
msgstr "Please draw the relevant area on the map." msgstr "Please draw the relevant area on the map."
#: authorizations/forms.py:66 #: streetup/authorizations/forms.py:66
msgid "Géométrie invalide." msgid "Géométrie invalide."
msgstr "Invalid geometry." msgstr "Invalid geometry."
#: authorizations/forms.py:68 #: streetup/authorizations/forms.py:68
msgid "Veuillez dessiner un polygone." msgid "Veuillez dessiner un polygone."
msgstr "Please draw a polygon." msgstr "Please draw a polygon."
#: authorizations/forms.py:77 #: streetup/authorizations/forms.py:77
#: authorizations/templates/authorizations/authorization_detail.html:175 #: streetup/authorizations/templates/authorizations/authorization_detail.html:175
msgid "Description du document" msgid "Description du document"
msgstr "Document description" msgstr "Document description"
#: authorizations/forms.py:127 #: streetup/authorizations/forms.py:127
msgid "Inclure les archives (> 2 ans)" msgid "Inclure les archives (> 2 ans)"
msgstr "Include archives (> 2 years)" msgstr "Include archives (> 2 years)"
#: authorizations/forms.py:132 #: streetup/authorizations/forms.py:132
msgid "Type d'autorisation" msgid "Type d'autorisation"
msgstr "Authorisation type" msgstr "Authorisation type"
#: authorizations/forms.py:140 #: streetup/authorizations/forms.py:140
msgid "Recherche…" msgid "Recherche…"
msgstr "Search…" msgstr "Search…"
#: authorizations/models.py:17 #: streetup/authorizations/models.py:17
msgid "Affichage (panneau, ...)" msgid "Affichage (panneau, ...)"
msgstr "Display (panel, ...)" msgstr "Display (panel, ...)"
#: authorizations/models.py:18 #: streetup/authorizations/models.py:18
msgid "Terrasse" msgid "Terrasse"
msgstr "Terrace" msgstr "Terrace"
#: authorizations/models.py:19 #: streetup/authorizations/models.py:19
msgid "Mobilier urbain" msgid "Mobilier urbain"
msgstr "Street furniture" msgstr "Street furniture"
#: authorizations/models.py:20 #: streetup/authorizations/models.py:20
msgid "Œuvre d'art" msgid "Œuvre d'art"
msgstr "Artwork" msgstr "Artwork"
#: authorizations/models.py:21 #: streetup/authorizations/models.py:21
msgid "Nouvel Emplacement PMR" msgid "Nouvel Emplacement PMR"
msgstr "New PRM parking space" msgstr "New PRM parking space"
#: authorizations/models.py:22 #: streetup/authorizations/models.py:22
msgid "Nouvelle Zone de livraison" msgid "Nouvelle Zone de livraison"
msgstr "New delivery zone" msgstr "New delivery zone"
#: authorizations/models.py:23 #: streetup/authorizations/models.py:23
msgid "Tournage audiovisuel" msgid "Tournage audiovisuel"
msgstr "Audiovisual filming" msgstr "Audiovisual filming"
#: authorizations/models.py:24 #: streetup/authorizations/models.py:24
msgid "Evénement" msgid "Evénement"
msgstr "Event" msgstr "Event"
#: authorizations/models.py:25 #: streetup/authorizations/models.py:25
msgid "Plante grimpante" msgid "Plante grimpante"
msgstr "Climbing plant" msgstr "Climbing plant"
#: authorizations/models.py:26 #: streetup/authorizations/models.py:26
msgid "Pied d'arbre" msgid "Pied d'arbre"
msgstr "Tree base" msgstr "Tree base"
#: authorizations/models.py:30 #: streetup/authorizations/models.py:30
msgid "À traiter" msgid "À traiter"
msgstr "To be processed" msgstr "To be processed"
#: authorizations/models.py:31 #: streetup/authorizations/models.py:31
msgid "En cours" msgid "En cours"
msgstr "In progress" msgstr "In progress"
#: authorizations/models.py:32 #: streetup/authorizations/models.py:32
msgid "Autorisé" msgid "Autorisé"
msgstr "Authorised" msgstr "Authorised"
#: authorizations/models.py:33 #: streetup/authorizations/models.py:33
msgid "Refusé" msgid "Refusé"
msgstr "Refused" msgstr "Refused"
#: authorizations/models.py:34 #: streetup/authorizations/models.py:34
msgid "Clôturé" msgid "Clôturé"
msgstr "Closed" msgstr "Closed"
#: authorizations/models.py:45 #: streetup/authorizations/models.py:45
msgid "Langue" msgid "Langue"
msgstr "Language" msgstr "Language"
#: authorizations/models.py:49 authorizations/models.py:201 #: streetup/authorizations/models.py:49 streetup/authorizations/models.py:201
msgid "Email vérifié" msgid "Email vérifié"
msgstr "Email verified" msgstr "Email verified"
#: authorizations/models.py:50 #: streetup/authorizations/models.py:50
msgid "Indique si l'adresse email du demandeur a été vérifiée." msgid "Indique si l'adresse email du demandeur a été vérifiée."
msgstr "Indicates whether the applicant's email address has been verified." msgstr "Indicates whether the applicant's email address has been verified."
#: authorizations/models.py:52 #: streetup/authorizations/models.py:52
msgid "Type d'occupation" msgid "Type d'occupation"
msgstr "Occupation type" msgstr "Occupation type"
#: authorizations/models.py:53 #: streetup/authorizations/models.py:53
#: authorizations/templates/authorizations/authorization_detail.html:92 #: streetup/authorizations/templates/authorizations/authorization_detail.html:92
msgid "Description de la demande" msgid "Description de la demande"
msgstr "Request description" msgstr "Request description"
#: authorizations/models.py:54 #: streetup/authorizations/models.py:54
#: authorizations/templates/authorizations/authorization_detail.html:105 #: streetup/authorizations/templates/authorizations/authorization_detail.html:105
msgid "Nom" msgid "Nom"
msgstr "Name" msgstr "Name"
#: authorizations/models.py:55 authorizations/models.py:197 #: streetup/authorizations/models.py:55 streetup/authorizations/models.py:197
#: authorizations/templates/authorizations/authorization_detail.html:108 #: streetup/authorizations/templates/authorizations/authorization_detail.html:108
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#: authorizations/models.py:56 #: streetup/authorizations/models.py:56
#: authorizations/templates/authorizations/authorization_detail.html:113 #: streetup/authorizations/templates/authorizations/authorization_detail.html:113
msgid "Adresse" msgid "Adresse"
msgstr "Address" msgstr "Address"
#: authorizations/models.py:57 #: streetup/authorizations/models.py:57
#: authorizations/templates/authorizations/authorization_detail.html:117 #: streetup/authorizations/templates/authorizations/authorization_detail.html:117
msgid "Entreprise" msgid "Entreprise"
msgstr "Company" msgstr "Company"
#: authorizations/models.py:64 #: streetup/authorizations/models.py:64
msgid "Agent traitant" msgid "Agent traitant"
msgstr "Handling agent" msgstr "Handling agent"
#: authorizations/models.py:66 #: streetup/authorizations/models.py:66
msgid "Commentaire" msgid "Commentaire"
msgstr "Comment" msgstr "Comment"
#: authorizations/models.py:67 #: streetup/authorizations/models.py:67
msgid "Localisation – Adresse" msgid "Localisation – Adresse"
msgstr "Location – Address" msgstr "Location – Address"
#: authorizations/models.py:68 #: streetup/authorizations/models.py:68
#: authorizations/templates/authorizations/authorization_detail.html:95 #: streetup/authorizations/templates/authorizations/authorization_detail.html:95
#: authorizations/templates/authorizations/authorization_detail.html:228 #: streetup/authorizations/templates/authorizations/authorization_detail.html:228
#: authorizations/templates/authorizations/authorization_list.html:142 #: streetup/authorizations/templates/authorizations/authorization_list.html:142
msgid "Localisation" msgid "Localisation"
msgstr "Location" msgstr "Location"
#: authorizations/models.py:107 #: streetup/authorizations/models.py:107
msgid "Autorisation de voirie" msgid "Autorisation de voirie"
msgstr "Road authorisation" msgstr "Road authorisation"
#: authorizations/models.py:108 #: streetup/authorizations/models.py:108
msgid "Autorisations de voirie" msgid "Autorisations de voirie"
msgstr "Road authorisations" msgstr "Road authorisations"
#: authorizations/models.py:146 #: streetup/authorizations/models.py:146
#, python-format #, python-format
msgid "La taille maximale du fichier est de %(limit)s Mo." msgid "La taille maximale du fichier est de %(limit)s Mo."
msgstr "The maximum file size is %(limit)s MB." msgstr "The maximum file size is %(limit)s MB."
#: authorizations/models.py:151 #: streetup/authorizations/models.py:151
#: authorizations/templates/authorizations/authorization_detail.html:179 #: streetup/authorizations/templates/authorizations/authorization_detail.html:179
msgid "Fichier" msgid "Fichier"
msgstr "File" msgstr "File"
#: authorizations/models.py:155 #: streetup/authorizations/models.py:155
msgid "Description" msgid "Description"
msgstr "Description" msgstr "Description"
#: authorizations/models.py:163 #: streetup/authorizations/models.py:163
msgid "Document" msgid "Document"
msgstr "Document" msgstr "Document"
#: authorizations/models.py:164 #: streetup/authorizations/models.py:164
msgid "Documents" msgid "Documents"
msgstr "Documents" msgstr "Documents"
#: authorizations/models.py:176 #: streetup/authorizations/models.py:176
msgid "Début de l'occupation" msgid "Début de l'occupation"
msgstr "Start of occupation" msgstr "Start of occupation"
#: authorizations/models.py:177 #: streetup/authorizations/models.py:177
msgid "Fin de l'occupation" msgid "Fin de l'occupation"
msgstr "End of occupation" msgstr "End of occupation"
#: authorizations/models.py:180 #: streetup/authorizations/models.py:180
msgid "Date d'occupation" msgid "Date d'occupation"
msgstr "Occupation date" msgstr "Occupation date"
#: authorizations/models.py:181 #: streetup/authorizations/models.py:181
#: authorizations/templates/authorizations/authorization_detail.html:127 #: streetup/authorizations/templates/authorizations/authorization_detail.html:127
msgid "Dates d'occupation" msgid "Dates d'occupation"
msgstr "Occupation dates" msgstr "Occupation dates"
#: authorizations/models.py:202 #: streetup/authorizations/models.py:202
msgid "Emails vérifiés" msgid "Emails vérifiés"
msgstr "Verified emails" msgstr "Verified emails"
#: authorizations/models.py:247 #: streetup/authorizations/models.py:247
msgid "Code de vérification" msgid "Code de vérification"
msgstr "Verification code" msgstr "Verification code"
#: authorizations/models.py:248 #: streetup/authorizations/models.py:248
msgid "Codes de vérification" msgid "Codes de vérification"
msgstr "Verification codes" msgstr "Verification codes"
#: authorizations/templates/authorizations/authorization_detail.html:12 #: streetup/authorizations/templates/authorizations/authorization_detail.html:12
msgid "Retour à la liste" msgid "Retour à la liste"
msgstr "Back to list" msgstr "Back to list"
#: authorizations/templates/authorizations/authorization_detail.html:13 #: streetup/authorizations/templates/authorizations/authorization_detail.html:13
#: authorizations/templates/authorizations/authorization_detail.html:27 #: streetup/authorizations/templates/authorizations/authorization_detail.html:27
msgid "Retour" msgid "Retour"
msgstr "Back" msgstr "Back"
#: authorizations/templates/authorizations/authorization_detail.html:23 #: streetup/authorizations/templates/authorizations/authorization_detail.html:23
msgid "Demande" msgid "Demande"
msgstr "Request" msgstr "Request"
#: authorizations/templates/authorizations/authorization_detail.html:52 #: streetup/authorizations/templates/authorizations/authorization_detail.html:52
msgid "Informations générales" msgid "Informations générales"
msgstr "General information" msgstr "General information"
#: authorizations/templates/authorizations/authorization_detail.html:55 #: streetup/authorizations/templates/authorizations/authorization_detail.html:55
#: authorizations/templates/authorizations/authorization_list.html:138 #: streetup/authorizations/templates/authorizations/authorization_list.html:138
msgid "Code" msgid "Code"
msgstr "Code" msgstr "Code"
#: authorizations/templates/authorizations/authorization_detail.html:58 #: streetup/authorizations/templates/authorizations/authorization_detail.html:58
#: authorizations/templates/authorizations/authorization_list.html:140 #: streetup/authorizations/templates/authorizations/authorization_list.html:140
msgid "Statut" msgid "Statut"
msgstr "Status" msgstr "Status"
#: authorizations/templates/authorizations/authorization_detail.html:76 #: streetup/authorizations/templates/authorizations/authorization_detail.html:76
msgid "Créé le" msgid "Créé le"
msgstr "Created on" msgstr "Created on"
#: authorizations/templates/authorizations/authorization_detail.html:79 #: streetup/authorizations/templates/authorizations/authorization_detail.html:79
msgid "Mis à jour" msgid "Mis à jour"
msgstr "Updated" msgstr "Updated"
#: authorizations/templates/authorizations/authorization_detail.html:83 #: streetup/authorizations/templates/authorizations/authorization_detail.html:83
msgid "Agent" msgid "Agent"
msgstr "Agent" msgstr "Agent"
#: authorizations/templates/authorizations/authorization_detail.html:102 #: streetup/authorizations/templates/authorizations/authorization_detail.html:102
#: authorizations/templates/authorizations/authorization_list.html:141 #: streetup/authorizations/templates/authorizations/authorization_list.html:141
msgid "Demandeur" msgid "Demandeur"
msgstr "Applicant" msgstr "Applicant"
#: authorizations/templates/authorizations/authorization_detail.html:132 #: streetup/authorizations/templates/authorizations/authorization_detail.html:132
msgid "Début" msgid "Début"
msgstr "Start" msgstr "Start"
#: authorizations/templates/authorizations/authorization_detail.html:133 #: streetup/authorizations/templates/authorizations/authorization_detail.html:133
msgid "Fin" msgid "Fin"
msgstr "End" msgstr "End"
#: authorizations/templates/authorizations/authorization_detail.html:151 #: streetup/authorizations/templates/authorizations/authorization_detail.html:151
msgid "Documents joints" msgid "Documents joints"
msgstr "Attached documents" msgstr "Attached documents"
#: authorizations/templates/authorizations/authorization_detail.html:167 #: streetup/authorizations/templates/authorizations/authorization_detail.html:167
#, fuzzy
#| msgid "Documents joints"
msgid "Aucun document joint." msgid "Aucun document joint."
msgstr "Attached documents" msgstr "Attached documents"
#: authorizations/templates/authorizations/authorization_detail.html:176 #: streetup/authorizations/templates/authorizations/authorization_detail.html:176
msgid "Ex: Arrêté de police" msgid "Ex: Arrêté de police"
msgstr "" msgstr ""
#: authorizations/templates/authorizations/authorization_detail.html:183 #: streetup/authorizations/templates/authorizations/authorization_detail.html:183
#, fuzzy
#| msgid "Ajouter une date"
msgid "Ajouter" msgid "Ajouter"
msgstr "Add a date" msgstr "Add a date"
#: authorizations/templates/authorizations/authorization_detail.html:194 #: streetup/authorizations/templates/authorizations/authorization_detail.html:194
#, fuzzy
#| msgid "Description de la demande"
msgid "Traitement de la demande" msgid "Traitement de la demande"
msgstr "Request description" msgstr "Request description"
#: authorizations/templates/authorizations/authorization_detail.html:201 #: streetup/authorizations/templates/authorizations/authorization_detail.html:201
msgid "Modifier le statut" msgid "Modifier le statut"
msgstr "" msgstr ""
#: authorizations/templates/authorizations/authorization_detail.html:211 #: streetup/authorizations/templates/authorizations/authorization_detail.html:211
msgid "Commentaire agent" msgid "Commentaire agent"
msgstr "Agent comment" msgstr "Agent comment"
#: authorizations/templates/authorizations/authorization_detail.html:212 #: streetup/authorizations/templates/authorizations/authorization_detail.html:212
msgid "Saisir un commentaire ou des notes..." msgid "Saisir un commentaire ou des notes..."
msgstr "" msgstr ""
#: authorizations/templates/authorizations/authorization_detail.html:216 #: streetup/authorizations/templates/authorizations/authorization_detail.html:216
msgid "Enregistrer le traitement" msgid "Enregistrer le traitement"
msgstr "" msgstr ""
#: authorizations/templates/authorizations/authorization_list.html:41 #: streetup/authorizations/templates/authorizations/authorization_list.html:41
#: authorizations/templates/authorizations/authorization_list.html:42 #: streetup/authorizations/templates/authorizations/authorization_list.html:42
#: authorizations/templates/authorizations/authorization_list.html:58 #: streetup/authorizations/templates/authorizations/authorization_list.html:58
msgid "Formulaire public" msgid "Formulaire public"
msgstr "Public form" msgstr "Public form"
#: authorizations/templates/authorizations/authorization_list.html:45 #: streetup/authorizations/templates/authorizations/authorization_list.html:45
msgid "Liste" msgid "Liste"
msgstr "List" msgstr "List"
#: authorizations/templates/authorizations/authorization_list.html:46 #: streetup/authorizations/templates/authorizations/authorization_list.html:46
msgid "Liste des demandes" msgid "Liste des demandes"
msgstr "List of requests" msgstr "List of requests"
#: authorizations/templates/authorizations/authorization_list.html:55 #: streetup/authorizations/templates/authorizations/authorization_list.html:55
msgid "Demandes d'autorisation de voirie" msgid "Demandes d'autorisation de voirie"
msgstr "Road authorisation requests" msgstr "Road authorisation requests"
#: authorizations/templates/authorizations/authorization_list.html:68 #: streetup/authorizations/templates/authorizations/authorization_list.html:68
msgid "Recherche" msgid "Recherche"
msgstr "Search" msgstr "Search"
#: authorizations/templates/authorizations/authorization_list.html:82 #: streetup/authorizations/templates/authorizations/authorization_list.html:82
#: authorizations/templates/authorizations/authorization_list.html:207 #: streetup/authorizations/templates/authorizations/authorization_list.html:207
msgid "Tous les types" msgid "Tous les types"
msgstr "" msgstr ""
#: authorizations/templates/authorizations/authorization_list.html:89 #: streetup/authorizations/templates/authorizations/authorization_list.html:89
#, fuzzy
#| msgid "Recherche"
msgid "Rechercher un type..." msgid "Rechercher un type..."
msgstr "Search" msgstr "Search"
#: authorizations/templates/authorizations/authorization_list.html:92 #: streetup/authorizations/templates/authorizations/authorization_list.html:92
#: authorizations/templates/authorizations/new_authorization.html:240 #: streetup/authorizations/templates/authorizations/new_authorization.html:240
msgid "Effacer" msgid "Effacer"
msgstr "Clear" msgstr "Clear"
#: authorizations/templates/authorizations/authorization_list.html:106 #: streetup/authorizations/templates/authorizations/authorization_list.html:106
msgid "Aucun type correspondant" msgid "Aucun type correspondant"
msgstr "" msgstr ""
#: authorizations/templates/authorizations/authorization_list.html:121 #: streetup/authorizations/templates/authorizations/authorization_list.html:121
msgid "Filtrer" msgid "Filtrer"
msgstr "Filter" msgstr "Filter"
#: authorizations/templates/authorizations/authorization_list.html:143 #: streetup/authorizations/templates/authorizations/authorization_list.html:143
msgid "Date de création" msgid "Date de création"
msgstr "Creation date" msgstr "Creation date"
#: authorizations/templates/authorizations/authorization_list.html:177 #: streetup/authorizations/templates/authorizations/authorization_list.html:177
msgid "Aucune demande trouvée." msgid "Aucune demande trouvée."
msgstr "No request found." msgstr "No request found."
#: authorizations/templates/authorizations/authorization_list.html:215 #: streetup/authorizations/templates/authorizations/authorization_list.html:215
msgid "types sélectionnés" msgid "types sélectionnés"
msgstr "" msgstr ""
#: authorizations/templates/authorizations/authorization_success.html:9 #: streetup/authorizations/templates/authorizations/authorization_success.html:9
#: authorizations/templates/authorizations/authorization_success.html:37 #: streetup/authorizations/templates/authorizations/authorization_success.html:37
msgid "Demande enregistrée" msgid "Demande enregistrée"
msgstr "Request saved" msgstr "Request saved"
#: authorizations/templates/authorizations/authorization_success.html:28 #: streetup/authorizations/templates/authorizations/authorization_success.html:28
#: authorizations/templates/authorizations/email_verification_failed.html:28 #: streetup/authorizations/templates/authorizations/email_verification_failed.html:28
#: authorizations/templates/authorizations/email_verification_pending.html:28 #: streetup/authorizations/templates/authorizations/email_verification_pending.html:28
#: authorizations/templates/authorizations/email_verify_code.html:35 #: streetup/authorizations/templates/authorizations/email_verify_code.html:35
#: authorizations/templates/authorizations/new_authorization.html:9 #: streetup/authorizations/templates/authorizations/new_authorization.html:9
#: authorizations/templates/authorizations/new_authorization.html:99 #: streetup/authorizations/templates/authorizations/new_authorization.html:99
msgid "Demande d'occupation de voirie régionale" msgid "Demande d'occupation de voirie régionale"
msgstr "Regional road occupation request" msgstr "Regional road occupation request"
#: authorizations/templates/authorizations/authorization_success.html:41 #: streetup/authorizations/templates/authorizations/authorization_success.html:41
msgid "Numéro de référence" msgid "Numéro de référence"
msgstr "Reference number" msgstr "Reference number"
#: authorizations/templates/authorizations/authorization_success.html:43 #: streetup/authorizations/templates/authorizations/authorization_success.html:43
msgid "Conservez ce numéro pour le suivi de votre dossier." msgid "Conservez ce numéro pour le suivi de votre dossier."
msgstr "Keep this number to track your application." msgstr "Keep this number to track your application."
#: authorizations/templates/authorizations/authorization_success.html:49 #: streetup/authorizations/templates/authorizations/authorization_success.html:49
msgid "" msgid ""
"Votre adresse email a été confirmée et votre demande d'autorisation " "Votre adresse email a été confirmée et votre demande d'autorisation "
"d'occupation de voirie régionale a bien été enregistrée. Un accusé de " "d'occupation de voirie régionale a bien été enregistrée. Un accusé de "
@ -422,7 +414,7 @@ msgstr ""
"request has been registered. An acknowledgement of receipt has been sent to " "request has been registered. An acknowledgement of receipt has been sent to "
"you by email. Our services will contact you as soon as possible." "you by email. Our services will contact you as soon as possible."
#: authorizations/templates/authorizations/authorization_success.html:55 #: streetup/authorizations/templates/authorizations/authorization_success.html:55
msgid "" msgid ""
"Votre demande d'autorisation d'occupation de voirie régionale a bien été " "Votre demande d'autorisation d'occupation de voirie régionale a bien été "
"enregistrée. Un accusé de réception vous a été envoyé par email. Nos " "enregistrée. Un accusé de réception vous a été envoyé par email. Nos "
@ -432,20 +424,20 @@ msgstr ""
"acknowledgement of receipt has been sent to you by email. Our services will " "acknowledgement of receipt has been sent to you by email. Our services will "
"contact you as soon as possible." "contact you as soon as possible."
#: authorizations/templates/authorizations/authorization_success.html:64 #: streetup/authorizations/templates/authorizations/authorization_success.html:64
msgid "Faire une nouvelle demande" msgid "Faire une nouvelle demande"
msgstr "Submit a new request" msgstr "Submit a new request"
#: authorizations/templates/authorizations/email_verification_failed.html:9 #: streetup/authorizations/templates/authorizations/email_verification_failed.html:9
#: authorizations/templates/authorizations/email_verification_failed.html:47 #: streetup/authorizations/templates/authorizations/email_verification_failed.html:47
msgid "Lien invalide" msgid "Lien invalide"
msgstr "Invalid link" msgstr "Invalid link"
#: authorizations/templates/authorizations/email_verification_failed.html:39 #: streetup/authorizations/templates/authorizations/email_verification_failed.html:39
msgid "Lien expiré" msgid "Lien expiré"
msgstr "Expired link" msgstr "Expired link"
#: authorizations/templates/authorizations/email_verification_failed.html:41 #: streetup/authorizations/templates/authorizations/email_verification_failed.html:41
msgid "" msgid ""
"Le lien de confirmation que vous avez utilisé n'est plus valide (délai de 48 " "Le lien de confirmation que vous avez utilisé n'est plus valide (délai de 48 "
"heures dépassé). Veuillez soumettre une nouvelle demande." "heures dépassé). Veuillez soumettre une nouvelle demande."
@ -453,7 +445,7 @@ msgstr ""
"The confirmation link you used is no longer valid (48-hour deadline " "The confirmation link you used is no longer valid (48-hour deadline "
"exceeded). Please submit a new request." "exceeded). Please submit a new request."
#: authorizations/templates/authorizations/email_verification_failed.html:49 #: streetup/authorizations/templates/authorizations/email_verification_failed.html:49
msgid "" msgid ""
"Le lien de confirmation que vous avez utilisé est invalide ou a déjà été " "Le lien de confirmation que vous avez utilisé est invalide ou a déjà été "
"utilisé. Si votre demande a déjà été validée, vous pouvez en soumettre une " "utilisé. Si votre demande a déjà été validée, vous pouvez en soumettre une "
@ -462,21 +454,21 @@ msgstr ""
"The confirmation link you used is invalid or has already been used. If your " "The confirmation link you used is invalid or has already been used. If your "
"request has already been validated, you can submit a new one." "request has already been validated, you can submit a new one."
#: authorizations/templates/authorizations/email_verification_failed.html:58 #: streetup/authorizations/templates/authorizations/email_verification_failed.html:58
#: authorizations/templates/authorizations/email_verify_code.html:55 #: streetup/authorizations/templates/authorizations/email_verify_code.html:55
#: authorizations/templates/authorizations/email_verify_code.html:72 #: streetup/authorizations/templates/authorizations/email_verify_code.html:72
msgid "Nouvelle demande" msgid "Nouvelle demande"
msgstr "New request" msgstr "New request"
#: authorizations/templates/authorizations/email_verification_pending.html:9 #: streetup/authorizations/templates/authorizations/email_verification_pending.html:9
msgid "Confirmation de votre email" msgid "Confirmation de votre email"
msgstr "Email confirmation" msgstr "Email confirmation"
#: authorizations/templates/authorizations/email_verification_pending.html:37 #: streetup/authorizations/templates/authorizations/email_verification_pending.html:37
msgid "Vérifiez votre boîte email" msgid "Vérifiez votre boîte email"
msgstr "Check your inbox" msgstr "Check your inbox"
#: authorizations/templates/authorizations/email_verification_pending.html:39 #: streetup/authorizations/templates/authorizations/email_verification_pending.html:39
msgid "" msgid ""
"Votre demande a bien été reçue. Pour la finaliser, veuillez confirmer votre " "Votre demande a bien été reçue. Pour la finaliser, veuillez confirmer votre "
"adresse email en cliquant sur le lien que nous venons de vous envoyer." "adresse email en cliquant sur le lien que nous venons de vous envoyer."
@ -484,7 +476,7 @@ msgstr ""
"Your request has been received. To finalise it, please confirm your email " "Your request has been received. To finalise it, please confirm your email "
"address by clicking the link we have just sent you." "address by clicking the link we have just sent you."
#: authorizations/templates/authorizations/email_verification_pending.html:47 #: streetup/authorizations/templates/authorizations/email_verification_pending.html:47
msgid "" msgid ""
"Le lien de confirmation est valable 48 heures. Si vous ne trouvez pas " "Le lien de confirmation est valable 48 heures. Si vous ne trouvez pas "
"l'email, vérifiez votre dossier de courrier indésirable (spam)." "l'email, vérifiez votre dossier de courrier indésirable (spam)."
@ -492,19 +484,19 @@ msgstr ""
"The confirmation link is valid for 48 hours. If you cannot find the email, " "The confirmation link is valid for 48 hours. If you cannot find the email, "
"please check your junk/spam folder." "please check your junk/spam folder."
#: authorizations/templates/authorizations/email_verification_pending.html:55 #: streetup/authorizations/templates/authorizations/email_verification_pending.html:55
msgid "Retour au formulaire" msgid "Retour au formulaire"
msgstr "Back to the form" msgstr "Back to the form"
#: authorizations/templates/authorizations/email_verify_code.html:9 #: streetup/authorizations/templates/authorizations/email_verify_code.html:9
msgid "Vérification de votre email" msgid "Vérification de votre email"
msgstr "Email verification" msgstr "Email verification"
#: authorizations/templates/authorizations/email_verify_code.html:46 #: streetup/authorizations/templates/authorizations/email_verify_code.html:46
msgid "Code expiré" msgid "Code expiré"
msgstr "Code expired" msgstr "Code expired"
#: authorizations/templates/authorizations/email_verify_code.html:48 #: streetup/authorizations/templates/authorizations/email_verify_code.html:48
msgid "" msgid ""
"Le code de vérification est arrivé à expiration (validité&nbsp;: " "Le code de vérification est arrivé à expiration (validité&nbsp;: "
"10&nbsp;minutes). Veuillez soumettre une nouvelle demande pour recevoir un " "10&nbsp;minutes). Veuillez soumettre une nouvelle demande pour recevoir un "
@ -513,11 +505,11 @@ msgstr ""
"The verification code has expired (validity:&nbsp;10&nbsp;minutes). Please " "The verification code has expired (validity:&nbsp;10&nbsp;minutes). Please "
"submit a new request to receive a new code." "submit a new request to receive a new code."
#: authorizations/templates/authorizations/email_verify_code.html:63 #: streetup/authorizations/templates/authorizations/email_verify_code.html:63
msgid "Trop de tentatives" msgid "Trop de tentatives"
msgstr "Too many attempts" msgstr "Too many attempts"
#: authorizations/templates/authorizations/email_verify_code.html:65 #: streetup/authorizations/templates/authorizations/email_verify_code.html:65
msgid "" msgid ""
"Le nombre maximum de tentatives a été atteint. Votre code est bloqué. " "Le nombre maximum de tentatives a été atteint. Votre code est bloqué. "
"Veuillez soumettre une nouvelle demande." "Veuillez soumettre une nouvelle demande."
@ -525,11 +517,11 @@ msgstr ""
"The maximum number of attempts has been reached. Your code is blocked. " "The maximum number of attempts has been reached. Your code is blocked. "
"Please submit a new request." "Please submit a new request."
#: authorizations/templates/authorizations/email_verify_code.html:80 #: streetup/authorizations/templates/authorizations/email_verify_code.html:80
msgid "Entrez votre code de vérification" msgid "Entrez votre code de vérification"
msgstr "Enter your verification code" msgstr "Enter your verification code"
#: authorizations/templates/authorizations/email_verify_code.html:82 #: streetup/authorizations/templates/authorizations/email_verify_code.html:82
#, python-format #, python-format
msgid "" msgid ""
"Un code à 6 chiffres a été envoyé à <strong>%(email)s</strong>. Saisissez-le " "Un code à 6 chiffres a été envoyé à <strong>%(email)s</strong>. Saisissez-le "
@ -538,30 +530,30 @@ msgstr ""
"A 6-digit code has been sent to <strong>%(email)s</strong>. Enter it below. " "A 6-digit code has been sent to <strong>%(email)s</strong>. Enter it below. "
"It is valid for 10&nbsp;minutes." "It is valid for 10&nbsp;minutes."
#: authorizations/templates/authorizations/email_verify_code.html:92 #: streetup/authorizations/templates/authorizations/email_verify_code.html:92
msgid "Code incorrect. Vérifiez l'email reçu et réessayez." msgid "Code incorrect. Vérifiez l'email reçu et réessayez."
msgstr "Incorrect code. Check the email you received and try again." msgstr "Incorrect code. Check the email you received and try again."
#: authorizations/templates/authorizations/email_verify_code.html:115 #: streetup/authorizations/templates/authorizations/email_verify_code.html:115
msgid "Code de vérification à 6 chiffres" msgid "Code de vérification à 6 chiffres"
msgstr "6-digit verification code" msgstr "6-digit verification code"
#: authorizations/templates/authorizations/email_verify_code.html:118 #: streetup/authorizations/templates/authorizations/email_verify_code.html:118
msgid "Vérifier" msgid "Vérifier"
msgstr "Verify" msgstr "Verify"
#: authorizations/templates/authorizations/email_verify_code.html:124 #: streetup/authorizations/templates/authorizations/email_verify_code.html:124
msgid "" msgid ""
"Vous n'avez pas reçu l'email ? Vérifiez votre dossier de courrier " "Vous n'avez pas reçu l'email ? Vérifiez votre dossier de courrier "
"indésirable (spam)." "indésirable (spam)."
msgstr "Didn't receive the email? Check your junk/spam folder." msgstr "Didn't receive the email? Check your junk/spam folder."
#: authorizations/templates/authorizations/new_authorization.html:96 #: streetup/authorizations/templates/authorizations/new_authorization.html:96
#: authorizations/templates/authorizations/new_authorization.html:98 #: streetup/authorizations/templates/authorizations/new_authorization.html:98
msgid "Brussels Mobility" msgid "Brussels Mobility"
msgstr "" msgstr ""
#: authorizations/templates/authorizations/new_authorization.html:116 #: streetup/authorizations/templates/authorizations/new_authorization.html:116
msgid "" msgid ""
"Toute occupation de l'espace public doit faire l'objet d'une autorisation de " "Toute occupation de l'espace public doit faire l'objet d'une autorisation de "
"la part du gestionnaire de voiries. En Région de Bruxelles-Capitale, les " "la part du gestionnaire de voiries. En Région de Bruxelles-Capitale, les "
@ -578,7 +570,7 @@ msgstr ""
"are managed by the municipalities. Regional roads are shown in <strong " "are managed by the municipalities. Regional roads are shown in <strong "
"style=\"color:#cc0000;\">red</strong> on the map below." "style=\"color:#cc0000;\">red</strong> on the map below."
#: authorizations/templates/authorizations/new_authorization.html:125 #: streetup/authorizations/templates/authorizations/new_authorization.html:125
msgid "" msgid ""
"Ce formulaire ne concerne que les autorisations sur <strong>voiries " "Ce formulaire ne concerne que les autorisations sur <strong>voiries "
"régionales</strong>. Veuillez vous adresser à votre commune pour les autres " "régionales</strong>. Veuillez vous adresser à votre commune pour les autres "
@ -587,7 +579,7 @@ msgstr ""
"This form only covers authorisations on <strong>regional roads</strong>. " "This form only covers authorisations on <strong>regional roads</strong>. "
"Please contact your municipality for other authorisations or documents." "Please contact your municipality for other authorisations or documents."
#: authorizations/templates/authorizations/new_authorization.html:131 #: streetup/authorizations/templates/authorizations/new_authorization.html:131
msgid "" msgid ""
"Pour les demandes liées à des chantiers, veuillez utiliser le système <a " "Pour les demandes liées à des chantiers, veuillez utiliser le système <a "
"href=\"https://my.osiris.brussels/\" target=\"_blank\" " "href=\"https://my.osiris.brussels/\" target=\"_blank\" "
@ -599,7 +591,7 @@ msgstr ""
"Requests for the placement of containers, scaffolding or removals are also " "Requests for the placement of containers, scaffolding or removals are also "
"made through this system." "made through this system."
#: authorizations/templates/authorizations/new_authorization.html:138 #: streetup/authorizations/templates/authorizations/new_authorization.html:138
msgid "" msgid ""
"Si vous éprouvez des difficultés, contactez-nous par mail (<a " "Si vous éprouvez des difficultés, contactez-nous par mail (<a "
"href=\"mailto:mobilite@sprb.brussels\">mobilite@sprb.brussels</a>) ou par " "href=\"mailto:mobilite@sprb.brussels\">mobilite@sprb.brussels</a>) ou par "
@ -609,23 +601,23 @@ msgstr ""
"href=\"mailto:mobilite@sprb.brussels\">mobilite@sprb.brussels</a>) or by " "href=\"mailto:mobilite@sprb.brussels\">mobilite@sprb.brussels</a>) or by "
"phone (<a href=\"tel:+3280094001\">0800 94 001</a>)." "phone (<a href=\"tel:+3280094001\">0800 94 001</a>)."
#: authorizations/templates/authorizations/new_authorization.html:158 #: streetup/authorizations/templates/authorizations/new_authorization.html:158
msgid "Votre demande" msgid "Votre demande"
msgstr "Your request" msgstr "Your request"
#: authorizations/templates/authorizations/new_authorization.html:230 #: streetup/authorizations/templates/authorizations/new_authorization.html:230
msgid "Localisation (dessiner un polygone pour indiquer la zone concernée)" msgid "Localisation (dessiner un polygone pour indiquer la zone concernée)"
msgstr "Location (draw a polygon to indicate the area concerned)" msgstr "Location (draw a polygon to indicate the area concerned)"
#: authorizations/templates/authorizations/new_authorization.html:234 #: streetup/authorizations/templates/authorizations/new_authorization.html:234
msgid "Dessiner un polygone" msgid "Dessiner un polygone"
msgstr "Draw a polygon" msgstr "Draw a polygon"
#: authorizations/templates/authorizations/new_authorization.html:237 #: streetup/authorizations/templates/authorizations/new_authorization.html:237
msgid "Terminer" msgid "Terminer"
msgstr "Finish" msgstr "Finish"
#: authorizations/templates/authorizations/new_authorization.html:245 #: streetup/authorizations/templates/authorizations/new_authorization.html:245
msgid "" msgid ""
"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " "Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter "
"des sommets. Cliquez sur le premier sommet pour fermer le polygone." "des sommets. Cliquez sur le premier sommet pour fermer le polygone."
@ -633,92 +625,92 @@ msgstr ""
"Click « Draw a polygon » then click on the map to add vertices. Click on the " "Click « Draw a polygon » then click on the map to add vertices. Click on the "
"first vertex to close the polygon." "first vertex to close the polygon."
#: authorizations/templates/authorizations/new_authorization.html:259 #: streetup/authorizations/templates/authorizations/new_authorization.html:259
msgid "Vos coordonnées" msgid "Vos coordonnées"
msgstr "Your contact details" msgstr "Your contact details"
#: authorizations/templates/authorizations/new_authorization.html:292 #: streetup/authorizations/templates/authorizations/new_authorization.html:292
#: authorizations/templates/authorizations/new_authorization.html:331 #: streetup/authorizations/templates/authorizations/new_authorization.html:331
#: authorizations/templates/authorizations/new_authorization.html:351 #: streetup/authorizations/templates/authorizations/new_authorization.html:351
msgid "optionnel" msgid "optionnel"
msgstr "optional" msgstr "optional"
#: authorizations/templates/authorizations/new_authorization.html:306 #: streetup/authorizations/templates/authorizations/new_authorization.html:306
msgid "Date(s) d'occupation" msgid "Date(s) d'occupation"
msgstr "Occupation date(s)" msgstr "Occupation date(s)"
#: authorizations/templates/authorizations/new_authorization.html:312 #: streetup/authorizations/templates/authorizations/new_authorization.html:312
msgid "Veuillez indiquer au moins une date de début d'occupation." msgid "Veuillez indiquer au moins une date de début d'occupation."
msgstr "Please provide at least one occupation start date." msgstr "Please provide at least one occupation start date."
#: authorizations/templates/authorizations/new_authorization.html:342 #: streetup/authorizations/templates/authorizations/new_authorization.html:342
msgid "Ajouter une date" msgid "Ajouter une date"
msgstr "Add a date" msgstr "Add a date"
#: authorizations/templates/authorizations/new_authorization.html:350 #: streetup/authorizations/templates/authorizations/new_authorization.html:350
msgid "Document(s) à joindre" msgid "Document(s) à joindre"
msgstr "Document(s) to attach" msgstr "Document(s) to attach"
#: authorizations/templates/authorizations/new_authorization.html:374 #: streetup/authorizations/templates/authorizations/new_authorization.html:374
msgid "Ajouter un document" msgid "Ajouter un document"
msgstr "Add a document" msgstr "Add a document"
#: authorizations/templates/authorizations/new_authorization.html:381 #: streetup/authorizations/templates/authorizations/new_authorization.html:381
msgid "Envoyer la demande" msgid "Envoyer la demande"
msgstr "Submit the request" msgstr "Submit the request"
#: authorizations/templates/authorizations/new_authorization.html:390 #: streetup/authorizations/templates/authorizations/new_authorization.html:390
msgid "https://servicepublic.brussels/politique-de-confidentialite/" msgid "https://servicepublic.brussels/politique-de-confidentialite/"
msgstr "https://servicepublic.brussels/en/privacy-policy/" msgstr "https://servicepublic.brussels/en/privacy-policy/"
#: authorizations/templates/authorizations/new_authorization.html:391 #: streetup/authorizations/templates/authorizations/new_authorization.html:391
msgid "Politique de confidentialité" msgid "Politique de confidentialité"
msgstr "Privacy policy" msgstr "Privacy policy"
#: authorizations/templates/authorizations/new_authorization.html:401 #: streetup/authorizations/templates/authorizations/new_authorization.html:401
msgid "" msgid ""
"Veillez à joindre en annexe l'attestation d'assurance couvrant tous les " "Veillez à joindre en annexe l'attestation d'assurance couvrant tous les "
"risques." "risques."
msgstr "Please attach the insurance certificate covering all risks." msgstr "Please attach the insurance certificate covering all risks."
#: authorizations/templates/authorizations/new_authorization.html:402 #: streetup/authorizations/templates/authorizations/new_authorization.html:402
msgid "Veillez à joindre en annexe l'autorisation de la commune." msgid "Veillez à joindre en annexe l'autorisation de la commune."
msgstr "Please attach the municipal authorisation." msgstr "Please attach the municipal authorisation."
#: authorizations/templates/authorizations/new_authorization.html:486 #: streetup/authorizations/templates/authorizations/new_authorization.html:486
#: authorizations/templates/authorizations/new_authorization.html:535 #: streetup/authorizations/templates/authorizations/new_authorization.html:535
msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner."
msgstr "Polygon saved. Click « Draw » to redraw it." msgstr "Polygon saved. Click « Draw » to redraw it."
#: authorizations/templates/authorizations/new_authorization.html:506 #: streetup/authorizations/templates/authorizations/new_authorization.html:506
msgid "Cliquer pour fermer le polygone" msgid "Cliquer pour fermer le polygone"
msgstr "Click to close the polygon" msgstr "Click to close the polygon"
#: authorizations/templates/authorizations/new_authorization.html:550 #: streetup/authorizations/templates/authorizations/new_authorization.html:550
msgid "" msgid ""
"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " "Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter "
"des sommets." "des sommets."
msgstr "Click « Draw a polygon » then click on the map to add vertices." msgstr "Click « Draw a polygon » then click on the map to add vertices."
#: authorizations/templates/authorizations/new_authorization.html:573 #: streetup/authorizations/templates/authorizations/new_authorization.html:573
msgid "" msgid ""
"Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet " "Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet "
"pour fermer le polygone." "pour fermer le polygone."
msgstr "" msgstr ""
#: authorizations/views.py:320 authorizations/views.py:369 #: streetup/authorizations/views.py:323 streetup/authorizations/views.py:372
msgid "Vous n'avez pas accès à cette vue." msgid "Vous n'avez pas accès à cette vue."
msgstr "" msgstr ""
#: authorizations/views.py:386 #: streetup/authorizations/views.py:389
msgid "La demande a été mise à jour avec succès." msgid "La demande a été mise à jour avec succès."
msgstr "" msgstr ""
#: authorizations/views.py:403 #: streetup/authorizations/views.py:406
msgid "Le document a été ajouté avec succès." msgid "Le document a été ajouté avec succès."
msgstr "" msgstr ""
#: authorizations/views.py:407 #: streetup/authorizations/views.py:410
msgid "Veuillez fournir une description et un fichier." msgid "Veuillez fournir une description et un fichier."
msgstr "" msgstr ""

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 11:25+0200\n" "POT-Creation-Date: 2026-07-27 09:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -294,8 +294,6 @@ msgid "Documents joints"
msgstr "Documents joints" msgstr "Documents joints"
#: streetup/authorizations/templates/authorizations/authorization_detail.html:167 #: streetup/authorizations/templates/authorizations/authorization_detail.html:167
#, fuzzy
#| msgid "Documents joints"
msgid "Aucun document joint." msgid "Aucun document joint."
msgstr "Documents joints" msgstr "Documents joints"
@ -304,14 +302,10 @@ msgid "Ex: Arrêté de police"
msgstr "" msgstr ""
#: streetup/authorizations/templates/authorizations/authorization_detail.html:183 #: streetup/authorizations/templates/authorizations/authorization_detail.html:183
#, fuzzy
#| msgid "Ajouter une date"
msgid "Ajouter" msgid "Ajouter"
msgstr "Ajouter une date" msgstr "Ajouter une date"
#: streetup/authorizations/templates/authorizations/authorization_detail.html:194 #: streetup/authorizations/templates/authorizations/authorization_detail.html:194
#, fuzzy
#| msgid "Description de la demande"
msgid "Traitement de la demande" msgid "Traitement de la demande"
msgstr "Description de la demande" msgstr "Description de la demande"
@ -359,8 +353,6 @@ msgid "Tous les types"
msgstr "" msgstr ""
#: streetup/authorizations/templates/authorizations/authorization_list.html:89 #: streetup/authorizations/templates/authorizations/authorization_list.html:89
#, fuzzy
#| msgid "Recherche"
msgid "Rechercher un type..." msgid "Rechercher un type..."
msgstr "Recherche" msgstr "Recherche"
@ -719,19 +711,19 @@ msgstr ""
"Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet " "Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet "
"pour fermer le polygone." "pour fermer le polygone."
#: streetup/authorizations/views.py:320 streetup/authorizations/views.py:369 #: streetup/authorizations/views.py:323 streetup/authorizations/views.py:372
msgid "Vous n'avez pas accès à cette vue." msgid "Vous n'avez pas accès à cette vue."
msgstr "" msgstr ""
#: streetup/authorizations/views.py:386 #: streetup/authorizations/views.py:389
msgid "La demande a été mise à jour avec succès." msgid "La demande a été mise à jour avec succès."
msgstr "" msgstr ""
#: streetup/authorizations/views.py:403 #: streetup/authorizations/views.py:406
msgid "Le document a été ajouté avec succès." msgid "Le document a été ajouté avec succès."
msgstr "" msgstr ""
#: streetup/authorizations/views.py:407 #: streetup/authorizations/views.py:410
msgid "Veuillez fournir une description et un fichier." msgid "Veuillez fournir une description et un fichier."
msgstr "" msgstr ""

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 11:25+0200\n" "POT-Creation-Date: 2026-07-27 09:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -294,8 +294,6 @@ msgid "Documents joints"
msgstr "Bijgevoegde documenten" msgstr "Bijgevoegde documenten"
#: streetup/authorizations/templates/authorizations/authorization_detail.html:167 #: streetup/authorizations/templates/authorizations/authorization_detail.html:167
#, fuzzy
#| msgid "Documents joints"
msgid "Aucun document joint." msgid "Aucun document joint."
msgstr "Bijgevoegde documenten" msgstr "Bijgevoegde documenten"
@ -304,14 +302,10 @@ msgid "Ex: Arrêté de police"
msgstr "" msgstr ""
#: streetup/authorizations/templates/authorizations/authorization_detail.html:183 #: streetup/authorizations/templates/authorizations/authorization_detail.html:183
#, fuzzy
#| msgid "Ajouter une date"
msgid "Ajouter" msgid "Ajouter"
msgstr "Een datum toevoegen" msgstr "Een datum toevoegen"
#: streetup/authorizations/templates/authorizations/authorization_detail.html:194 #: streetup/authorizations/templates/authorizations/authorization_detail.html:194
#, fuzzy
#| msgid "Description de la demande"
msgid "Traitement de la demande" msgid "Traitement de la demande"
msgstr "Beschrijving van de aanvraag" msgstr "Beschrijving van de aanvraag"
@ -359,8 +353,6 @@ msgid "Tous les types"
msgstr "" msgstr ""
#: streetup/authorizations/templates/authorizations/authorization_list.html:89 #: streetup/authorizations/templates/authorizations/authorization_list.html:89
#, fuzzy
#| msgid "Recherche"
msgid "Rechercher un type..." msgid "Rechercher un type..."
msgstr "Zoeken" msgstr "Zoeken"
@ -711,19 +703,19 @@ msgid ""
"pour fermer le polygone." "pour fermer le polygone."
msgstr "" msgstr ""
#: streetup/authorizations/views.py:320 streetup/authorizations/views.py:369 #: streetup/authorizations/views.py:323 streetup/authorizations/views.py:372
msgid "Vous n'avez pas accès à cette vue." msgid "Vous n'avez pas accès à cette vue."
msgstr "" msgstr ""
#: streetup/authorizations/views.py:386 #: streetup/authorizations/views.py:389
msgid "La demande a été mise à jour avec succès." msgid "La demande a été mise à jour avec succès."
msgstr "" msgstr ""
#: streetup/authorizations/views.py:403 #: streetup/authorizations/views.py:406
msgid "Le document a été ajouté avec succès." msgid "Le document a été ajouté avec succès."
msgstr "" msgstr ""
#: streetup/authorizations/views.py:407 #: streetup/authorizations/views.py:410
msgid "Veuillez fournir une description et un fichier." msgid "Veuillez fournir une description et un fichier."
msgstr "" msgstr ""

View file

@ -0,0 +1,17 @@
# Generated by Django 6.0.7 on 2026-07-29 06:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authorizations', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='roadauthorization',
options={'ordering': ['status', '-code'], 'verbose_name': "Autorisation d'occupation de l'espace public", 'verbose_name_plural': "Autorisations d'occupation de l'espace public"},
),
]

View file

@ -104,8 +104,8 @@ class RoadAuthorization(models.Model):
class Meta: class Meta:
ordering = ['status', '-code'] ordering = ['status', '-code']
verbose_name = _('Autorisation de voirie') verbose_name = _("Autorisation d'occupation de l'espace public")
verbose_name_plural = _('Autorisations de voirie') verbose_name_plural = _("Autorisations d'occupation de l'espace public")
# Maps AUTH_TYPE values to their 2-digit code prefix # Maps AUTH_TYPE values to their 2-digit code prefix
TYPE_PREFIX = { TYPE_PREFIX = {

View file

@ -52,7 +52,7 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h4 mb-0"> <h1 class="h4 mb-0">
<i class="bi bi-file-earmark-text me-2"></i>{% translate "Demandes d'autorisation de voirie" %} <i class="bi bi-file-earmark-text me-2"></i>{% translate "Demandes d'autorisation d'occupation de l'espace public" %}
</h1> </h1>
<a href="{% url 'authorizations:new_authorization' %}" class="btn btn-sm btn-outline-primary" target="_blank" rel="noopener"> <a href="{% url 'authorizations:new_authorization' %}" class="btn btn-sm btn-outline-primary" target="_blank" rel="noopener">
<i class="bi bi-box-arrow-up-right"></i> {% translate "Formulaire public" %} <i class="bi bi-box-arrow-up-right"></i> {% translate "Formulaire public" %}

View file

@ -6,7 +6,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Demande enregistrée" %} – Brussels Mobility</title> <title>{% translate "Demande enregistrée" %}{% if organization_name %} – {{ organization_name }}{% endif %}</title>
<link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet">
<style> <style>
@ -22,10 +22,10 @@
<body> <body>
<header class="page-header mb-5"> <header class="page-header mb-5">
<div class="container d-flex align-items-center gap-3"> <div class="container d-flex align-items-center gap-3">
<img src="{% static 'common/android-192.png' %}" alt="Brussels Mobility"> <img src="{% static organization_logo_header %}" alt="{{ organization_name }}">
<div> <div>
<div class="fw-bold fs-5">Brussels Mobility</div> <div class="fw-bold fs-5">{{ organization_name }}</div>
<div class="fs-6 opacity-75">{% translate "Demande d'occupation de voirie régionale" %}</div> <div class="fs-6 opacity-75">{% translate "Demande d'occupation de l'espace public" %}</div>
</div> </div>
</div> </div>
</header> </header>
@ -47,13 +47,13 @@
<p class="text-center text-muted"> <p class="text-center text-muted">
{% if just_verified %} {% if just_verified %}
{% blocktranslate trimmed %} {% blocktranslate trimmed %}
Votre adresse email a été confirmée et votre demande d'autorisation d'occupation de voirie régionale a bien été enregistrée. Votre adresse email a été confirmée et votre demande d'autorisation d'occupation de l'espace public a bien été enregistrée.
Un accusé de réception vous a été envoyé par email. Un accusé de réception vous a été envoyé par email.
Nos services prendront contact avec vous dans les meilleurs délais. Nos services prendront contact avec vous dans les meilleurs délais.
{% endblocktranslate %} {% endblocktranslate %}
{% else %} {% else %}
{% blocktranslate trimmed %} {% blocktranslate trimmed %}
Votre demande d'autorisation d'occupation de voirie régionale a bien été enregistrée. Votre demande d'autorisation d'occupation de l'espace public a bien été enregistrée.
Un accusé de réception vous a été envoyé par email. Un accusé de réception vous a été envoyé par email.
Nos services prendront contact avec vous dans les meilleurs délais. Nos services prendront contact avec vous dans les meilleurs délais.
{% endblocktranslate %} {% endblocktranslate %}

View file

@ -6,7 +6,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Lien invalide" %} – Brussels Mobility</title> <title>{% translate "Lien invalide" %}{% if organization_name %} – {{ organization_name }}{% endif %}</title>
<link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet">
<style> <style>
@ -22,10 +22,10 @@
<body> <body>
<header class="page-header mb-5"> <header class="page-header mb-5">
<div class="container d-flex align-items-center gap-3"> <div class="container d-flex align-items-center gap-3">
<img src="{% static 'common/android-192.png' %}" alt="Brussels Mobility"> <img src="{% static organization_logo_header %}" alt="{{ organization_name }}">
<div> <div>
<div class="fw-bold fs-5">Brussels Mobility</div> <div class="fw-bold fs-5">{{ organization_name }}</div>
<div class="fs-6 opacity-75">{% translate "Demande d'occupation de voirie régionale" %}</div> <div class="fs-6 opacity-75">{% translate "Demande d'occupation de l'espace public" %}</div>
</div> </div>
</div> </div>
</header> </header>

View file

@ -6,7 +6,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Confirmation de votre email" %} – Brussels Mobility</title> <title>{% translate "Confirmation de votre email" %}{% if organization_name %} – {{ organization_name }}{% endif %}</title>
<link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet">
<style> <style>
@ -22,10 +22,10 @@
<body> <body>
<header class="page-header mb-5"> <header class="page-header mb-5">
<div class="container d-flex align-items-center gap-3"> <div class="container d-flex align-items-center gap-3">
<img src="{% static 'common/android-192.png' %}" alt="Brussels Mobility"> <img src="{% static organization_logo_header %}" alt="{{ organization_name }}">
<div> <div>
<div class="fw-bold fs-5">Brussels Mobility</div> <div class="fw-bold fs-5">{{ organization_name }}</div>
<div class="fs-6 opacity-75">{% translate "Demande d'occupation de voirie régionale" %}</div> <div class="fs-6 opacity-75">{% translate "Demande d'occupation de l'espace public" %}</div>
</div> </div>
</div> </div>
</header> </header>

View file

@ -6,7 +6,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Vérification de votre email" %} – Brussels Mobility</title> <title>{% translate "Vérification de votre email" %}{% if organization_name %} – {{ organization_name }}{% endif %}</title>
<link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap-icons.min.css' %}" rel="stylesheet">
<style> <style>
@ -29,10 +29,10 @@
<body> <body>
<header class="page-header mb-5"> <header class="page-header mb-5">
<div class="container d-flex align-items-center gap-3"> <div class="container d-flex align-items-center gap-3">
<img src="{% static 'common/android-192.png' %}" alt="Brussels Mobility"> <img src="{% static organization_logo_header %}" alt="{{ organization_name }}">
<div> <div>
<div class="fw-bold fs-5">Brussels Mobility</div> <div class="fw-bold fs-5">{{ organization_name }}</div>
<div class="fs-6 opacity-75">{% translate "Demande d'occupation de voirie régionale" %}</div> <div class="fs-6 opacity-75">{% translate "Demande d'occupation de l'espace public" %}</div>
</div> </div>
</div> </div>
</header> </header>

View file

@ -1,7 +1,7 @@
Madame, Monsieur, Madame, Monsieur,
Nous avons bien reçu votre demande d'autorisation d'occupation de la voirie. Nous avons bien reçu votre demande d'autorisation d'occupation de l'espace public.
{% load settings_tags %}Votre dossier est traité par la Direction Gestion et Inspection de {% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %}. {% load settings_tags %}Votre dossier est traité par {% get_setting "ORGANIZATION_NAME_FR" "l'Administration" %}.
Numéro de référence : {{ authorization.code }} Numéro de référence : {{ authorization.code }}
@ -37,15 +37,10 @@ Vous trouverez ci-dessous une copie des informations que vous avez transmises vi
• Cet e-mail est un accusé de réception. • Cet e-mail est un accusé de réception.
• Il ne vaut pas autorisation. • Il ne vaut pas autorisation.
• Vous ne pouvez pas commencer l'occupation de la voirie tant que vous n'avez pas reçu une décision officielle. • Vous ne pouvez pas commencer l'occupation tant que vous n'avez pas reçu une décision officielle.
📧 Contact
Pour toute question, vous pouvez nous écrire à :
{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %}
Cordialement, Cordialement,
{% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %} {% get_setting "ORGANIZATION_NAME_FR" "l'Administration" %}
Direction Gestion et Inspection Service des Autorisations – Occupation de l'espace public
Autorisations – Occupation de la voirie

View file

@ -1,8 +1,8 @@
{% autoescape off %} {% autoescape off %}
Dear {{ authorization.applicant_name }}, Dear {{ authorization.applicant_name }},
We have received your application for authorisation to use the public road. We have received your application for authorisation to use the public space.
{% load settings_tags %}Your application is handled by the Management and Inspection Department of {% get_setting "ORGANIZATION_NAME" "Brussels Mobility" %}. {% load settings_tags %}Your application is handled by {% get_setting "ORGANIZATION_NAME" "the Administration" %}.
Reference number: {{ authorization.code }} Reference number: {{ authorization.code }}
@ -38,16 +38,10 @@ You will find below a copy of the information you submitted via the online form.
• This e-mail is an acknowledgement of receipt. • This e-mail is an acknowledgement of receipt.
• It does not constitute an authorisation. • It does not constitute an authorisation.
• You may not begin using the public road until you have received an official decision. • You may not begin using the public space until you have received an official decision.
📧 Contact
For any questions, please contact us at:
{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %}
Yours sincerely, Yours sincerely,
{% get_setting "ORGANIZATION_NAME" "Brussels Mobility" %} {% get_setting "ORGANIZATION_NAME" "the Administration" %}
Management and Inspection Department Authorisations Department – Use of public space
Authorisations – Use of public road
{% endautoescape %} {% endautoescape %}

View file

@ -1,7 +1,7 @@
{% autoescape off %} {% autoescape off %}
Dear {{ applicant_name }}, Dear {{ applicant_name }},
Thank you for your application for authorisation to use the public road. Thank you for your application for authorisation to use the public space.
To finalise your application, please enter the verification code below on the confirmation page: To finalise your application, please enter the verification code below on the confirmation page:
@ -15,6 +15,6 @@ Confirmation page:
If you did not submit this application, you can ignore this message. If you did not submit this application, you can ignore this message.
--- ---
Brussels Mobility {% load settings_tags %}{% get_setting "ORGANIZATION_NAME" "the Administration" %}
Management and Inspection Department – Road authorisations Authorisations Department – Use of public space
{% endautoescape %} {% endautoescape %}

View file

@ -1,8 +1,8 @@
{% autoescape off %} {% autoescape off %}
Madame, Monsieur, Madame, Monsieur,
Nous avons bien reçu votre demande d'autorisation d'occupation de la voirie. Nous avons bien reçu votre demande d'autorisation d'occupation de l'espace public.
{% load settings_tags %}Votre dossier est traité par la Direction Gestion et Inspection de {% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %}. {% load settings_tags %}Votre dossier est traité par {% get_setting "ORGANIZATION_NAME_FR" "l'Administration" %}.
Numéro de référence : {{ authorization.code }} Numéro de référence : {{ authorization.code }}
@ -38,16 +38,10 @@ Vous trouverez ci-dessous une copie des informations que vous avez transmises vi
• Cet e-mail est un accusé de réception. • Cet e-mail est un accusé de réception.
• Il ne vaut pas autorisation. • Il ne vaut pas autorisation.
• Vous ne pouvez pas commencer l'occupation de la voirie tant que vous n'avez pas reçu une décision officielle. • Vous ne pouvez pas commencer l'occupation tant que vous n'avez pas reçu une décision officielle.
📧 Contact
Pour toute question, vous pouvez nous écrire à :
{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %}
Cordialement, Cordialement,
{% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %} {% get_setting "ORGANIZATION_NAME_FR" "l'Administration" %}
Direction Gestion et Inspection Service des Autorisations – Occupation de l'espace public
Autorisations – Occupation de la voirie
{% endautoescape %} {% endautoescape %}

View file

@ -1,7 +1,7 @@
{% autoescape off %} {% autoescape off %}
Bonjour {{ applicant_name }}, Bonjour {{ applicant_name }},
Merci pour votre demande d'autorisation d'occupation de voirie régionale. Merci pour votre demande d'autorisation d'occupation de l'espace public.
Pour finaliser votre demande, saisissez le code de vérification ci-dessous sur la page de confirmation : Pour finaliser votre demande, saisissez le code de vérification ci-dessous sur la page de confirmation :
@ -15,6 +15,6 @@ Page de confirmation :
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message. Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.
--- ---
Brussels Mobility {% load settings_tags %}{% get_setting "ORGANIZATION_NAME_FR" "l'Administration" %}
Direction Infrastructure Routière – Autorisations de voirie Service des Autorisations – Occupation de l'espace public
{% endautoescape %} {% endautoescape %}

View file

@ -1,8 +1,8 @@
{% autoescape off %} {% autoescape off %}
Geachte {{ authorization.applicant_name }}, Geachte {{ authorization.applicant_name }},
Wij hebben uw aanvraag voor een vergunning tot inname van de openbare weg goed ontvangen. Wij hebben uw aanvraag voor een vergunning tot inname van het openbaar domein goed ontvangen.
{% load settings_tags %}Uw dossier wordt behandeld door de Directie Beheer en Inspectie van {% get_setting "ORGANIZATION_NAME_NL" "Brussel Mobiliteit" %}. {% load settings_tags %}Uw dossier wordt behandeld door {% get_setting "ORGANIZATION_NAME_NL" "de Administratie" %}.
Referentienummer : {{ authorization.code }} Referentienummer : {{ authorization.code }}
@ -38,16 +38,10 @@ U vindt hieronder een kopie van de informatie die u via het online formulier hee
• Dit e-mailbericht is een ontvangstbewijs. • Dit e-mailbericht is een ontvangstbewijs.
• Het geldt niet als vergunning. • Het geldt niet als vergunning.
• U mag de openbare weg niet innemen zolang u geen officiële beslissing heeft ontvangen. • U mag het openbaar domein niet innemen zolang u geen officiële beslissing heeft ontvangen.
📧 Contact
Voor vragen kunt u ons bereiken op :
{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %}
Met vriendelijke groeten, Met vriendelijke groeten,
{% get_setting "ORGANIZATION_NAME_NL" "Brussel Mobiliteit" %} {% get_setting "ORGANIZATION_NAME_NL" "de Administratie" %}
Directie Beheer en Inspectie Dienst Vergunningen – Inname van het openbaar domein
Vergunningen – Inname van de openbare weg
{% endautoescape %} {% endautoescape %}

View file

@ -1,7 +1,7 @@
{% autoescape off %} {% autoescape off %}
Geachte {{ applicant_name }}, Geachte {{ applicant_name }},
Bedankt voor uw aanvraag voor een vergunning tot inname van de openbare weg. Bedankt voor uw aanvraag voor een vergunning tot inname van het openbaar domein.
Om uw aanvraag te voltooien, voert u de verificatiecode hieronder in op de bevestigingspagina : Om uw aanvraag te voltooien, voert u de verificatiecode hieronder in op de bevestigingspagina :
@ -15,6 +15,6 @@ Bevestigingspagina :
Als u deze aanvraag niet heeft ingediend, kunt u dit bericht negeren. Als u deze aanvraag niet heeft ingediend, kunt u dit bericht negeren.
--- ---
Brussel Mobiliteit {% load settings_tags %}{% get_setting "ORGANIZATION_NAME_NL" "de Administratie" %}
Directie Beheer en Inspectie – Vergunningen openbare weg Dienst Vergunningen – Inname van het openbaar domein
{% endautoescape %} {% endautoescape %}

View file

@ -1,6 +1,6 @@
Bonjour {{ applicant_name }}, Bonjour {{ applicant_name }},
Merci pour votre demande d'autorisation d'occupation de voirie régionale. Merci pour votre demande d'autorisation d'occupation de l'espace public.
Pour finaliser votre demande, saisissez le code de vérification ci-dessous sur la page de confirmation : Pour finaliser votre demande, saisissez le code de vérification ci-dessous sur la page de confirmation :
@ -12,5 +12,6 @@ Ce code est valable {{ validity_minutes }} minutes à compter de la réception d
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message. Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.
--- ---
Brussels Mobility {% load settings_tags %}{% get_setting "ORGANIZATION_NAME_FR" "l'Administration" %}
Direction Infrastructure Routière – Autorisations de voirie Service des Autorisations – Occupation de l'espace public

View file

@ -6,7 +6,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Demande d'occupation de voirie régionale" %} – Brussels Mobility</title> <title>{% translate "Demande d'occupation de l'espace public" %}{% if organization_name %} – {{ organization_name }}{% endif %}</title>
<!-- Bootstrap 5.3 --> <!-- Bootstrap 5.3 -->
<link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'common/bootstrap.min.css' %}" rel="stylesheet">
@ -93,10 +93,10 @@
<!-- Header --> <!-- Header -->
<header class="page-header mb-4"> <header class="page-header mb-4">
<div class="container d-flex align-items-center gap-3"> <div class="container d-flex align-items-center gap-3">
<img src="{% static 'common/android-192.png' %}" alt="{% translate "Brussels Mobility" %}"> <img src="{% static organization_logo_header %}" alt="{{ organization_name }}">
<div> <div>
<div class="fw-bold fs-5">{% translate "Brussels Mobility" %}</div> <div class="fw-bold fs-5">{{ organization_name }}</div>
<div class="fs-6 opacity-75">{% translate "Demande d'occupation de voirie régionale" %}</div> <div class="fs-6 opacity-75">{% translate "Demande d'occupation de l'espace public" %}</div>
</div> </div>
<div class="ms-auto d-flex align-items-center gap-1" id="lang-switcher"> <div class="ms-auto d-flex align-items-center gap-1" id="lang-switcher">
<i class="bi bi-globe2 me-1" style="color:rgba(255,255,255,0.6);font-size:1.05rem;"></i> <i class="bi bi-globe2 me-1" style="color:rgba(255,255,255,0.6);font-size:1.05rem;"></i>
@ -114,31 +114,17 @@
<div class="card-body"> <div class="card-body">
<p class="mb-2"> <p class="mb-2">
{% blocktranslate trimmed %} {% blocktranslate trimmed %}
Toute occupation de l'espace public doit faire l'objet d'une autorisation de la part du gestionnaire de voiries. Toute occupation de l'espace public (terrasse, affichage, événement, mobilier, etc.) doit faire l'objet d'une autorisation préalable de la part de l'administration.
En Région de Bruxelles-Capitale, les axes principaux sont gérés par la Région
(<a href="https://mobilite-mobiliteit.brussels/fr/qui-sommes-nous" target="_blank" rel="noopener">Bruxelles Mobilité</a>),
tandis que les autres voiries sont gérées par les communes.
Les voiries régionales sont représentées en <strong style="color:#cc0000;">rouge</strong> sur la carte ci-dessous.
{% endblocktranslate %} {% endblocktranslate %}
</p> </p>
<p class="mb-2"> <p class="mb-2">
{% blocktranslate trimmed %} {% blocktranslate trimmed %}
Ce formulaire ne concerne que les autorisations sur <strong>voiries régionales</strong>. Veuillez compléter ce formulaire et délimiter la zone concernée sur la carte ci-dessous ainsi que la période souhaitée.
Veuillez vous adresser à votre commune pour les autres autorisations ou documents.
{% endblocktranslate %} {% endblocktranslate %}
</p> </p>
<p class="mb-0 fst-italic text-muted"> <p class="mb-0 text-muted">
{% blocktranslate trimmed %} {% blocktranslate trimmed %}
Pour les demandes liées à des chantiers, veuillez utiliser le système Si vous éprouvez des difficultés ou avez des questions, vous pouvez contacter nos services.
<a href="https://my.osiris.brussels/" target="_blank" rel="noopener">Osiris</a>.
Les demandes pour le placement de conteneurs, d'échafaudages ou les déménagements se font également via ce système.
{% endblocktranslate %}
</p>
<p class="mb-0 mt-2 text-muted">
{% blocktranslate trimmed %}
Si vous éprouvez des difficultés, contactez-nous par mail
(<a href="mailto:mobilite@sprb.brussels">mobilite@sprb.brussels</a>)
ou par téléphone (<a href="tel:+3280094001">0800 94 001</a>).
{% endblocktranslate %} {% endblocktranslate %}
</p> </p>
</div> </div>
@ -386,10 +372,8 @@
</main> </main>
<footer class="mt-5"> <footer class="mt-5">
<div class="container text-center"> <div class="container text-center text-muted">
<a href="{% translate 'https://servicepublic.brussels/politique-de-confidentialite/' %}" target="_blank" rel="noopener"> {% translate "Demande d'occupation de l'espace public" %}
{% translate "Politique de confidentialité" %}
</a>
</div> </div>
</footer> </footer>
@ -404,6 +388,7 @@
<script> <script>
window.MAP_STYLE_LIGHT = "{{ MAP_STYLE_LIGHT|escapejs }}"; window.MAP_STYLE_LIGHT = "{{ MAP_STYLE_LIGHT|escapejs }}";
window.MAP_STYLE_STANDARD = "{{ MAP_STYLE_STANDARD|escapejs }}"; window.MAP_STYLE_STANDARD = "{{ MAP_STYLE_STANDARD|escapejs }}";
window.MAP_OVERLAY_WMS_URL = "{{ map_overlay_wms_url|escapejs }}";
(function () { (function () {
'use strict'; 'use strict';
@ -413,7 +398,6 @@
let map, drawing = false, vertices = [], markers = []; let map, drawing = false, vertices = [], markers = [];
const MAPTILER_STYLE = window.MAP_STYLE_LIGHT; const MAPTILER_STYLE = window.MAP_STYLE_LIGHT;
const REGIONAL_ROADS_WMS = 'https://data.mobility.brussels/geoserver/bm_network/wms?';
function initMap() { function initMap() {
map = new maplibregl.Map({ map = new maplibregl.Map({
@ -426,23 +410,25 @@
map.addControl(new maplibregl.NavigationControl(), 'top-left'); map.addControl(new maplibregl.NavigationControl(), 'top-left');
map.on('load', function () { map.on('load', function () {
// Regional roads overlay (Bruxelles Mobilité) // Optional overlay WMS layer (e.g., regional roads if configured via MAP_OVERLAY_WMS_URL)
map.addSource('regional-roads', { if (window.MAP_OVERLAY_WMS_URL) {
type: 'raster', const wmsUrl = window.MAP_OVERLAY_WMS_URL +
tiles: [ (window.MAP_OVERLAY_WMS_URL.includes('?') ? '&' : '?') +
REGIONAL_ROADS_WMS +
'SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap' + 'SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap' +
'&LAYERS=Regional_roads&FORMAT=image/png&TRANSPARENT=true' + '&LAYERS=Regional_roads&FORMAT=image/png&TRANSPARENT=true' +
'&SRS=EPSG:3857&WIDTH=256&HEIGHT=256&BBOX={bbox-epsg-3857}' '&SRS=EPSG:3857&WIDTH=256&HEIGHT=256&BBOX={bbox-epsg-3857}';
], map.addSource('overlay-wms', {
type: 'raster',
tiles: [wmsUrl],
tileSize: 256, tileSize: 256,
}); });
map.addLayer({ map.addLayer({
id: 'regional-roads-layer', id: 'overlay-wms-layer',
type: 'raster', type: 'raster',
source: 'regional-roads', source: 'overlay-wms',
paint: { 'raster-opacity': 0.5 }, paint: { 'raster-opacity': 0.5 },
}); });
}
// Polygon preview sources // Polygon preview sources
map.addSource('poly-preview', { map.addSource('poly-preview', {

View file

@ -75,12 +75,12 @@ _VERIFY_SUBJECTS = {
} }
_ACK_SUBJECTS = { _ACK_SUBJECTS = {
'fr': "{code} – Accusé de réception de votre demande – Occupation de voirie régionale", 'fr': "{code} – Accusé de réception de votre demande – Occupation de l'espace public",
'nl': "{code} – Ontvangstbewijs van uw aanvraag – Inname van het openbaar domein", 'nl': "{code} – Ontvangstbewijs van uw aanvraag – Inname van het openbaar domein",
'en': "{code} – Acknowledgement of your application – Use of public road", 'en': "{code} – Acknowledgement of your application – Use of public space",
} }
_BCC_ADDRESS = getattr(settings, 'EMAIL_AUTHORIZATIONS_BCC', 'autorisation.voiries@sprb.brussels') _BCC_ADDRESS = getattr(settings, 'EMAIL_AUTHORIZATIONS_BCC', '')
def _send_verification_email(request, authorization, token_obj): def _send_verification_email(request, authorization, token_obj):
@ -95,8 +95,8 @@ def _send_verification_email(request, authorization, token_obj):
'verify_page_url': verify_page_url, 'verify_page_url': verify_page_url,
'validity_minutes': EmailVerificationToken.TOKEN_VALIDITY_MINUTES, 'validity_minutes': EmailVerificationToken.TOKEN_VALIDITY_MINUTES,
} }
org_name = getattr(settings, 'ORGANIZATION_NAME', 'Brussels Mobility') org_name = getattr(settings, 'ORGANIZATION_NAME', 'StreetUp')
org_name_nl = getattr(settings, 'ORGANIZATION_NAME_NL', 'Brussel Mobiliteit') org_name_nl = getattr(settings, 'ORGANIZATION_NAME_NL', 'StreetUp')
org = org_name_nl if lang == 'nl' else org_name org = org_name_nl if lang == 'nl' else org_name
subject = _VERIFY_SUBJECTS.get(lang, _VERIFY_SUBJECTS['fr']).format(code=authorization.code, org=org) subject = _VERIFY_SUBJECTS.get(lang, _VERIFY_SUBJECTS['fr']).format(code=authorization.code, org=org)
body = render_to_string(f'authorizations/emails/{lang}/verify_email.txt', context) body = render_to_string(f'authorizations/emails/{lang}/verify_email.txt', context)
@ -129,7 +129,7 @@ def _send_acknowledgment_email(request, authorization):
recipients = _resolve_recipients([authorization.applicant_email]) recipients = _resolve_recipients([authorization.applicant_email])
# BCC uniquement en production (pas en mode override) # BCC uniquement en production (pas en mode override)
override = getattr(settings, 'EMAIL_OVERRIDE', None) override = getattr(settings, 'EMAIL_OVERRIDE', None)
bcc = [] if override else [_BCC_ADDRESS] bcc = [] if override else ([_BCC_ADDRESS] if _BCC_ADDRESS else [])
if not _is_smtp_available(): if not _is_smtp_available():
logger.warning("SMTP non disponible – accusé de réception non envoyé pour %s", authorization.code) logger.warning("SMTP non disponible – accusé de réception non envoyé pour %s", authorization.code)
return return
@ -234,7 +234,10 @@ def new_authorization(request):
if lang_for_render in ('fr', 'nl', 'en'): if lang_for_render in ('fr', 'nl', 'en'):
translation.activate(lang_for_render) translation.activate(lang_for_render)
return render(request, 'authorizations/new_authorization.html', { return render(request, [
'authorizations/new_authorization.html',
'authorizations/new_authorization_default.html',
], {
'form': form, 'form': form,
'doc_formset': doc_formset, 'doc_formset': doc_formset,
'date_formset': date_formset, 'date_formset': date_formset,

1
streetup/bim/__init__.py Normal file
View file

@ -0,0 +1 @@
"""BIM (Building Information Modeling) application for StreetUp."""

8
streetup/bim/apps.py Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class BimConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "bim"
verbose_name = _("BIM & IFC Viewer")

View file

@ -0,0 +1 @@
# Management package for bim app

View file

@ -0,0 +1 @@
# Commands package

View file

@ -0,0 +1,82 @@
import os
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.core.files import File
from documents.models import ManagedDocument, DocumentVersion, DocumentShare, DocumentFolder
User = get_user_model()
class Command(BaseCommand):
help = "Crée automatiquement un document IFC d'exemple dans la gestion documentaire et l'attache à un dossier partagé."
def handle(self, *args, **options):
users = User.objects.filter(is_active=True)
if not users.exists():
self.stderr.write(self.style.ERROR("Aucun utilisateur actif disponible."))
return
creator = users.filter(is_superuser=True).first() or users.first()
samples_dir = os.path.join(os.path.dirname(__file__), "..", "..", "samples")
sample_file_path = os.path.join(samples_dir, "Duplex_Architecture.ifc")
if not os.path.exists(sample_file_path):
self.stderr.write(self.style.ERROR(f"Fichier d'exemple non trouvé à l'emplacement : {sample_file_path}"))
return
# Ensure a root folder "Maquettes 3D (BIM)" exists so the document is visible in folder navigation
folder, _ = DocumentFolder.objects.get_or_create(
slug="maquettes-3d-bim",
defaults={
"name": "Maquettes 3D (BIM)",
"created_by": creator,
}
)
# Share folder with all active users
for user in users:
DocumentFolder.objects.filter(pk=folder.pk).update()
folder.shares.get_or_create(
user=user,
defaults={"permission": ManagedDocument.PERMISSION_VIEW}
)
title = "Duplex_Architecture"
doc, created = ManagedDocument.objects.get_or_create(
title=title,
defaults={
"description": "Exemple de maquette 3D au format IFC pour tests du viewer BIM (openBIM Duplex).",
"created_by": creator,
}
)
# Attach folder to document
doc.folders.add(folder)
if not doc.current_version:
with open(sample_file_path, "rb") as f:
version = DocumentVersion.objects.create(
document=doc,
version_number=1,
file=File(f, name="Duplex_Architecture.ifc"),
uploaded_by=creator,
description="Version initiale d'exemple IFC"
)
doc.current_version = version
doc.save(update_fields=["current_version"])
# Share document with all active users so everyone can find it in Document Management
for user in users:
DocumentShare.objects.get_or_create(
document=doc,
user=user,
defaults={"permission": ManagedDocument.PERMISSION_VIEW}
)
self.stdout.write(
self.style.SUCCESS(
f"Document d'exemple IFC disponible dans le dossier 'Maquettes 3D (BIM)' (UUID: {doc.uuid}) - Partagé avec {users.count()} utilisateurs."
)
)

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,541 @@
/* ==========================================================================
StreetUp BIM & IFC Viewer - Modern Dark Workstation Design
========================================================================== */
:root {
--bim-bg-dark: #0f172a;
--bim-bg-canvas: #1e293b;
--bim-panel-bg: rgba(15, 23, 42, 0.85);
--bim-panel-border: rgba(255, 255, 255, 0.1);
--bim-accent-primary: #3b82f6;
--bim-accent-hover: #2563eb;
--bim-accent-light: #60a5fa;
--bim-highlight: #f59e0b;
--bim-text-main: #f8fafc;
--bim-text-muted: #94a3b8;
--bim-sidebar-width: 320px;
--bim-header-height: 56px;
--bim-toolbar-height: 48px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body.bim-viewer-body {
background-color: var(--bim-bg-dark);
color: var(--bim-text-main);
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
height: 100vh;
width: 100vw;
}
/* App Wrapper */
.bim-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
position: relative;
}
/* Header Navbar */
.bim-header {
height: var(--bim-header-height);
background: var(--bim-panel-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--bim-panel-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.25rem;
z-index: 100;
}
.bim-header-brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.bim-header-title {
font-size: 1.05rem;
font-weight: 600;
color: var(--bim-text-main);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 360px;
}
.bim-badge {
font-size: 0.75rem;
font-weight: 600;
padding: 0.2rem 0.6rem;
border-radius: 9999px;
background: rgba(59, 130, 246, 0.2);
color: var(--bim-accent-light);
border: 1px solid rgba(59, 130, 246, 0.3);
}
.bim-header-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Preset Camera Controls */
.bim-preset-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--bim-panel-border);
color: var(--bim-text-main);
padding: 0.35rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.825rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.bim-preset-btn:hover {
background: rgba(59, 130, 246, 0.25);
border-color: var(--bim-accent-primary);
color: #fff;
}
.bim-preset-btn.active {
background: var(--bim-accent-primary);
border-color: var(--bim-accent-primary);
color: #fff;
}
/* Main Workspace */
.bim-main {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
/* 3D Canvas Container */
.bim-canvas-container {
flex: 1;
height: 100%;
position: relative;
background: radial-gradient(circle at center, #1e293b 0%, #0f172a 100%);
}
#bim-canvas {
width: 100%;
height: 100%;
display: block;
}
/* Sidebars (Left Spatial Tree / Right Property Inspector) */
.bim-sidebar {
width: var(--bim-sidebar-width);
height: 100%;
background: var(--bim-panel-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-color: var(--bim-panel-border);
display: flex;
flex-direction: column;
z-index: 50;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.bim-sidebar-left {
border-right: 1px solid var(--bim-panel-border);
}
.bim-sidebar-right {
border-left: 1px solid var(--bim-panel-border);
}
.bim-sidebar.collapsed-left {
transform: translateX(-100%);
position: absolute;
left: 0;
}
.bim-sidebar.collapsed-right {
transform: translateX(100%);
position: absolute;
right: 0;
}
.bim-sidebar-header {
padding: 0.85rem 1rem;
border-bottom: 1px solid var(--bim-panel-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.bim-sidebar-title {
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--bim-text-muted);
display: flex;
align-items: center;
gap: 0.5rem;
}
.bim-sidebar-content {
flex: 1;
overflow-y: auto;
padding: 0.75rem;
}
/* Custom Scrollbar */
.bim-sidebar-content::-webkit-scrollbar {
width: 6px;
}
.bim-sidebar-content::-webkit-scrollbar-track {
background: transparent;
}
.bim-sidebar-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
}
.bim-sidebar-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Spatial Tree & Layer UI */
.bim-tree-list {
list-style: none;
padding-left: 0;
}
.bim-tree-item {
margin-bottom: 0.25rem;
}
.bim-tree-node {
display: flex;
align-items: center;
padding: 0.4rem 0.5rem;
border-radius: 0.375rem;
cursor: pointer;
transition: background 0.15s ease;
user-select: none;
font-size: 0.85rem;
}
.bim-tree-node:hover {
background: rgba(255, 255, 255, 0.06);
}
.bim-tree-toggle {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 0.35rem;
color: var(--bim-text-muted);
}
.bim-tree-checkbox {
margin-right: 0.5rem;
accent-color: var(--bim-accent-primary);
cursor: pointer;
}
.bim-tree-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bim-tree-count {
font-size: 0.75rem;
color: var(--bim-text-muted);
background: rgba(255, 255, 255, 0.08);
padding: 0.1rem 0.4rem;
border-radius: 9999px;
margin-left: 0.4rem;
}
.bim-tree-children {
list-style: none;
padding-left: 1.25rem;
margin-top: 0.15rem;
}
/* Property Inspector UI */
.bim-prop-group {
margin-bottom: 1rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--bim-panel-border);
border-radius: 0.5rem;
overflow: hidden;
}
.bim-prop-group-title {
background: rgba(255, 255, 255, 0.05);
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
color: var(--bim-accent-light);
border-bottom: 1px solid var(--bim-panel-border);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.bim-prop-table {
width: 100%;
border-collapse: collapse;
font-size: 0.825rem;
}
.bim-prop-table tr {
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.bim-prop-table tr:last-child {
border-bottom: none;
}
.bim-prop-key {
padding: 0.4rem 0.75rem;
color: var(--bim-text-muted);
width: 45%;
font-weight: 500;
vertical-align: top;
}
.bim-prop-val {
padding: 0.4rem 0.75rem;
color: var(--bim-text-main);
word-break: break-word;
}
/* Floating Bottom Toolbar */
.bim-toolbar {
position: absolute;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
background: var(--bim-panel-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--bim-panel-border);
border-radius: 9999px;
padding: 0.4rem 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
z-index: 80;
}
.bim-tool-btn {
background: transparent;
border: none;
color: var(--bim-text-muted);
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.bim-tool-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--bim-text-main);
}
.bim-tool-btn.active {
background: var(--bim-accent-primary);
color: #fff;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.5);
}
.bim-tool-divider {
width: 1px;
height: 24px;
background: var(--bim-panel-border);
}
/* Tooltip */
.bim-tool-btn[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background: #090d16;
color: #fff;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
border: 1px solid var(--bim-panel-border);
}
.bim-tool-btn:hover[data-tooltip]::after {
opacity: 1;
}
/* Loading Overlay */
.bim-loader-overlay {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 200;
transition: opacity 0.4s ease;
}
.bim-loader-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.bim-spinner {
width: 50px;
height: 50px;
border: 3px solid rgba(59, 130, 246, 0.2);
border-radius: 50%;
border-top-color: var(--bim-accent-primary);
animation: bim-spin 1s linear infinite;
margin-bottom: 1.25rem;
}
@keyframes bim-spin {
to { transform: rotate(360deg); }
}
.bim-loader-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.bim-loader-progress-bar {
width: 240px;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 9999px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.bim-loader-progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
transition: width 0.2s ease;
}
.bim-loader-text {
font-size: 0.825rem;
color: var(--bim-text-muted);
}
/* Clipping Controls Popup */
.bim-clipping-panel {
position: absolute;
bottom: 5.25rem;
left: 50%;
transform: translateX(-50%);
background: var(--bim-panel-bg);
backdrop-filter: blur(16px);
border: 1px solid var(--bim-panel-border);
border-radius: 0.75rem;
padding: 1rem;
width: 320px;
z-index: 85;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
display: none;
}
.bim-clipping-panel.active {
display: block;
}
.bim-slider-group {
margin-bottom: 0.75rem;
}
.bim-slider-group:last-child {
margin-bottom: 0;
}
.bim-slider-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--bim-text-muted);
margin-bottom: 0.35rem;
}
.bim-slider {
width: 100%;
accent-color: var(--bim-accent-primary);
}
/* Measurement Banner & HTML Labels */
.bim-measure-banner {
position: absolute;
top: 1rem;
left: 50%;
transform: translateX(-50%);
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(12px);
border: 1px solid var(--bim-accent-primary);
color: var(--bim-text-main);
padding: 0.4rem 1rem;
border-radius: 9999px;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
z-index: 90;
transition: opacity 0.2s ease;
}
.bim-measure-banner.hidden {
display: none !important;
}
.bim-measure-badge {
background: #2563eb;
color: #ffffff;
padding: 0.2rem 0.6rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 700;
border: 1px solid #60a5fa;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
pointer-events: none;
white-space: nowrap;
}

View file

@ -0,0 +1,979 @@
/**
* StreetUp BIM & IFC Viewer
* Powered by Three.js & Web-IFC (That Open Company Engine)
*/
(function () {
'use strict';
// Global App State
const state = {
config: window.BIM_CONFIG || {},
container: null,
canvas: null,
scene: null,
camera: null,
renderer: null,
controls: null,
raycaster: new THREE.Raycaster(),
mouse: new THREE.Vector2(),
// Web-IFC Engine instance
ifcApi: null,
modelID: 0,
ifcData: null,
// Meshes & Spatial Data
meshes: [], // All loaded 3D meshes: [{ expressID, typeName, storeyName, mesh, origMaterial }]
spatialTree: {}, // Storeys & Categories tree structure
expressIDToMesh: new Map(),
selectedMesh: null,
selectedOrigMat: null,
// Viewport bounds
boundingBox: new THREE.Box3(),
center: new THREE.Vector3(),
size: new THREE.Vector3(),
// Clipping Planes
clippingPlanes: {
z: new THREE.Plane(new THREE.Vector3(0, -1, 0), 1000),
x: new THREE.Plane(new THREE.Vector3(-1, 0, 0), 1000),
y: new THREE.Plane(new THREE.Vector3(0, 0, -1), 1000)
},
// Modes
isWireframe: false,
isGhosting: false,
// 3D Measurement System State
isMeasuring: false,
measureStartPoint: null,
measureStartSphere: null,
measureTempLine: null,
measureGroup: new THREE.Group(),
measureBadges: []
};
// Material Palette for IFC Element Types
const IFC_MATERIALS = {
IFCWALL: new THREE.MeshStandardMaterial({ color: 0xe2e8f0, roughness: 0.5, metalness: 0.1, side: THREE.DoubleSide }),
IFCWALLSTANDARDCASE: new THREE.MeshStandardMaterial({ color: 0xe2e8f0, roughness: 0.5, metalness: 0.1, side: THREE.DoubleSide }),
IFCSLAB: new THREE.MeshStandardMaterial({ color: 0x94a3b8, roughness: 0.7, metalness: 0.1, side: THREE.DoubleSide }),
IFCWINDOW: new THREE.MeshStandardMaterial({ color: 0x38bdf8, roughness: 0.1, metalness: 0.8, transparent: true, opacity: 0.5, side: THREE.DoubleSide }),
IFCDOOR: new THREE.MeshStandardMaterial({ color: 0xb45309, roughness: 0.6, metalness: 0.1, side: THREE.DoubleSide }),
IFCCOLUMN: new THREE.MeshStandardMaterial({ color: 0x64748b, roughness: 0.4, metalness: 0.3, side: THREE.DoubleSide }),
IFCBEAM: new THREE.MeshStandardMaterial({ color: 0x475569, roughness: 0.4, metalness: 0.3, side: THREE.DoubleSide }),
IFCROOF: new THREE.MeshStandardMaterial({ color: 0x991b1b, roughness: 0.6, metalness: 0.1, side: THREE.DoubleSide }),
IFCPIPESEGMENT: new THREE.MeshStandardMaterial({ color: 0x0284c7, roughness: 0.2, metalness: 0.6, side: THREE.DoubleSide }),
IFCDUCTSEGMENT: new THREE.MeshStandardMaterial({ color: 0xd97706, roughness: 0.3, metalness: 0.5, side: THREE.DoubleSide }),
IFCFURNISHINGELEMENT: new THREE.MeshStandardMaterial({ color: 0xa855f7, roughness: 0.5, metalness: 0.2, side: THREE.DoubleSide }),
DEFAULT: new THREE.MeshStandardMaterial({ color: 0xc0c6d0, roughness: 0.5, metalness: 0.1, side: THREE.DoubleSide })
};
// Selection Highlight Material
const HIGHLIGHT_MATERIAL = new THREE.MeshStandardMaterial({
color: 0xf59e0b,
emissive: 0xd97706,
emissiveIntensity: 0.4,
roughness: 0.3,
side: THREE.DoubleSide
});
// Initialize Application
document.addEventListener('DOMContentLoaded', () => {
initThree();
if (window.WebIFC && window.WebIFC.IfcAPI) {
initWebIfc();
} else {
document.addEventListener('web-ifc-loaded', () => {
initWebIfc();
});
}
bindUIEvents();
});
/**
* Set up Three.js Canvas, Camera, Lights, and OrbitControls
*/
function initThree() {
state.canvas = document.getElementById('bim-canvas');
state.container = state.canvas.parentElement;
// Scene
state.scene = new THREE.Scene();
state.scene.background = new THREE.Color(0x0f172a);
// Add 3D Measures Group to Scene
state.measureGroup.name = "BIM_MEASURES";
state.scene.add(state.measureGroup);
// Camera
const aspect = state.container.clientWidth / state.container.clientHeight;
state.camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
state.camera.position.set(20, 20, 20);
// Renderer
state.renderer = new THREE.WebGLRenderer({
canvas: state.canvas,
antialias: true,
alpha: true,
powerPreference: "high-performance"
});
state.renderer.setSize(state.container.clientWidth, state.container.clientHeight);
state.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
state.renderer.shadowMap.enabled = true;
state.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
state.renderer.localClippingEnabled = true;
// Controls
state.controls = new THREE.OrbitControls(state.camera, state.renderer.domElement);
state.controls.enableDamping = true;
state.controls.dampingFactor = 0.05;
state.controls.screenSpacePanning = true;
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
state.scene.add(ambientLight);
const dirLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight1.position.set(50, 100, 50);
dirLight1.castShadow = true;
dirLight1.shadow.mapSize.width = 2048;
dirLight1.shadow.mapSize.height = 2048;
state.scene.add(dirLight1);
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.4);
dirLight2.position.set(-50, -50, -50);
state.scene.add(dirLight2);
// Grid & Axis Helpers
const gridHelper = new THREE.GridHelper(100, 50, 0x3b82f6, 0x334155);
gridHelper.position.y = -0.01;
state.scene.add(gridHelper);
// Resize Listener
window.addEventListener('resize', onWindowResize);
// Animation Loop
function animate() {
requestAnimationFrame(animate);
state.controls.update();
updateBadgePositions();
state.renderer.render(state.scene, state.camera);
}
animate();
}
/**
* Initialize Web-IFC Engine & Fetch File
*/
async function initWebIfc() {
const loaderStatus = document.getElementById('loader-status');
const loaderProgress = document.getElementById('loader-progress');
const loaderOverlay = document.getElementById('bim-loader');
try {
updateLoader(10, "Initialisation du moteur WebAssembly (Web-IFC)...");
// Instancier l'API WebIFC
state.ifcApi = new WebIFC.IfcAPI();
state.ifcApi.SetWasmPath(state.config.wasmPath || "/static/bim/vendor/", true);
await state.ifcApi.Init();
updateLoader(25, "Téléchargement du fichier IFC...");
// Fetch IFC file
const response = await fetch(state.config.downloadUrl);
if (!response.ok) throw new Error("Impossible de télécharger le fichier IFC (HTTP " + response.status + ")");
const contentLength = response.headers.get('content-length');
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
const reader = response.body.getReader();
let receivedBytes = 0;
let chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedBytes += value.length;
if (totalBytes > 0) {
const percent = 25 + Math.round((receivedBytes / totalBytes) * 45);
updateLoader(percent, `Téléchargement: ${(receivedBytes / 1024 / 1024).toFixed(2)} MB`);
}
}
// Combine chunks
const rawData = new Uint8Array(receivedBytes);
let offset = 0;
for (let chunk of chunks) {
rawData.set(chunk, offset);
offset += chunk.length;
}
updateLoader(75, "Analyse des géométries 3D & structure spatiale...");
// Open Model in WebIFC
state.modelID = state.ifcApi.OpenModel(rawData);
// Build Model Geometries & Scene
loadIfcGeometries();
// Build Spatial Tree UI
buildSpatialTreeUI();
// Calculate Model Bounding Box & Fit Camera
fitCameraToModel();
updateLoader(100, "Prêt !");
setTimeout(() => {
if (loaderOverlay) loaderOverlay.classList.add('hidden');
}, 400);
} catch (err) {
console.error("Erreur d'initialisation IFC:", err);
if (loaderStatus) loaderStatus.textContent = "Erreur : " + err.message;
if (loaderStatus) loaderStatus.style.color = "#ef4444";
}
}
function updateLoader(percent, text) {
const fill = document.getElementById('loader-progress');
const status = document.getElementById('loader-status');
if (fill) fill.style.width = percent + '%';
if (status && text) status.textContent = text;
}
/**
* Load IFC Geometries from WebIFC and Create Three.js Meshes
*/
function loadIfcGeometries() {
const modelID = state.modelID;
const api = state.ifcApi;
// Group containing all model meshes
const modelGroup = new THREE.Group();
modelGroup.name = "IFC_MODEL";
// Stream all geometries from WebIFC
api.StreamAllMeshes(modelID, (flatMesh) => {
const expressID = flatMesh.expressID;
const placedGeometries = flatMesh.geometries;
// Fetch IFC Entity Type Name (e.g. IFCWALL, IFCSLAB)
let typeName = "IFCELEMENT";
try {
const typeID = api.GetLineType(modelID, expressID);
typeName = api.GetNameFromTypeCode(typeID) || "IFCELEMENT";
} catch (e) {}
// Assign Material based on Type
const mat = (IFC_MATERIALS[typeName] || IFC_MATERIALS.DEFAULT).clone();
for (let i = 0; i < placedGeometries.size(); i++) {
const placedGeom = placedGeometries.get(i);
const geom = api.GetGeometry(modelID, placedGeom.geometryExpressID);
// Build Three.js BufferGeometry
const bufferGeom = buildBufferGeometry(api, geom);
if (!bufferGeom) continue;
// Matrix Transformation
const matrix = new THREE.Matrix4();
matrix.fromArray(placedGeom.flatTransformation);
bufferGeom.applyMatrix4(matrix);
// Create Mesh
const mesh = new THREE.Mesh(bufferGeom, mat);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.userData = {
expressID: expressID,
typeName: typeName,
modelID: modelID
};
modelGroup.add(mesh);
// Track mesh
const meshRecord = {
expressID: expressID,
typeName: typeName,
mesh: mesh,
origMaterial: mat
};
state.meshes.push(meshRecord);
state.expressIDToMesh.set(expressID, meshRecord);
}
});
state.scene.add(modelGroup);
}
/**
* Converts WebIFC Geometry Data to Three.js BufferGeometry
*/
function buildBufferGeometry(api, geom) {
const vertexData = api.GetVertexArray(geom.GetVertexData(), geom.GetVertexDataSize());
const indexData = api.GetIndexArray(geom.GetIndexData(), geom.GetIndexDataSize());
if (!vertexData || vertexData.length === 0) return null;
const bufferGeom = new THREE.BufferGeometry();
// Web-IFC vertices layout: [px, py, pz, nx, ny, nz]
const numVertices = vertexData.length / 6;
const positions = new Float32Array(numVertices * 3);
const normals = new Float32Array(numVertices * 3);
for (let i = 0; i < numVertices; i++) {
positions[i * 3] = vertexData[i * 6];
positions[i * 3 + 1] = vertexData[i * 6 + 1];
positions[i * 3 + 2] = vertexData[i * 6 + 2];
normals[i * 3] = vertexData[i * 6 + 3];
normals[i * 3 + 1] = vertexData[i * 6 + 4];
normals[i * 3 + 2] = vertexData[i * 6 + 5];
}
bufferGeom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
bufferGeom.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
if (indexData && indexData.length > 0) {
bufferGeom.setIndex(new THREE.BufferAttribute(indexData, 1));
}
return bufferGeom;
}
/**
* Compute Model Bounding Box and Frame Camera
*/
function fitCameraToModel() {
state.boundingBox.makeEmpty();
for (let rec of state.meshes) {
rec.mesh.geometry.computeBoundingBox();
state.boundingBox.expandByObject(rec.mesh);
}
if (state.boundingBox.isEmpty()) return;
state.boundingBox.getCenter(state.center);
state.boundingBox.getSize(state.size);
const maxDim = Math.max(state.size.x, state.size.y, state.size.z);
const fov = state.camera.fov * (Math.PI / 180);
let cameraDistance = Math.abs(maxDim / (2 * Math.tan(fov / 2))) * 1.5;
state.camera.position.set(
state.center.x + cameraDistance * 0.7,
state.center.y + cameraDistance * 0.6,
state.center.z + cameraDistance * 0.7
);
state.controls.target.copy(state.center);
state.controls.update();
// Configure Clipping Sliders Bounds
const sliderZ = document.getElementById('slider-clip-z');
const sliderX = document.getElementById('slider-clip-x');
const sliderY = document.getElementById('slider-clip-y');
if (sliderZ) { sliderZ.value = 100; updateClippingPlane('z', 100); }
if (sliderX) { sliderX.value = 100; updateClippingPlane('x', 100); }
if (sliderY) { sliderY.value = 100; updateClippingPlane('y', 100); }
}
/**
* Build Spatial Tree & Category Layers UI (Left Sidebar)
*/
function buildSpatialTreeUI() {
const container = document.getElementById('spatial-tree-container');
if (!container) return;
// Group meshes by IFC Category Type Name
const categoriesMap = new Map();
for (let rec of state.meshes) {
if (!categoriesMap.has(rec.typeName)) {
categoriesMap.set(rec.typeName, []);
}
categoriesMap.get(rec.typeName).push(rec);
}
let html = '<ul class="bim-tree-list">';
// Category Layers Section
html += `
<li class="bim-tree-item">
<div class="bim-tree-node fw-bold text-light">
<span class="bim-tree-toggle"><i class="bi bi-folder2-open"></i></span>
<span class="bim-tree-label">Catégories IFC (Calques)</span>
<span class="bim-tree-count">${categoriesMap.size}</span>
</div>
<ul class="bim-tree-children">
`;
for (let [typeName, meshList] of categoriesMap.entries()) {
const prettyName = formatTypeName(typeName);
html += `
<li class="bim-tree-item" data-category="${typeName}">
<div class="bim-tree-node">
<input type="checkbox" class="bim-tree-checkbox category-checkbox" data-category="${typeName}" checked>
<span class="bim-tree-label" title="${typeName}">${prettyName}</span>
<span class="bim-tree-count">${meshList.length}</span>
</div>
</li>
`;
}
html += `</ul></li></ul>`;
container.innerHTML = html;
// Bind Category Checkbox Listeners
container.querySelectorAll('.category-checkbox').forEach(cb => {
cb.addEventListener('change', (e) => {
const catName = e.target.getAttribute('data-category');
const isVisible = e.target.checked;
for (let rec of state.meshes) {
if (rec.typeName === catName) {
rec.mesh.visible = isVisible;
}
}
});
});
// Search Filter
const searchInput = document.getElementById('tree-search-input');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
container.querySelectorAll('li[data-category]').forEach(li => {
const text = li.textContent.toLowerCase();
if (!query || text.includes(query)) {
li.style.display = '';
} else {
li.style.display = 'none';
}
});
});
}
}
function formatTypeName(typeName) {
const mapping = {
IFCWALL: "Murs (IfcWall)",
IFCWALLSTANDARDCASE: "Murs Standard (IfcWall)",
IFCSLAB: "Dalles / Planchers (IfcSlab)",
IFCWINDOW: "Fenêtres (IfcWindow)",
IFCDOOR: "Portes (IfcDoor)",
IFCCOLUMN: "Poteaux (IfcColumn)",
IFCBEAM: "Poutres (IfcBeam)",
IFCROOF: "Toitures (IfcRoof)",
IFCPIPESEGMENT: "Tuyauterie (IfcPipeSegment)",
IFCDUCTSEGMENT: "Gaines de ventilation",
IFCFURNISHINGELEMENT: "Mobilier / Équipement"
};
return mapping[typeName] || typeName;
}
/**
* Raycasting & Selection Handler
*/
function onCanvasClick(event) {
// Prevent action if clicked outside 3D canvas
if (event.target.id !== 'bim-canvas') return;
const rect = state.canvas.getBoundingClientRect();
state.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
state.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
state.raycaster.setFromCamera(state.mouse, state.camera);
// Filter visible meshes only
const visibleMeshes = state.meshes.filter(r => r.mesh.visible).map(r => r.mesh);
const intersects = state.raycaster.intersectObjects(visibleMeshes, false);
// 3D Measurement Mode
if (state.isMeasuring) {
if (intersects.length > 0) {
const hitPoint = intersects[0].point.clone();
handleMeasurementClick(hitPoint);
}
return;
}
// Standard Object Selection
if (intersects.length > 0) {
const hitMesh = intersects[0].object;
selectMesh(hitMesh);
} else {
deselectMesh();
}
}
/**
* Dynamic Preview Line & Cursor update on Mouse Move (Measurement Mode)
*/
function onCanvasMouseMove(event) {
if (!state.isMeasuring || !state.measureStartPoint) return;
if (event.target.id !== 'bim-canvas') return;
const rect = state.canvas.getBoundingClientRect();
state.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
state.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
state.raycaster.setFromCamera(state.mouse, state.camera);
const visibleMeshes = state.meshes.filter(r => r.mesh.visible).map(r => r.mesh);
const intersects = state.raycaster.intersectObjects(visibleMeshes, false);
if (intersects.length > 0) {
const tempPoint = intersects[0].point;
const distance = state.measureStartPoint.distanceTo(tempPoint);
// Update preview line
if (state.measureTempLine) {
state.scene.remove(state.measureTempLine);
}
const lineGeom = new THREE.BufferGeometry().setFromPoints([state.measureStartPoint, tempPoint]);
const lineMat = new THREE.LineDashedMaterial({ color: 0x3b82f6, dashSize: 0.2, gapSize: 0.1, depthTest: false });
state.measureTempLine = new THREE.Line(lineGeom, lineMat);
state.measureTempLine.computeLineDistances();
state.measureTempLine.renderOrder = 999;
state.scene.add(state.measureTempLine);
const bannerText = document.getElementById('measure-banner-text');
if (bannerText) {
bannerText.textContent = `Point 1 sélectionné. Distance : ${distance.toFixed(2)} m (Cliquez pour poser le 2nd point)`;
}
}
}
/**
* 3D Measurement System Logic
*/
function toggleMeasurementMode(enable) {
state.isMeasuring = (typeof enable === 'boolean') ? enable : !state.isMeasuring;
const btn = document.getElementById('tool-measure');
const banner = document.getElementById('measure-banner');
const bannerText = document.getElementById('measure-banner-text');
if (btn) btn.classList.toggle('active', state.isMeasuring);
if (banner) banner.classList.toggle('hidden', !state.isMeasuring);
if (state.canvas) {
state.canvas.style.cursor = state.isMeasuring ? 'crosshair' : 'default';
}
if (state.isMeasuring) {
resetCurrentMeasurement();
if (bannerText) bannerText.textContent = "Mode Mesure : Cliquez sur un premier point 3D sur la maquette...";
} else {
resetCurrentMeasurement();
}
}
function handleMeasurementClick(hitPoint) {
const bannerText = document.getElementById('measure-banner-text');
if (!state.measureStartPoint) {
// First Point Clicked
state.measureStartPoint = hitPoint.clone();
// Create Green Start Sphere Marker
const geom = new THREE.SphereGeometry(0.12, 16, 16);
const mat = new THREE.MeshBasicMaterial({ color: 0x22c55e, depthTest: false });
state.measureStartSphere = new THREE.Mesh(geom, mat);
state.measureStartSphere.position.copy(hitPoint);
state.measureStartSphere.renderOrder = 999;
state.scene.add(state.measureStartSphere);
if (bannerText) {
bannerText.textContent = "Premier point posé ! Cliquez sur un second point 3D...";
}
} else {
// Second Point Clicked -> Complete Measurement!
const p1 = state.measureStartPoint.clone();
const p2 = hitPoint.clone();
const distance = p1.distanceTo(p2);
// Create Final 3D Line
const lineGeom = new THREE.BufferGeometry().setFromPoints([p1, p2]);
const lineMat = new THREE.LineBasicMaterial({ color: 0x06b6d4, linewidth: 3, depthTest: false });
const line = new THREE.Line(lineGeom, lineMat);
line.renderOrder = 999;
state.measureGroup.add(line);
// Create Red End Sphere Marker
const geom2 = new THREE.SphereGeometry(0.12, 16, 16);
const mat2 = new THREE.MeshBasicMaterial({ color: 0xef4444, depthTest: false });
const endSphere = new THREE.Mesh(geom2, mat2);
endSphere.position.copy(p2);
endSphere.renderOrder = 999;
state.measureGroup.add(endSphere);
// Transfer start sphere to measureGroup
if (state.measureStartSphere) {
state.scene.remove(state.measureStartSphere);
state.measureGroup.add(state.measureStartSphere);
state.measureStartSphere = null;
}
// Create HTML overlay badge at midpoint
const midpoint = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5);
createDistanceBadge(midpoint, distance);
// Reset temp line
if (state.measureTempLine) {
state.scene.remove(state.measureTempLine);
state.measureTempLine = null;
}
state.measureStartPoint = null;
if (bannerText) {
bannerText.textContent = `Mesure effectuée : ${distance.toFixed(2)} m ! Cliquez à nouveau pour mesurer un autre segment.`;
}
}
}
function resetCurrentMeasurement() {
if (state.measureStartSphere) {
state.scene.remove(state.measureStartSphere);
state.measureStartSphere = null;
}
if (state.measureTempLine) {
state.scene.remove(state.measureTempLine);
state.measureTempLine = null;
}
state.measureStartPoint = null;
}
function clearAllMeasurements() {
resetCurrentMeasurement();
if (state.measureGroup) {
state.scene.remove(state.measureGroup);
state.measureGroup = new THREE.Group();
state.measureGroup.name = "BIM_MEASURES";
state.scene.add(state.measureGroup);
}
// Remove badge elements
if (state.measureBadges) {
for (let badge of state.measureBadges) {
if (badge && badge.parentElement) {
badge.parentElement.removeChild(badge);
}
}
state.measureBadges = [];
}
const bannerText = document.getElementById('measure-banner-text');
if (bannerText && state.isMeasuring) {
bannerText.textContent = "Toutes les mesures ont été effacées. Cliquez sur un premier point 3D...";
}
}
function createDistanceBadge(midpoint, distance) {
const badge = document.createElement('div');
badge.className = 'bim-measure-badge';
badge.textContent = `${distance.toFixed(2)} m`;
badge.style.position = 'absolute';
state.container.appendChild(badge);
badge.userData = { worldPos: midpoint };
state.measureBadges.push(badge);
updateBadgePositions();
}
function updateBadgePositions() {
if (!state.measureBadges || state.measureBadges.length === 0 || !state.camera || !state.container) return;
const rect = state.container.getBoundingClientRect();
for (let badge of state.measureBadges) {
if (!badge || !badge.parentElement) continue;
const pos = badge.userData.worldPos.clone();
pos.project(state.camera);
const x = (pos.x * 0.5 + 0.5) * rect.width;
const y = (-(pos.y * 0.5) + 0.5) * rect.height;
badge.style.left = `${x}px`;
badge.style.top = `${y}px`;
badge.style.transform = 'translate(-50%, -50%)';
badge.style.display = (pos.z > 1.0) ? 'none' : 'block';
}
}
/**
* Highlight Selected 3D Object and Inspect Properties
*/
function selectMesh(mesh) {
// Restore previous selection
if (state.selectedMesh && state.selectedMesh !== mesh) {
state.selectedMesh.material = state.selectedOrigMat;
}
const expressID = mesh.userData.expressID;
const rec = state.expressIDToMesh.get(expressID);
state.selectedMesh = mesh;
state.selectedOrigMat = rec ? rec.origMaterial : mesh.material;
// Apply Highlight Material
mesh.material = HIGHLIGHT_MATERIAL;
// Render Property Inspector in Right Sidebar
renderPropertyInspector(expressID, mesh.userData.typeName);
// Auto Open Right Sidebar if Collapsed
const sidebarRight = document.getElementById('sidebar-right');
if (sidebarRight && sidebarRight.classList.contains('collapsed-right')) {
sidebarRight.classList.remove('collapsed-right');
}
}
function deselectMesh() {
if (state.selectedMesh) {
state.selectedMesh.material = state.selectedOrigMat;
state.selectedMesh = null;
state.selectedOrigMat = null;
}
renderEmptyInspector();
}
/**
* Query Web-IFC Properties and Render in Inspector
*/
function renderPropertyInspector(expressID, typeName) {
const inspectorContainer = document.getElementById('property-inspector-content');
if (!inspectorContainer) return;
let props = null;
let psets = [];
try {
props = state.ifcApi.getItemProperties(state.modelID, expressID);
} catch (e) {
console.warn("Erreur lecture propriétés:", e);
}
let html = `
<div class="bim-prop-group">
<div class="bim-prop-group-title"><i class="bi bi-cube"></i> Élément IFC</div>
<table class="bim-prop-table">
<tr><td class="bim-prop-key">Express ID</td><td class="bim-prop-val">${expressID}</td></tr>
<tr><td class="bim-prop-key">Catégorie</td><td class="bim-prop-val"><span class="badge bg-primary">${typeName}</span></td></tr>
<tr><td class="bim-prop-key">Nom</td><td class="bim-prop-val">${props && props.Name ? props.Name.value : 'N/A'}</td></tr>
<tr><td class="bim-prop-key">GlobalId</td><td class="bim-prop-val"><code>${props && props.GlobalId ? props.GlobalId.value : 'N/A'}</code></td></tr>
<tr><td class="bim-prop-key">Description</td><td class="bim-prop-val">${props && props.Description ? props.Description.value : '-'}</td></tr>
<tr><td class="bim-prop-key">Tag IFC</td><td class="bim-prop-val">${props && props.Tag ? props.Tag.value : '-'}</td></tr>
</table>
</div>
`;
// Fetch PropertySets (Psets) if available
try {
const psetLines = state.ifcApi.getPropertySets(state.modelID, expressID);
if (psetLines && psetLines.length > 0) {
for (let psetRef of psetLines) {
const psetData = state.ifcApi.getItemProperties(state.modelID, psetRef.expressID);
if (!psetData) continue;
html += `
<div class="bim-prop-group">
<div class="bim-prop-group-title"><i class="bi bi-file-earmark-code"></i> ${psetData.Name ? psetData.Name.value : 'PropertySet'}</div>
<table class="bim-prop-table">
`;
if (psetData.HasProperties) {
for (let propRef of psetData.HasProperties) {
const pVal = state.ifcApi.getItemProperties(state.modelID, propRef.value);
if (pVal && pVal.Name && pVal.NominalValue) {
html += `<tr><td class="bim-prop-key">${pVal.Name.value}</td><td class="bim-prop-val">${pVal.NominalValue.value}</td></tr>`;
}
}
}
html += `</table></div>`;
}
}
} catch (e) {}
inspectorContainer.innerHTML = html;
}
function renderEmptyInspector() {
const inspectorContainer = document.getElementById('property-inspector-content');
if (!inspectorContainer) return;
inspectorContainer.innerHTML = `
<div class="text-center text-muted p-4">
<i class="bi bi-cursor fs-1 d-block mb-2 opacity-50"></i>
<p class="small mb-0">Cliquez sur n'importe quel élément 3D de la maquette pour inspecter ses propriétés et jeux de données (PropertySets).</p>
</div>
`;
}
/**
* Update Section Clipping Plane Positions
*/
function updateClippingPlane(axis, percent) {
const plane = state.clippingPlanes[axis];
if (!plane) return;
const p = percent / 100;
if (axis === 'z') {
plane.constant = state.boundingBox.min.y + state.size.y * p;
} else if (axis === 'x') {
plane.constant = state.boundingBox.min.x + state.size.x * p;
} else if (axis === 'y') {
plane.constant = state.boundingBox.min.z + state.size.z * p;
}
}
/**
* UI Event Bindings & Toolbar Setup
*/
function bindUIEvents() {
state.canvas.addEventListener('click', onCanvasClick);
state.canvas.addEventListener('mousemove', onCanvasMouseMove);
// Sidebars Toggle
const sidebarLeft = document.getElementById('sidebar-left');
const sidebarRight = document.getElementById('sidebar-right');
document.getElementById('toggle-left-sidebar')?.addEventListener('click', () => {
sidebarLeft.classList.toggle('collapsed-left');
});
document.getElementById('tool-toggle-left')?.addEventListener('click', () => {
sidebarLeft.classList.toggle('collapsed-left');
});
document.getElementById('toggle-right-sidebar')?.addEventListener('click', () => {
sidebarRight.classList.toggle('collapsed-right');
});
document.getElementById('tool-toggle-right')?.addEventListener('click', () => {
sidebarRight.classList.toggle('collapsed-right');
});
// Camera Presets
document.getElementById('btn-view-iso')?.addEventListener('click', () => setPresetCamera('iso'));
document.getElementById('btn-view-top')?.addEventListener('click', () => setPresetCamera('top'));
document.getElementById('btn-view-front')?.addEventListener('click', () => setPresetCamera('front'));
document.getElementById('btn-view-right')?.addEventListener('click', () => setPresetCamera('right'));
document.getElementById('btn-view-fit')?.addEventListener('click', () => fitCameraToModel());
document.getElementById('tool-reset')?.addEventListener('click', () => fitCameraToModel());
// Clipping Panel & Sliders
const clippingPanel = document.getElementById('clipping-panel');
document.getElementById('tool-clipping')?.addEventListener('click', () => {
clippingPanel.classList.toggle('active');
const isActive = clippingPanel.classList.contains('active');
state.renderer.localClippingEnabled = isActive;
// Apply clipping planes to materials
state.meshes.forEach(r => {
r.mesh.material.clippingPlanes = isActive ? [state.clippingPlanes.z, state.clippingPlanes.x, state.clippingPlanes.y] : [];
});
});
document.getElementById('close-clipping-panel')?.addEventListener('click', () => {
clippingPanel.classList.remove('active');
});
document.getElementById('slider-clip-z')?.addEventListener('input', (e) => {
updateClippingPlane('z', e.target.value);
document.getElementById('val-clip-z').textContent = e.target.value + '%';
});
document.getElementById('slider-clip-x')?.addEventListener('input', (e) => {
updateClippingPlane('x', e.target.value);
document.getElementById('val-clip-x').textContent = e.target.value + '%';
});
document.getElementById('slider-clip-y')?.addEventListener('input', (e) => {
updateClippingPlane('y', e.target.value);
document.getElementById('val-clip-y').textContent = e.target.value + '%';
});
// Wireframe Toggle
document.getElementById('tool-wireframe')?.addEventListener('click', (e) => {
state.isWireframe = !state.isWireframe;
e.currentTarget.classList.toggle('active', state.isWireframe);
state.meshes.forEach(r => {
r.origMaterial.wireframe = state.isWireframe;
});
});
// Ghosting / Transparency Toggle
document.getElementById('tool-ghosting')?.addEventListener('click', (e) => {
state.isGhosting = !state.isGhosting;
e.currentTarget.classList.toggle('active', state.isGhosting);
state.meshes.forEach(r => {
if (r.typeName !== 'IFCWINDOW') {
r.origMaterial.transparent = state.isGhosting;
r.origMaterial.opacity = state.isGhosting ? 0.3 : 1.0;
}
});
});
// 3D Distance Measurement Tool Events
document.getElementById('tool-measure')?.addEventListener('click', () => {
toggleMeasurementMode();
});
document.getElementById('btn-clear-measures')?.addEventListener('click', () => {
clearAllMeasurements();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && state.isMeasuring) {
toggleMeasurementMode(false);
}
});
}
/**
* Change Preset Camera Angles
*/
function setPresetCamera(viewType) {
const center = state.center;
const maxDim = Math.max(state.size.x, state.size.y, state.size.z) || 20;
const dist = maxDim * 1.5;
// Highlight active preset button
document.querySelectorAll('.bim-preset-btn').forEach(btn => btn.classList.remove('active'));
switch (viewType) {
case 'iso':
state.camera.position.set(center.x + dist * 0.7, center.y + dist * 0.6, center.z + dist * 0.7);
document.getElementById('btn-view-iso')?.classList.add('active');
break;
case 'top':
state.camera.position.set(center.x, center.y + dist * 1.2, center.z + 0.01);
document.getElementById('btn-view-top')?.classList.add('active');
break;
case 'front':
state.camera.position.set(center.x, center.y + dist * 0.2, center.z + dist);
document.getElementById('btn-view-front')?.classList.add('active');
break;
case 'right':
state.camera.position.set(center.x + dist, center.y + dist * 0.2, center.z);
document.getElementById('btn-view-right')?.classList.add('active');
break;
}
state.controls.target.copy(center);
state.controls.update();
}
function onWindowResize() {
if (!state.container || !state.camera || !state.renderer) return;
const aspect = state.container.clientWidth / state.container.clientHeight;
state.camera.aspect = aspect;
state.camera.updateProjectionMatrix();
state.renderer.setSize(state.container.clientWidth, state.container.clientHeight);
}
})();

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,197 @@
{% load static i18n %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ document.title }} - Visionneur BIM IFC | StreetUp</title>
<!-- Bootstrap Icons & Fonts -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Viewer CSS -->
<link rel="stylesheet" href="{% static 'bim/css/ifc_viewer.css' %}">
</head>
<body class="bim-viewer-body">
<div class="bim-wrapper">
<!-- Header Navbar -->
<header class="bim-header">
<div class="bim-header-brand">
<a href="{% url 'documents:detail' document.pk %}" class="bim-preset-btn" title="{% translate 'Retour au document' %}">
<i class="bi bi-arrow-left"></i>
</a>
<i class="bi bi-box-seam fs-5 text-primary"></i>
<h1 class="bim-header-title" title="{{ document.title }}">{{ document.title }}</h1>
<span class="bim-badge">IFC v{{ version.version_number }}</span>
</div>
<!-- Camera View Presets -->
<div class="bim-header-actions">
<button type="button" class="bim-preset-btn active" id="btn-view-iso" title="{% translate 'Vue Isométrique' %}">
<i class="bi bi-box"></i> {% translate "Iso" %}
</button>
<button type="button" class="bim-preset-btn" id="btn-view-top" title="{% translate 'Vue de Dessus' %}">
<i class="bi bi-square"></i> {% translate "Haut" %}
</button>
<button type="button" class="bim-preset-btn" id="btn-view-front" title="{% translate 'Vue de Face' %}">
<i class="bi bi-layout-sidebar-inset"></i> {% translate "Face" %}
</button>
<button type="button" class="bim-preset-btn" id="btn-view-right" title="{% translate 'Vue de Droite' %}">
<i class="bi bi-layout-sidebar"></i> {% translate "Droite" %}
</button>
<button type="button" class="bim-preset-btn" id="btn-view-fit" title="{% translate 'Recentrer la vue' %}">
<i class="bi bi-aspect-ratio"></i> {% translate "Recentrer" %}
</button>
</div>
</header>
<!-- Main Workspace -->
<main class="bim-main">
<!-- Left Sidebar: Spatial Tree & Layers -->
<aside class="bim-sidebar bim-sidebar-left" id="sidebar-left">
<div class="bim-sidebar-header">
<span class="bim-sidebar-title">
<i class="bi bi-layers"></i> {% translate "Arborescence & Calques" %}
</span>
<button type="button" class="bim-preset-btn p-1" id="toggle-left-sidebar" title="Masquer">
<i class="bi bi-chevron-left"></i>
</button>
</div>
<div class="bim-sidebar-content">
<div class="mb-3">
<input type="text" id="tree-search-input" class="form-control form-control-sm bg-dark text-light border-secondary" placeholder="{% translate 'Filtrer un élément ou niveau...' %}" style="border-color: var(--bim-panel-border) !important; font-size: 0.825rem;">
</div>
<div id="spatial-tree-container">
<p class="text-muted small p-2">{% translate "Analyse de la structure IFC en cours..." %}</p>
</div>
</div>
</aside>
<!-- 3D Canvas Container -->
<div class="bim-canvas-container">
<canvas id="bim-canvas"></canvas>
<!-- Floating Toolbar -->
<div class="bim-toolbar">
<button type="button" class="bim-tool-btn" id="tool-toggle-left" data-tooltip="{% translate 'Arborescence / Calques' %}">
<i class="bi bi-list-nested"></i>
</button>
<div class="bim-tool-divider"></div>
<button type="button" class="bim-tool-btn" id="tool-clipping" data-tooltip="{% translate 'Plan de coupe 3D' %}">
<i class="bi bi-scissors"></i>
</button>
<button type="button" class="bim-tool-btn" id="tool-ghosting" data-tooltip="{% translate 'Mode Transparence' %}">
<i class="bi bi-eye"></i>
</button>
<button type="button" class="bim-tool-btn" id="tool-wireframe" data-tooltip="{% translate 'Mode Filaire' %}">
<i class="bi bi-grid-3x3"></i>
</button>
<button type="button" class="bim-tool-btn" id="tool-measure" data-tooltip="{% translate 'Mesure de distance 3D' %}">
<i class="bi bi-rulers"></i>
</button>
<div class="bim-tool-divider"></div>
<button type="button" class="bim-tool-btn" id="tool-reset" data-tooltip="{% translate 'Réinitialiser la vue' %}">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button type="button" class="bim-tool-btn" id="tool-toggle-right" data-tooltip='{% translate "Propriétés de l’objet" %}'>
<i class="bi bi-info-circle"></i>
</button>
</div>
<!-- Measure Guidance Banner -->
<div class="bim-measure-banner hidden" id="measure-banner">
<i class="bi bi-rulers text-primary"></i>
<span id="measure-banner-text">{% translate "Mode Mesure : Cliquez sur un premier point 3D sur la maquette..." %}</span>
<button type="button" class="btn btn-sm btn-outline-danger ms-2 py-0 px-1" id="btn-clear-measures" title="{% translate 'Effacer toutes les mesures' %}">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- Section Clipping Controls Panel -->
<div class="bim-clipping-panel" id="clipping-panel">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong class="small text-light"><i class="bi bi-scissors"></i> {% translate "Plans de coupe 3D" %}</strong>
<button type="button" class="btn-close btn-close-white btn-sm" id="close-clipping-panel"></button>
</div>
<div class="bim-slider-group">
<div class="bim-slider-label">
<span>{% translate "Coupe horizontale (Étage Z)" %}</span>
<span id="val-clip-z">100%</span>
</div>
<input type="range" min="0" max="100" value="100" class="bim-slider" id="slider-clip-z">
</div>
<div class="bim-slider-group">
<div class="bim-slider-label">
<span>{% translate "Coupe transversale (Axe X)" %}</span>
<span id="val-clip-x">100%</span>
</div>
<input type="range" min="0" max="100" value="100" class="bim-slider" id="slider-clip-x">
</div>
<div class="bim-slider-group">
<div class="bim-slider-label">
<span>{% translate "Coupe longitudinale (Axe Y)" %}</span>
<span id="val-clip-y">100%</span>
</div>
<input type="range" min="0" max="100" value="100" class="bim-slider" id="slider-clip-y">
</div>
</div>
<!-- Loading Overlay -->
<div class="bim-loader-overlay" id="bim-loader">
<div class="bim-spinner"></div>
<div class="bim-loader-title">{% translate "Chargement de la maquette BIM" %}</div>
<div class="bim-loader-progress-bar">
<div class="bim-loader-progress-fill" id="loader-progress"></div>
</div>
<div class="bim-loader-text" id="loader-status">{% translate "Téléchargement du fichier IFC..." %}</div>
</div>
</div>
<!-- Right Sidebar: Property Inspector -->
<aside class="bim-sidebar bim-sidebar-right" id="sidebar-right">
<div class="bim-sidebar-header">
<span class="bim-sidebar-title">
<i class="bi bi-info-square"></i> {% translate "Propriétés de l'objet" %}
</span>
<button type="button" class="bim-preset-btn p-1" id="toggle-right-sidebar" title="Masquer">
<i class="bi bi-chevron-right"></i>
</button>
</div>
<div class="bim-sidebar-content" id="property-inspector-content">
<div class="text-center text-muted p-4">
<i class="bi bi-cursor fs-1 d-block mb-2 opacity-50"></i>
<p class="small mb-0">{% translate "Cliquez sur n'importe quel élément 3D de la maquette pour inspecter ses propriétés et jeux de données (PropertySets)." %}</p>
</div>
</div>
</aside>
</main>
</div>
<!-- Vendor JavaScript Libraries (Three.js & Web-IFC) -->
<script src="{% static 'bim/vendor/three.min.js' %}"></script>
<script src="{% static 'bim/vendor/OrbitControls.js' %}"></script>
<script type="module">
import * as WebIFC from "{% static 'bim/vendor/web-ifc-api.js' %}";
window.WebIFC = WebIFC;
document.dispatchEvent(new Event('web-ifc-loaded'));
</script>
<!-- Viewer Configuration Context -->
<script>
window.BIM_CONFIG = {
docCode: "{{ doc_code }}",
downloadUrl: "{% url 'bim:download_ifc' doc_code %}",
infoUrl: "{% url 'bim:document_info' doc_code %}",
wasmPath: "{% static 'bim/vendor/' %}"
};
</script>
<!-- BIM Viewer Application Script -->
<script src="{% static 'bim/js/ifc_viewer.js' %}"></script>
</body>
</html>

65
streetup/bim/tests.py Normal file
View file

@ -0,0 +1,65 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from documents.models import ManagedDocument, DocumentVersion
User = get_user_model()
class BimViewerTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="testuser", email="test@streetup.be", password="password123", is_superuser=True)
self.other_user = User.objects.create_user(username="otheruser", email="other@streetup.be", password="password123")
# Create sample IFC ManagedDocument
self.doc = ManagedDocument.objects.create(
title="Maquette Test BIM",
description="Fichier IFC de test",
created_by=self.user,
)
dummy_ifc_content = b"ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n#1= IFCORGANIZATION('Test',$,$,$,$);\nENDSEC;\nEND-ISO-10303-21;\n"
self.version = DocumentVersion.objects.create(
document=self.doc,
version_number=1,
file=SimpleUploadedFile("model.ifc", dummy_ifc_content, content_type="application/x-step"),
uploaded_by=self.user,
)
self.doc.current_version = self.version
self.doc.title = "Maquette Test BIM"
self.doc.save()
def test_unauthenticated_user_redirected(self):
url = reverse("bim:viewer", kwargs={"doc_code": self.doc.uuid})
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
def test_owner_can_access_viewer(self):
self.client.force_login(self.user)
url = reverse("bim:viewer", kwargs={"doc_code": self.doc.uuid})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_info_api(self):
self.client.force_login(self.user)
url = reverse("bim:document_info", kwargs={"doc_code": self.doc.uuid})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
json_data = response.json()
self.assertEqual(json_data["title"], "Maquette Test BIM")
self.assertEqual(json_data["uuid"], str(self.doc.uuid))
def test_download_api(self):
self.client.force_login(self.user)
url = reverse("bim:download_ifc", kwargs={"doc_code": self.doc.uuid})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn(b"ISO-10303-21", b"".join(response.streaming_content))
def test_unauthorized_user_denied(self):
self.client.force_login(self.other_user)
url = reverse("bim:viewer", kwargs={"doc_code": self.doc.uuid})
response = self.client.get(url)
self.assertEqual(response.status_code, 403)

10
streetup/bim/urls.py Normal file
View file

@ -0,0 +1,10 @@
from django.urls import path
from . import views
app_name = "bim"
urlpatterns = [
path("viewer/<uuid:doc_code>/", views.ifc_viewer_view, name="viewer"),
path("api/document/<uuid:doc_code>/download/", views.ifc_file_download_view, name="download_ifc"),
path("api/document/<uuid:doc_code>/info/", views.ifc_document_info_api, name="document_info"),
]

92
streetup/bim/views.py Normal file
View file

@ -0,0 +1,92 @@
import os
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import FileResponse, JsonResponse, Http404
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from documents.models import ManagedDocument
from documents.permissions import user_has_document_permission
def _get_document_by_code(doc_code):
"""Utility to fetch ManagedDocument by UUID string/object or integer PK."""
try:
return ManagedDocument.objects.get(uuid=doc_code)
except (ManagedDocument.DoesNotExist, ValueError):
# Fallback to PK lookup if integer passed
if str(doc_code).isdigit():
return get_object_or_404(ManagedDocument, pk=int(doc_code))
raise Http404("Document non trouvé")
@login_required
def ifc_viewer_view(request, doc_code):
"""Renders the full-screen 3D IFC Viewer page for a given document code."""
document = _get_document_by_code(doc_code)
if not user_has_document_permission(request.user, document, required_permission=ManagedDocument.PERMISSION_VIEW):
raise PermissionDenied("Vous n'avez pas la permission de visualiser ce document.")
version = document.latest_version
if not version or not version.file:
raise Http404("Aucun fichier disponible pour ce document.")
filename = version.filename
ext = os.path.splitext(filename)[1].lower()
context = {
"document": document,
"version": version,
"filename": filename,
"ext": ext,
"doc_code": document.uuid,
}
return render(request, "bim/viewer.html", context)
@login_required
def ifc_file_download_view(request, doc_code):
"""Streams the IFC file for consumption by the WebGL 3D viewer."""
document = _get_document_by_code(doc_code)
if not user_has_document_permission(request.user, document, required_permission=ManagedDocument.PERMISSION_VIEW):
raise PermissionDenied("Accès refusé.")
version = document.latest_version
if not version or not version.file:
raise Http404("Fichier non trouvé.")
response = FileResponse(
version.file.open("rb"),
content_type="application/octet-stream",
filename=version.filename
)
# Enable CORS headers if viewer runs under different subdomains / frames
response["Access-Control-Allow-Origin"] = "*"
response["Accept-Ranges"] = "bytes"
return response
@login_required
def ifc_document_info_api(request, doc_code):
"""API endpoint providing document metadata for the BIM viewer UI."""
document = _get_document_by_code(doc_code)
if not user_has_document_permission(request.user, document, required_permission=ManagedDocument.PERMISSION_VIEW):
return JsonResponse({"error": "Accès refusé"}, status=403)
version = document.latest_version
file_size = version.file.size if version and version.file else 0
return JsonResponse({
"uuid": str(document.uuid),
"id": document.id,
"title": document.title,
"description": document.description or "",
"filename": version.filename if version else "",
"version_number": version.version_number if version else 1,
"file_size": file_size,
"created_at": document.created_at.isoformat() if document.created_at else None,
"updated_at": document.updated_at.isoformat() if document.updated_at else None,
})

View file

@ -53,18 +53,18 @@ def current_app_view(request):
Context processor that determines the current active AppView based on the URL namespace. Context processor that determines the current active AppView based on the URL namespace.
Makes 'current_view' available in all templates. Makes 'current_view' available in all templates.
Only 'sign', 'documents', and 'schools' have their own dedicated views. Only 'sign' and 'documents' have their own dedicated views.
All other apps (assets, interventions, controls, contracts, etc.) use 'asset_management' view. All other apps (assets, interventions, controls, contracts, etc.) use 'asset_management' view.
""" """
current_view = None current_view = None
# Apps that have their own dedicated view # Apps that have their own dedicated view
DEDICATED_VIEW_APPS = ['sign', 'documents', 'schools', 'reporting', 'maps', 'authorizations', 'panoramax'] DEDICATED_VIEW_APPS = ['sign', 'documents', 'reporting', 'maps', 'authorizations', 'panoramax']
try: try:
# Get the namespace from the current URL # Get the namespace from the current URL
resolved = resolve(request.path) resolved = resolve(request.path)
namespace = resolved.namespace # e.g., 'schools', 'sign', 'home', 'assets' namespace = resolved.namespace # e.g., 'sign', 'home', 'assets'
if namespace: if namespace:
if namespace in DEDICATED_VIEW_APPS: if namespace in DEDICATED_VIEW_APPS:
@ -143,13 +143,34 @@ def organization_info(request):
""" """
return { return {
'product_name': getattr(settings, 'PRODUCT_NAME', 'StreetUp'), 'product_name': getattr(settings, 'PRODUCT_NAME', 'StreetUp'),
'organization_name': getattr(settings, 'ORGANIZATION_NAME', 'Brussels Mobility'), 'organization_name': getattr(settings, 'ORGANIZATION_NAME', 'StreetUp'),
'organization_logo_header': getattr(settings, 'ORGANIZATION_LOGO_HEADER', 'common/android-192.png'), 'organization_logo_header': getattr(settings, 'ORGANIZATION_LOGO_HEADER', 'common/android-192.png'),
'organization_primary_color': getattr(settings, 'ORGANIZATION_PRIMARY_COLOR_HEX', '#2f3e8b'), 'organization_primary_color': getattr(settings, 'ORGANIZATION_PRIMARY_COLOR_HEX', '#2f3e8b'),
'map_default_center': [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)], 'map_default_center': [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)],
'map_default_zoom': getattr(settings, 'MAP_DEFAULT_ZOOM', 11), 'map_default_zoom': getattr(settings, 'MAP_DEFAULT_ZOOM', 11),
'map_default_extent': getattr(settings, 'MAP_DEFAULT_EXTENT', None), 'map_default_extent': getattr(settings, 'MAP_DEFAULT_EXTENT', None),
'map_overlay_wms_url': getattr(settings, 'MAP_OVERLAY_WMS_URL', ''),
'map_ortho_wms_url': getattr(settings, 'MAP_ORTHO_WMS_URL', ''), 'map_ortho_wms_url': getattr(settings, 'MAP_ORTHO_WMS_URL', ''),
'map_ortho_wms_name': getattr(settings, 'MAP_ORTHO_WMS_NAME', ''), 'map_ortho_wms_name': getattr(settings, 'MAP_ORTHO_WMS_NAME', ''),
'map_ortho_wms_attribution': getattr(settings, 'MAP_ORTHO_WMS_ATTRIBUTION', ''), 'map_ortho_wms_attribution': getattr(settings, 'MAP_ORTHO_WMS_ATTRIBUTION', ''),
} }
def regional_terminology(request):
"""
Context processor providing regional terminology & standard definitions
(e.g., PEB in Belgium vs DPE/GES in France) based on the user's UserConfig or system default.
Makes 'regional_terms' and 'regional_standard' available in all templates.
"""
from .utils import get_regional_terms
standard_code = getattr(settings, 'REGIONAL_STANDARD', 'BE')
if request.user.is_authenticated:
user_config = getattr(request.user, 'config', None)
if user_config and getattr(user_config, 'regional_standard', None):
standard_code = user_config.regional_standard
return {
'regional_standard': standard_code,
'regional_terms': get_regional_terms(standard_code),
}

View file

@ -129,6 +129,19 @@
"uses_occupations": false "uses_occupations": false
} }
}, },
{
"model": "common.thematic",
"pk": 12,
"fields": {
"code": "publicbuildings",
"name_fr": "Bâtiments publics",
"name_nl": "Openbare gebouwen",
"description": "Gestion du patrimoine bâti public (écoles, mairies, équipements sportifs, culturels, etc.)",
"icon": "bi-building",
"color": "#2B579A",
"uses_occupations": false
}
},
{ {
"model": "common.role", "model": "common.role",
"pk": 1, "pk": 1,
@ -178,19 +191,6 @@
"name": "observer" "name": "observer"
} }
}, },
{
"model": "common.appview",
"pk": 1,
"fields": {
"code": "schools",
"name_fr": "Écoles",
"name_nl": "Scholen",
"icon": "bi-building",
"url_name": "schools:index",
"is_default": false,
"order": 30
}
},
{ {
"model": "common.appview", "model": "common.appview",
"pk": 2, "pk": 2,

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 10:57+0200\n" "POT-Creation-Date: 2026-07-28 14:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,243 +17,451 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: common/middleware.py:154 #: streetup/common/middleware.py:154
msgid "" msgid ""
"Votre session a expiré après le délai configuré. Veuillez vous reconnecter." "Votre session a expiré après le délai configuré. Veuillez vous reconnecter."
msgstr "" msgstr ""
"Your session has expired after the configured timeout. Please log in again." "Your session has expired after the configured timeout. Please log in again."
#: common/private_files/views.py:37 #: streetup/common/private_files/views.py:37
msgid "Lien expiré ou invalide" msgid "Lien expiré ou invalide"
msgstr "Expired or invalid link" msgstr "Expired or invalid link"
#: common/private_files/views.py:43 #: streetup/common/private_files/views.py:43
msgid "Modèle inconnu" msgid "Modèle inconnu"
msgstr "Unknown model" msgstr "Unknown model"
#: common/private_files/views.py:47 #: streetup/common/private_files/views.py:47
msgid "Objet introuvable" msgid "Objet introuvable"
msgstr "Object not found" msgstr "Object not found"
#: common/private_files/views.py:53 #: streetup/common/private_files/views.py:53
msgid "Fichier non exposé" msgid "Fichier non exposé"
msgstr "File not exposed" msgstr "File not exposed"
#: common/private_files/views.py:58 #: streetup/common/private_files/views.py:58
msgid "Accès refusé" msgid "Accès refusé"
msgstr "Access denied" msgstr "Access denied"
#: common/private_files/views.py:63 #: streetup/common/private_files/views.py:63
msgid "Fichier introuvable" msgid "Fichier introuvable"
msgstr "File not found" msgstr "File not found"
#: common/templates/admin/common/status_permission_matrix.html:19 #: streetup/common/templates/admin/common/status_permission_matrix.html:19
msgid "Permission" msgid "Permission"
msgstr "Permission" msgstr "Permission"
#: common/templates/admin/common/status_permission_matrix.html:36 #: streetup/common/templates/admin/common/status_permission_matrix.html:36
msgid "Voir" msgid "Voir"
msgstr "View" msgstr "View"
#: common/templates/admin/common/status_permission_matrix.html:53 #: streetup/common/templates/admin/common/status_permission_matrix.html:53
msgid "Modifier" msgid "Modifier"
msgstr "Edit" msgstr "Edit"
#: common/templates/admin/common/status_permission_matrix.html:70 #: streetup/common/templates/admin/common/status_permission_matrix.html:70
msgid "Changer → vers" msgid "Changer → vers"
msgstr "Change → to" msgstr "Change → to"
#: common/templates/admin/common/status_permission_matrix.html:102 #: streetup/common/templates/admin/common/status_permission_matrix.html:102
msgid "Enregistrez d'abord pour configurer les permissions par statut." msgid "Enregistrez d'abord pour configurer les permissions par statut."
msgstr "Save first to configure permissions by status." msgstr "Save first to configure permissions by status."
#: common/templates/common/account_info.html:8 #: streetup/common/templates/common/account_info.html:8
msgid "Déconnexion" msgid "Déconnexion"
msgstr "Logout" msgstr "Logout"
#: common/templates/common/account_info.html:26 #: streetup/common/templates/common/account_info.html:26
msgid "Vous êtes connecté en tant que :" msgid "Vous êtes connecté en tant que :"
msgstr "You are logged in as:" msgstr "You are logged in as:"
#: common/templates/common/account_info.html:32 #: streetup/common/templates/common/account_info.html:32
msgid "Changer de langue" msgid "Changer de langue"
msgstr "Change language" msgstr "Change language"
#: common/templates/common/account_info.html:40 #: streetup/common/templates/common/account_info.html:40
msgid "Choisir la langue" msgid "Choisir la langue"
msgstr "Choose language" msgstr "Choose language"
#: common/templates/common/account_info.html:42 #: streetup/common/templates/common/account_info.html:42
#: common/templates/common/account_info.html:88 #: streetup/common/templates/common/account_info.html:88
#: common/templates/common/components/document_viewer.html:201 #: streetup/common/templates/common/components/document_viewer.html:201
msgid "Fermer" msgid "Fermer"
msgstr "Close" msgstr "Close"
#: common/templates/common/account_info.html:73 #: streetup/common/templates/common/account_info.html:73
#: common/templates/common/account_info.html:86 #: streetup/common/templates/common/account_info.html:86
msgid "Modifier mon mot de passe" msgid "Modifier mon mot de passe"
msgstr "Change my password" msgstr "Change my password"
#: common/templates/common/account_info.html:158 #: streetup/common/templates/common/account_info.html:158
msgid "Annuler" msgid "Annuler"
msgstr "Cancel" msgstr "Cancel"
#: common/templates/common/account_info.html:161 #: streetup/common/templates/common/account_info.html:161
msgid "Enregistrer" msgid "Enregistrer"
msgstr "Save" msgstr "Save"
#: common/templates/common/account_info.html:174 #: streetup/common/templates/common/account_info.html:174
msgid "Se déconnecter" msgid "Se déconnecter"
msgstr "Sign out" msgstr "Sign out"
#: common/templates/common/account_info.html:181 #: streetup/common/templates/common/account_info.html:181
#: streetup/common/templates/common/components/help_panel.html:109
msgid "Retour" msgid "Retour"
msgstr "Back" msgstr "Back"
#: common/templates/common/components/document_viewer.html:40 #: streetup/common/templates/common/components/document_viewer.html:40
#: common/templates/common/components/document_viewer.html:50 #: streetup/common/templates/common/components/document_viewer.html:50
#: common/templates/common/components/document_viewer.html:61 #: streetup/common/templates/common/components/document_viewer.html:61
msgid "Supprimer" msgid "Supprimer"
msgstr "Delete" msgstr "Delete"
#: common/templates/common/components/document_viewer.html:75 #: streetup/common/templates/common/components/document_viewer.html:75
msgid "Copier vers une localisation ou un asset" msgid "Copier vers une localisation ou un asset"
msgstr "Copy to a location or an asset" msgstr "Copy to a location or an asset"
#: common/templates/common/components/document_viewer.html:99 #: streetup/common/templates/common/components/document_viewer.html:99
#: common/templates/common/components/document_viewer.html:154 #: streetup/common/templates/common/components/document_viewer.html:154
msgid "Historique des versions" msgid "Historique des versions"
msgstr "Version history" msgstr "Version history"
#: common/templates/common/components/document_viewer.html:156 #: streetup/common/templates/common/components/document_viewer.html:156
msgid "Historique" msgid "Historique"
msgstr "History" msgstr "History"
#: common/templates/common/components/document_viewer.html:180 #: streetup/common/templates/common/components/document_viewer.html:180
msgid "Visionneuse de documents" msgid "Visionneuse de documents"
msgstr "Document viewer" msgstr "Document viewer"
#: common/templates/common/components/document_viewer.html:188 #: streetup/common/templates/common/components/document_viewer.html:188
msgid "Ouvrir en pleine page" msgid "Ouvrir en pleine page"
msgstr "Open in full screen" msgstr "Open in full screen"
#: common/templates/common/components/document_viewer.html:195 #: streetup/common/templates/common/components/document_viewer.html:195
#: common/templates/common/components/document_viewer.html:196 #: streetup/common/templates/common/components/document_viewer.html:196
#: common/templates/common/components/document_viewer.html:261 #: streetup/common/templates/common/components/document_viewer.html:261
msgid "Télécharger" msgid "Télécharger"
msgstr "Download" msgstr "Download"
#: common/templates/common/components/document_viewer.html:229 #: streetup/common/templates/common/components/document_viewer.html:229
msgid "File truncated to the first 50 000 characters." msgid "File truncated to the first 50 000 characters."
msgstr "File truncated to the first 50,000 characters." msgstr "File truncated to the first 50,000 characters."
#: common/templates/common/components/document_viewer.html:245 #: streetup/common/templates/common/components/document_viewer.html:245
msgid "Only the first 200 rows are shown." msgid "Only the first 200 rows are shown."
msgstr "Only the first 200 rows are shown." msgstr "Only the first 200 rows are shown."
#: common/templates/common/components/document_viewer.html:301 #: streetup/common/templates/common/components/document_viewer.html:301
msgid "Précédent" msgid "Précédent"
msgstr "Previous" msgstr "Previous"
#: common/templates/common/components/document_viewer.html:309 #: streetup/common/templates/common/components/document_viewer.html:309
msgid "Suivant" msgid "Suivant"
msgstr "Next" msgstr "Next"
#: common/templates/common/components/document_viewer.html:432 #: streetup/common/templates/common/components/document_viewer.html:432
msgid "Aucun document associé" msgid "Aucun document associé"
msgstr "No associated document" msgstr "No associated document"
#: common/tests.py:46 common/tests.py:78 common/views.py:265 #: streetup/common/templates/common/components/help_panel.html:9
#: common/views.py:269 common/views.py:280 msgid "Chargement..."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:10
msgid "Erreur lors du chargement."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:16
msgid "Centre d'aide"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:30
msgid "Rechercher un guide..."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:39
msgid "Prise en main de StreetUp"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:40
msgid "Découvrez les bases de l'application, la navigation et le menu."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:48
msgid "Rechercher et filtrer des assets"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:49
msgid "Apprenez à rechercher, filtrer et personnaliser l'inventaire."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:57
msgid "Statuts des interventions"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:58
msgid ""
"Comprendre le cycle de vie d'une intervention à travers ses différents "
"statuts."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:67
msgid "Approuver des interventions"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:68
msgid ""
"Guide destiné aux gestionnaires pour valider et approuver les chantiers."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:78
msgid "Gérer les projets"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:79
msgid ""
"Comment regrouper les interventions et suivre la progression des chantiers."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:89
msgid "Ajouter un contrôle"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:90
msgid "Comment planifier, réaliser et synchroniser un contrôle de conformité."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:99
msgid "Aucun guide ne correspond à votre recherche."
msgstr ""
#: streetup/common/tests.py:46 streetup/common/tests.py:78
#: streetup/common/views.py:265 streetup/common/views.py:269
#: streetup/common/views.py:280
msgid "Vous ne pouvez pas accéder à ce compte." msgid "Vous ne pouvez pas accéder à ce compte."
msgstr "You cannot access this account." msgstr "You cannot access this account."
#: common/views.py:104 #: streetup/common/utils.py:132
msgid "Belgique (PEB / Maison Communale / Sécurité PMR)"
msgstr "Belgium (PEB / Town Hall / PRM Safety)"
#: streetup/common/utils.py:133
msgid "Hôtel de Ville / Maison Communale"
msgstr "Town Hall / Municipal Building"
#: streetup/common/utils.py:134
msgid "PEB"
msgstr "PEB"
#: streetup/common/utils.py:135
msgid "Performance Énergétique des Bâtiments (PEB)"
msgstr "Building Energy Performance (PEB)"
#: streetup/common/utils.py:136
msgid "Bilan Carbone"
msgstr "Carbon Footprint"
#: streetup/common/utils.py:137
msgid "Émissions de Carbone / Bilan Énergétique"
msgstr "Carbon Emissions / Energy Audit"
#: streetup/common/utils.py:138
msgid "Accès public & PMR"
msgstr "Public & PRM Access"
#: streetup/common/utils.py:139
msgid "Accueil & Sécurité du Public (Normes PMR)"
msgstr "Public Reception & Safety (PRM Standards)"
#: streetup/common/utils.py:140
msgid "Catégorie d'accueil"
msgstr "Reception Category"
#: streetup/common/utils.py:141
msgid "Type d'affectation"
msgstr "Assignment Type"
#: streetup/common/utils.py:144
msgid "France (DPE / GES / Mairie / ERP)"
msgstr "France (DPE / GHG / Town Hall / ERP)"
#: streetup/common/utils.py:145
msgid "Mairie / Hôtel de Ville"
msgstr "Town Hall / City Hall"
#: streetup/common/utils.py:146
msgid "DPE"
msgstr "DPE"
#: streetup/common/utils.py:147
msgid "Diagnostic de Performance Énergétique (DPE)"
msgstr "Energy Performance Certificate (DPE)"
#: streetup/common/utils.py:148
msgid "GES"
msgstr "GHG"
#: streetup/common/utils.py:149
msgid "Émissions de Gaz à Effet de Serre (GES)"
msgstr "Greenhouse Gas Emissions (GHG)"
#: streetup/common/utils.py:150
msgid "ERP"
msgstr "ERP"
#: streetup/common/utils.py:151
msgid "Établissement Recevant du Public (ERP)"
msgstr "Public Reception Facility (ERP)"
#: streetup/common/utils.py:152
msgid "Catégorie ERP"
msgstr "ERP Category"
#: streetup/common/utils.py:153
msgid "Type ERP"
msgstr "ERP Type"
#: streetup/common/utils.py:156
msgid "International / Générique"
msgstr "International / Generic"
#: streetup/common/utils.py:157
msgid "Bâtiment Administratif / Hôtel de Ville"
msgstr "Administrative Building / Town Hall"
#: streetup/common/utils.py:158
msgid "PEB / DPE"
msgstr "EPC / Energy Diag"
#: streetup/common/utils.py:159
msgid "Performance Énergétique (PEB / DPE / EPC)"
msgstr "Energy Performance (PEB / DPE / EPC)"
#: streetup/common/utils.py:160
msgid "Émissions Carbone"
msgstr "Carbon Emissions"
#: streetup/common/utils.py:161
msgid "Bilan Carbone & Émissions"
msgstr "Carbon Footprint & Emissions"
#: streetup/common/utils.py:162
msgid "Accès Public"
msgstr "Public Access"
#: streetup/common/utils.py:163
msgid "Bâtiment d'Accueil du Public (ERP / PMR)"
msgstr "Public Reception Building (ERP / PRM)"
#: streetup/common/utils.py:164
msgid "Capacité d'accueil"
msgstr "Reception Capacity"
#: streetup/common/utils.py:165
msgid "Type d'établissement"
msgstr "Establishment Type"
#: streetup/common/views.py:104
msgid "Coordonnées invalides" msgid "Coordonnées invalides"
msgstr "Invalid coordinates" msgstr "Invalid coordinates"
#: common/views.py:120 #: streetup/common/views.py:120
msgid "Aucune rue trouvée" msgid "Aucune rue trouvée"
msgstr "No street found" msgstr "No street found"
#: common/views.py:205 #: streetup/common/views.py:205
msgid "Mot de passe mis à jour avec succès." msgid "Mot de passe mis à jour avec succès."
msgstr "Password updated successfully." msgstr "Password updated successfully."
#: common/views.py:235 #: streetup/common/views.py:235
msgid "Aucun compte lié n'est disponible." msgid "Aucun compte lié n'est disponible."
msgstr "No linked account is available." msgstr "No linked account is available."
#: common/views.py:239 #: streetup/common/views.py:239
msgid "Aucun compte cible n'a été sélectionné." msgid "Aucun compte cible n'a été sélectionné."
msgstr "No target account was selected." msgstr "No target account was selected."
#: common/views.py:243 #: streetup/common/views.py:243
msgid "Ce compte est déjà actif." msgid "Ce compte est déjà actif."
msgstr "This account is already active." msgstr "This account is already active."
#: common/views.py:286 #: streetup/common/views.py:286
msgid "Changement de compte effectué." msgid "Changement de compte effectué."
msgstr "Account switch completed." msgstr "Account switch completed."
#: common/views.py:304 common/views.py:329 common/views.py:356 #: streetup/common/views.py:304 streetup/common/views.py:329
#: common/views.py:397 common/views.py:426 common/views.py:487 #: streetup/common/views.py:356 streetup/common/views.py:397
#: common/views.py:514 #: streetup/common/views.py:426 streetup/common/views.py:487
#: streetup/common/views.py:514
msgid "Données invalides" msgid "Données invalides"
msgstr "Invalid data" msgstr "Invalid data"
#: common/views.py:308 common/views.py:333 #: streetup/common/views.py:308 streetup/common/views.py:333
msgid "Layout invalide" msgid "Layout invalide"
msgstr "Invalid layout" msgstr "Invalid layout"
#: common/views.py:316 common/views.py:341 common/views.py:383 #: streetup/common/views.py:316 streetup/common/views.py:341
#: common/views.py:414 common/views.py:474 common/views.py:500 #: streetup/common/views.py:383 streetup/common/views.py:414
#: common/views.py:531 #: streetup/common/views.py:474 streetup/common/views.py:500
#: streetup/common/views.py:531 streetup/common/views.py:549
msgid "Configuration utilisateur non trouvée" msgid "Configuration utilisateur non trouvée"
msgstr "User configuration not found" msgstr "User configuration not found"
#: common/views.py:363 common/views.py:403 common/views.py:520 #: streetup/common/views.py:363 streetup/common/views.py:403
#: streetup/common/views.py:520
msgid "Format de filtres invalide" msgid "Format de filtres invalide"
msgstr "Invalid filter format" msgstr "Invalid filter format"
#: common/views.py:367 common/views.py:406 common/views.py:523 #: streetup/common/views.py:367 streetup/common/views.py:406
#: streetup/common/views.py:523
msgid "Format de tri invalide" msgid "Format de tri invalide"
msgstr "Invalid sort format" msgstr "Invalid sort format"
#: common/views.py:375 #: streetup/common/views.py:375
msgid "Format de colonnes invalide" msgid "Format de colonnes invalide"
msgstr "Invalid column format" msgstr "Invalid column format"
#: common/views.py:439 #: streetup/common/views.py:439
msgid "Format de thématiques invalide" msgid "Format de thématiques invalide"
msgstr "Invalid thematic format" msgstr "Invalid thematic format"
#: common/views.py:441 #: streetup/common/views.py:441
msgid "Format de catégories invalide" msgid "Format de catégories invalide"
msgstr "Invalid category format" msgstr "Invalid category format"
#: common/views.py:443 #: streetup/common/views.py:443
msgid "Format de modèles invalide" msgid "Format de modèles invalide"
msgstr "Invalid model format" msgstr "Invalid model format"
#: common/views.py:445 #: streetup/common/views.py:445
msgid "Format de filtres thématiques invalide" msgid "Format de filtres thématiques invalide"
msgstr "Invalid thematic filter format" msgstr "Invalid thematic filter format"
#: common/views.py:447 #: streetup/common/views.py:447
msgid "Format de colonnes visibles invalide" msgid "Format de colonnes visibles invalide"
msgstr "Invalid visible columns format" msgstr "Invalid visible columns format"
#: common/views.py:449 #: streetup/common/views.py:449
msgid "Format de filtre type invalide" msgid "Format de filtre type invalide"
msgstr "Invalid type filter format" msgstr "Invalid type filter format"
#: common/views.py:451 #: streetup/common/views.py:451
msgid "Format de filtres statut invalide" msgid "Format de filtres statut invalide"
msgstr "Invalid status filter format" msgstr "Invalid status filter format"
#: common/views.py:493 #: streetup/common/views.py:493
#, fuzzy
#| msgid "Format de filtres invalide"
msgid "Format de préférences invalide" msgid "Format de préférences invalide"
msgstr "Invalid filter format" msgstr "Invalid filter format"
#: streetup/common/views.py:544 streetup/common/views.py:570
msgid "Document non trouvé"
msgstr "No street found"
#: streetup/common/views.py:554 streetup/common/views.py:558
msgid "Accès interdit"
msgstr ""
#: streetup/common/views.py:576
msgid "Erreur lors de la lecture du document"
msgstr ""

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 11:25+0200\n" "POT-Creation-Date: 2026-07-28 14:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -107,6 +107,7 @@ msgid "Se déconnecter"
msgstr "Se déconnecter" msgstr "Se déconnecter"
#: streetup/common/templates/common/account_info.html:181 #: streetup/common/templates/common/account_info.html:181
#: streetup/common/templates/common/components/help_panel.html:109
msgid "Retour" msgid "Retour"
msgstr "Retour" msgstr "Retour"
@ -163,12 +164,204 @@ msgstr "Suivant"
msgid "Aucun document associé" msgid "Aucun document associé"
msgstr "Aucun document associé" msgstr "Aucun document associé"
#: streetup/common/templates/common/components/help_panel.html:9
msgid "Chargement..."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:10
msgid "Erreur lors du chargement."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:16
msgid "Centre d'aide"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:30
msgid "Rechercher un guide..."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:39
msgid "Prise en main de StreetUp"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:40
msgid "Découvrez les bases de l'application, la navigation et le menu."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:48
msgid "Rechercher et filtrer des assets"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:49
msgid "Apprenez à rechercher, filtrer et personnaliser l'inventaire."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:57
msgid "Statuts des interventions"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:58
msgid ""
"Comprendre le cycle de vie d'une intervention à travers ses différents "
"statuts."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:67
msgid "Approuver des interventions"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:68
msgid ""
"Guide destiné aux gestionnaires pour valider et approuver les chantiers."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:78
msgid "Gérer les projets"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:79
msgid ""
"Comment regrouper les interventions et suivre la progression des chantiers."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:89
msgid "Ajouter un contrôle"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:90
msgid "Comment planifier, réaliser et synchroniser un contrôle de conformité."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:99
msgid "Aucun guide ne correspond à votre recherche."
msgstr ""
#: streetup/common/tests.py:46 streetup/common/tests.py:78 #: streetup/common/tests.py:46 streetup/common/tests.py:78
#: streetup/common/views.py:265 streetup/common/views.py:269 #: streetup/common/views.py:265 streetup/common/views.py:269
#: streetup/common/views.py:280 #: streetup/common/views.py:280
msgid "Vous ne pouvez pas accéder à ce compte." msgid "Vous ne pouvez pas accéder à ce compte."
msgstr "Vous ne pouvez pas accéder à ce compte." msgstr "Vous ne pouvez pas accéder à ce compte."
#: streetup/common/utils.py:132
msgid "Belgique (PEB / Maison Communale / Sécurité PMR)"
msgstr ""
#: streetup/common/utils.py:133
msgid "Hôtel de Ville / Maison Communale"
msgstr ""
#: streetup/common/utils.py:134
msgid "PEB"
msgstr ""
#: streetup/common/utils.py:135
msgid "Performance Énergétique des Bâtiments (PEB)"
msgstr ""
#: streetup/common/utils.py:136
msgid "Bilan Carbone"
msgstr ""
#: streetup/common/utils.py:137
msgid "Émissions de Carbone / Bilan Énergétique"
msgstr ""
#: streetup/common/utils.py:138
msgid "Accès public & PMR"
msgstr ""
#: streetup/common/utils.py:139
msgid "Accueil & Sécurité du Public (Normes PMR)"
msgstr ""
#: streetup/common/utils.py:140
msgid "Catégorie d'accueil"
msgstr ""
#: streetup/common/utils.py:141
msgid "Type d'affectation"
msgstr ""
#: streetup/common/utils.py:144
msgid "France (DPE / GES / Mairie / ERP)"
msgstr ""
#: streetup/common/utils.py:145
msgid "Mairie / Hôtel de Ville"
msgstr ""
#: streetup/common/utils.py:146
msgid "DPE"
msgstr ""
#: streetup/common/utils.py:147
msgid "Diagnostic de Performance Énergétique (DPE)"
msgstr ""
#: streetup/common/utils.py:148
msgid "GES"
msgstr ""
#: streetup/common/utils.py:149
msgid "Émissions de Gaz à Effet de Serre (GES)"
msgstr ""
#: streetup/common/utils.py:150
msgid "ERP"
msgstr ""
#: streetup/common/utils.py:151
msgid "Établissement Recevant du Public (ERP)"
msgstr ""
#: streetup/common/utils.py:152
msgid "Catégorie ERP"
msgstr ""
#: streetup/common/utils.py:153
msgid "Type ERP"
msgstr ""
#: streetup/common/utils.py:156
msgid "International / Générique"
msgstr ""
#: streetup/common/utils.py:157
msgid "Bâtiment Administratif / Hôtel de Ville"
msgstr ""
#: streetup/common/utils.py:158
msgid "PEB / DPE"
msgstr ""
#: streetup/common/utils.py:159
msgid "Performance Énergétique (PEB / DPE / EPC)"
msgstr ""
#: streetup/common/utils.py:160
msgid "Émissions Carbone"
msgstr ""
#: streetup/common/utils.py:161
msgid "Bilan Carbone & Émissions"
msgstr ""
#: streetup/common/utils.py:162
msgid "Accès Public"
msgstr ""
#: streetup/common/utils.py:163
msgid "Bâtiment d'Accueil du Public (ERP / PMR)"
msgstr ""
#: streetup/common/utils.py:164
msgid "Capacité d'accueil"
msgstr ""
#: streetup/common/utils.py:165
msgid "Type d'établissement"
msgstr ""
#: streetup/common/views.py:104 #: streetup/common/views.py:104
msgid "Coordonnées invalides" msgid "Coordonnées invalides"
msgstr "Coordonnées invalides" msgstr "Coordonnées invalides"
@ -211,7 +404,7 @@ msgstr "Layout invalide"
#: streetup/common/views.py:316 streetup/common/views.py:341 #: streetup/common/views.py:316 streetup/common/views.py:341
#: streetup/common/views.py:383 streetup/common/views.py:414 #: streetup/common/views.py:383 streetup/common/views.py:414
#: streetup/common/views.py:474 streetup/common/views.py:500 #: streetup/common/views.py:474 streetup/common/views.py:500
#: streetup/common/views.py:531 #: streetup/common/views.py:531 streetup/common/views.py:549
msgid "Configuration utilisateur non trouvée" msgid "Configuration utilisateur non trouvée"
msgstr "Configuration utilisateur non trouvée" msgstr "Configuration utilisateur non trouvée"
@ -258,7 +451,17 @@ msgid "Format de filtres statut invalide"
msgstr "Format de filtres statut invalide" msgstr "Format de filtres statut invalide"
#: streetup/common/views.py:493 #: streetup/common/views.py:493
#, fuzzy
#| msgid "Format de filtres invalide"
msgid "Format de préférences invalide" msgid "Format de préférences invalide"
msgstr "Format de filtres invalide" msgstr "Format de filtres invalide"
#: streetup/common/views.py:544 streetup/common/views.py:570
msgid "Document non trouvé"
msgstr "Aucune rue trouvée"
#: streetup/common/views.py:554 streetup/common/views.py:558
msgid "Accès interdit"
msgstr ""
#: streetup/common/views.py:576
msgid "Erreur lors de la lecture du document"
msgstr ""

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 11:25+0200\n" "POT-Creation-Date: 2026-07-28 14:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -108,6 +108,7 @@ msgid "Se déconnecter"
msgstr "Afmelden" msgstr "Afmelden"
#: streetup/common/templates/common/account_info.html:181 #: streetup/common/templates/common/account_info.html:181
#: streetup/common/templates/common/components/help_panel.html:109
msgid "Retour" msgid "Retour"
msgstr "Terug" msgstr "Terug"
@ -164,12 +165,204 @@ msgstr "Volgende"
msgid "Aucun document associé" msgid "Aucun document associé"
msgstr "Geen gekoppeld document" msgstr "Geen gekoppeld document"
#: streetup/common/templates/common/components/help_panel.html:9
msgid "Chargement..."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:10
msgid "Erreur lors du chargement."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:16
msgid "Centre d'aide"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:30
msgid "Rechercher un guide..."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:39
msgid "Prise en main de StreetUp"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:40
msgid "Découvrez les bases de l'application, la navigation et le menu."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:48
msgid "Rechercher et filtrer des assets"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:49
msgid "Apprenez à rechercher, filtrer et personnaliser l'inventaire."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:57
msgid "Statuts des interventions"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:58
msgid ""
"Comprendre le cycle de vie d'une intervention à travers ses différents "
"statuts."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:67
msgid "Approuver des interventions"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:68
msgid ""
"Guide destiné aux gestionnaires pour valider et approuver les chantiers."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:78
msgid "Gérer les projets"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:79
msgid ""
"Comment regrouper les interventions et suivre la progression des chantiers."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:89
msgid "Ajouter un contrôle"
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:90
msgid "Comment planifier, réaliser et synchroniser un contrôle de conformité."
msgstr ""
#: streetup/common/templates/common/components/help_panel.html:99
msgid "Aucun guide ne correspond à votre recherche."
msgstr ""
#: streetup/common/tests.py:46 streetup/common/tests.py:78 #: streetup/common/tests.py:46 streetup/common/tests.py:78
#: streetup/common/views.py:265 streetup/common/views.py:269 #: streetup/common/views.py:265 streetup/common/views.py:269
#: streetup/common/views.py:280 #: streetup/common/views.py:280
msgid "Vous ne pouvez pas accéder à ce compte." msgid "Vous ne pouvez pas accéder à ce compte."
msgstr "U hebt geen toegang tot dit account." msgstr "U hebt geen toegang tot dit account."
#: streetup/common/utils.py:132
msgid "Belgique (PEB / Maison Communale / Sécurité PMR)"
msgstr "België (EPB / Gemeentehuis / PMR-veiligheid)"
#: streetup/common/utils.py:133
msgid "Hôtel de Ville / Maison Communale"
msgstr "Stadhuis / Gemeentehuis"
#: streetup/common/utils.py:134
msgid "PEB"
msgstr "EPB"
#: streetup/common/utils.py:135
msgid "Performance Énergétique des Bâtiments (PEB)"
msgstr "Energieprestatie van Gebouwen (EPB)"
#: streetup/common/utils.py:136
msgid "Bilan Carbone"
msgstr "Koolstofbalans"
#: streetup/common/utils.py:137
msgid "Émissions de Carbone / Bilan Énergétique"
msgstr "Koolstofemissies / Energiebalans"
#: streetup/common/utils.py:138
msgid "Accès public & PMR"
msgstr "Publiekstoegang & PMR"
#: streetup/common/utils.py:139
msgid "Accueil & Sécurité du Public (Normes PMR)"
msgstr "Publieksontvangst & Veiligheid (PMR-normen)"
#: streetup/common/utils.py:140
msgid "Catégorie d'accueil"
msgstr "Ontvangstcategorie"
#: streetup/common/utils.py:141
msgid "Type d'affectation"
msgstr "Bestemmingstype"
#: streetup/common/utils.py:144
msgid "France (DPE / GES / Mairie / ERP)"
msgstr "Frankrijk (DPE / BKG / Stadhuis / ERP)"
#: streetup/common/utils.py:145
msgid "Mairie / Hôtel de Ville"
msgstr "Gemeentehuis / Stadhuis"
#: streetup/common/utils.py:146
msgid "DPE"
msgstr "DPE"
#: streetup/common/utils.py:147
msgid "Diagnostic de Performance Énergétique (DPE)"
msgstr "Energieprestatiediagnose (DPE)"
#: streetup/common/utils.py:148
msgid "GES"
msgstr "BKG"
#: streetup/common/utils.py:149
msgid "Émissions de Gaz à Effet de Serre (GES)"
msgstr "Broeikasgasemissies (BKG)"
#: streetup/common/utils.py:150
msgid "ERP"
msgstr "ERP"
#: streetup/common/utils.py:151
msgid "Établissement Recevant du Public (ERP)"
msgstr "Publiek Toegankelijke Inrichting (ERP)"
#: streetup/common/utils.py:152
msgid "Catégorie ERP"
msgstr "ERP-categorie"
#: streetup/common/utils.py:153
msgid "Type ERP"
msgstr "ERP-type"
#: streetup/common/utils.py:156
msgid "International / Générique"
msgstr "Internationaal / Generiek"
#: streetup/common/utils.py:157
msgid "Bâtiment Administratif / Hôtel de Ville"
msgstr "Administratief Gebouw / Stadhuis"
#: streetup/common/utils.py:158
msgid "PEB / DPE"
msgstr "EPB / DPE"
#: streetup/common/utils.py:159
msgid "Performance Énergétique (PEB / DPE / EPC)"
msgstr "Energieprestatie (EPB / DPE / EPC)"
#: streetup/common/utils.py:160
msgid "Émissions Carbone"
msgstr "Koolstofemissies"
#: streetup/common/utils.py:161
msgid "Bilan Carbone & Émissions"
msgstr "Koolstofbalans & Emissies"
#: streetup/common/utils.py:162
msgid "Accès Public"
msgstr "Publiekstoegang"
#: streetup/common/utils.py:163
msgid "Bâtiment d'Accueil du Public (ERP / PMR)"
msgstr "Publieksontvangstgebouw (ERP / PMR)"
#: streetup/common/utils.py:164
msgid "Capacité d'accueil"
msgstr "Ontvangstcapaciteit"
#: streetup/common/utils.py:165
msgid "Type d'établissement"
msgstr "Type inrichting"
#: streetup/common/views.py:104 #: streetup/common/views.py:104
msgid "Coordonnées invalides" msgid "Coordonnées invalides"
msgstr "Ongeldige coördinaten" msgstr "Ongeldige coördinaten"
@ -212,7 +405,7 @@ msgstr "Ongeldige lay-out"
#: streetup/common/views.py:316 streetup/common/views.py:341 #: streetup/common/views.py:316 streetup/common/views.py:341
#: streetup/common/views.py:383 streetup/common/views.py:414 #: streetup/common/views.py:383 streetup/common/views.py:414
#: streetup/common/views.py:474 streetup/common/views.py:500 #: streetup/common/views.py:474 streetup/common/views.py:500
#: streetup/common/views.py:531 #: streetup/common/views.py:531 streetup/common/views.py:549
msgid "Configuration utilisateur non trouvée" msgid "Configuration utilisateur non trouvée"
msgstr "Gebruikersconfiguratie niet gevonden" msgstr "Gebruikersconfiguratie niet gevonden"
@ -259,7 +452,17 @@ msgid "Format de filtres statut invalide"
msgstr "Ongeldig statusfilterformaat" msgstr "Ongeldig statusfilterformaat"
#: streetup/common/views.py:493 #: streetup/common/views.py:493
#, fuzzy
#| msgid "Format de filtres invalide"
msgid "Format de préférences invalide" msgid "Format de préférences invalide"
msgstr "Ongeldig filterformaat" msgstr "Ongeldig filterformaat"
#: streetup/common/views.py:544 streetup/common/views.py:570
msgid "Document non trouvé"
msgstr "Geen straat gevonden"
#: streetup/common/views.py:554 streetup/common/views.py:558
msgid "Accès interdit"
msgstr ""
#: streetup/common/views.py:576
msgid "Erreur lors de la lecture du document"
msgstr ""

View file

@ -0,0 +1,18 @@
# Generated by Django 6.0.7 on 2026-07-26 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0004_initial'),
]
operations = [
migrations.AddField(
model_name='userconfig',
name='regional_standard',
field=models.CharField(choices=[('BE', 'Belgique (PEB, Maison Communale / Hôtel de Ville, Sécurité PMR)'), ('FR', 'France (DPE, GES, Mairie, Normes ERP)'), ('INT', 'International / Générique')], default='BE', help_text='Définit la terminologie et les normes réglementaires adaptées à la région.', max_length=10, verbose_name='Norme régionale & vocabulaire'),
),
]

View file

@ -110,6 +110,18 @@ class UserConfig(models.Model):
help_text="Limite les raccourcis et les compteurs de la page d'accueil aux contrats pour lesquels l'utilisateur dispose de permissions explicites.", help_text="Limite les raccourcis et les compteurs de la page d'accueil aux contrats pour lesquels l'utilisateur dispose de permissions explicites.",
) )
generic_email = models.EmailField(blank=True, null=True) # Email générique de l'utilisateur generic_email = models.EmailField(blank=True, null=True) # Email générique de l'utilisateur
REGIONAL_STANDARD_CHOICES = [
('BE', 'Belgique (PEB, Maison Communale / Hôtel de Ville, Sécurité PMR)'),
('FR', 'France (DPE, GES, Mairie, Normes ERP)'),
('INT', 'International / Générique'),
]
regional_standard = models.CharField(
max_length=10,
choices=REGIONAL_STANDARD_CHOICES,
default='BE',
verbose_name="Norme régionale & vocabulaire",
help_text="Définit la terminologie et les normes réglementaires adaptées à la région.",
)
hide_intervention_map_by_default = models.BooleanField( hide_intervention_map_by_default = models.BooleanField(
default=False, default=False,
verbose_name="Masquer la carte des interventions par défaut", verbose_name="Masquer la carte des interventions par défaut",

View file

@ -5,6 +5,8 @@ from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
import json
from common.models import UserConfig from common.models import UserConfig
@ -249,4 +251,41 @@ class HelpPanelTests(TestCase):
self.assertIn("Ajouter un contrôle", data["html"]) self.assertIn("Ajouter un contrôle", data["html"])
class AssetsIndexPreferencesTests(TestCase):
def setUp(self):
from common.models import Thematic
self.superuser = User.objects.create_superuser(username="superprefuser", password="password123")
Thematic.objects.create(code="sign", name_fr="Signalisation")
Thematic.objects.create(code="roads", name_fr="Voirie")
def test_save_and_restore_assets_index_preferences_for_superuser(self):
self.client.force_login(self.superuser)
save_url = reverse("common:save_assets_index_preferences")
payload = {
"thematics": ["sign", "roads"],
"categories": [1, 2],
"models": [],
"conditional_filters": {},
"visible_columns": ["code", "status"],
"type_filter": "assets",
"status_filters": ["installed"],
"validation_status_filters": ["validated"],
"include_no_geom": False,
}
response = self.client.post(save_url, json.dumps(payload), content_type="application/json")
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json().get("success"))
# Verify unified_assets_index restores saved thematics and sets has_url_thematics to False
index_url = reverse("assets:index")
response = self.client.get(index_url)
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["has_url_thematics"])
self.assertEqual(response.context["selected_thematics"], ["sign", "roads"])
saved_prefs = json.loads(response.context["assets_index_prefs_json"])
self.assertEqual(saved_prefs["thematics"], ["sign", "roads"])
self.assertEqual(saved_prefs["validation_status_filters"], ["validated"])

View file

@ -17,6 +17,12 @@ def check_thematic_access(thematic_code, edit_permission_required=False):
def decorator(view_func): def decorator(view_func):
@functools.wraps(view_func) @functools.wraps(view_func)
def _wrapped_view(request, *args, **kwargs): def _wrapped_view(request, *args, **kwargs):
if not getattr(request.user, 'is_authenticated', False):
return HttpResponseForbidden("Access denied: Authentication required.")
if getattr(request.user, 'is_superuser', False):
return view_func(request, *args, **kwargs)
user_config = UserConfig.objects.filter(user=request.user).first() # Get user access configuration user_config = UserConfig.objects.filter(user=request.user).first() # Get user access configuration
thematic = get_object_or_404(Thematic, code=thematic_code) # Get thematic from the given parameter thematic = get_object_or_404(Thematic, code=thematic_code) # Get thematic from the given parameter
@ -117,3 +123,50 @@ def get_short_name(user):
last = getattr(user, "last_name", "") or "" last = getattr(user, "last_name", "") or ""
initial = (last[0].upper() + ".") if last else "" initial = (last[0].upper() + ".") if last else ""
return f"{first} {initial}".strip() return f"{first} {initial}".strip()
from django.utils.translation import gettext_lazy as _
REGIONAL_TERMINOLOGY = {
'BE': {
'regional_standard_name': _('Belgique (PEB / Maison Communale / Sécurité PMR)'),
'admin_building': _('Hôtel de Ville / Maison Communale'),
'energy_diag_short': _('PEB'),
'energy_diag_full': _('Performance Énergétique des Bâtiments (PEB)'),
'carbon_diag_short': _('Bilan Carbone'),
'carbon_diag_full': _('Émissions de Carbone / Bilan Énergétique'),
'public_access_short': _('Accès public & PMR'),
'public_access_full': _('Accueil & Sécurité du Public (Normes PMR)'),
'public_access_category': _('Catégorie d\'accueil'),
'public_access_type': _('Type d\'affectation'),
},
'FR': {
'regional_standard_name': _('France (DPE / GES / Mairie / ERP)'),
'admin_building': _('Mairie / Hôtel de Ville'),
'energy_diag_short': _('DPE'),
'energy_diag_full': _('Diagnostic de Performance Énergétique (DPE)'),
'carbon_diag_short': _('GES'),
'carbon_diag_full': _('Émissions de Gaz à Effet de Serre (GES)'),
'public_access_short': _('ERP'),
'public_access_full': _('Établissement Recevant du Public (ERP)'),
'public_access_category': _('Catégorie ERP'),
'public_access_type': _('Type ERP'),
},
'INT': {
'regional_standard_name': _('International / Générique'),
'admin_building': _('Bâtiment Administratif / Hôtel de Ville'),
'energy_diag_short': _('PEB / DPE'),
'energy_diag_full': _('Performance Énergétique (PEB / DPE / EPC)'),
'carbon_diag_short': _('Émissions Carbone'),
'carbon_diag_full': _('Bilan Carbone & Émissions'),
'public_access_short': _('Accès Public'),
'public_access_full': _('Bâtiment d\'Accueil du Public (ERP / PMR)'),
'public_access_category': _('Capacité d\'accueil'),
'public_access_type': _('Type d\'établissement'),
}
}
def get_regional_terms(standard_code='BE'):
"""Returns regional terminology dictionary for a given standard code ('BE', 'FR', 'INT')."""
return REGIONAL_TERMINOLOGY.get(standard_code or 'BE', REGIONAL_TERMINOLOGY['BE'])

View file

@ -432,6 +432,8 @@ def save_assets_index_preferences(request):
visible_columns = data.get('visible_columns') visible_columns = data.get('visible_columns')
type_filter = data.get('type_filter', 'all') type_filter = data.get('type_filter', 'all')
status_filters = data.get('status_filters', []) status_filters = data.get('status_filters', [])
validation_status_filters = data.get('validation_status_filters', [])
include_no_geom = data.get('include_no_geom', True)
map_center = data.get('map_center') map_center = data.get('map_center')
map_zoom = data.get('map_zoom') map_zoom = data.get('map_zoom')
@ -449,9 +451,11 @@ def save_assets_index_preferences(request):
return JsonResponse({'error': _('Format de filtre type invalide')}, status=400) return JsonResponse({'error': _('Format de filtre type invalide')}, status=400)
if not isinstance(status_filters, list): if not isinstance(status_filters, list):
return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400) return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400)
if not isinstance(validation_status_filters, list):
return JsonResponse({'error': _('Format de filtres statut de validation invalide')}, status=400)
try: try:
user_config = request.user.config user_config, _ = UserConfig.objects.get_or_create(user=request.user)
existing_prefs = user_config.assets_index_preferences or {} existing_prefs = user_config.assets_index_preferences or {}
existing_visible_columns = existing_prefs.get('visible_columns', []) if isinstance(existing_prefs, dict) else [] existing_visible_columns = existing_prefs.get('visible_columns', []) if isinstance(existing_prefs, dict) else []
if not isinstance(existing_visible_columns, list): if not isinstance(existing_visible_columns, list):
@ -465,6 +469,8 @@ def save_assets_index_preferences(request):
'visible_columns': visible_columns if visible_columns is not None else existing_visible_columns, 'visible_columns': visible_columns if visible_columns is not None else existing_visible_columns,
'type_filter': type_filter, 'type_filter': type_filter,
'status_filters': status_filters, 'status_filters': status_filters,
'validation_status_filters': validation_status_filters,
'include_no_geom': include_no_geom,
'map_center': map_center, 'map_center': map_center,
'map_zoom': map_zoom, 'map_zoom': map_zoom,
} }

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 11:25+0200\n" "POT-Creation-Date: 2026-07-27 09:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -40,7 +40,7 @@ msgid "Date de la commande"
msgstr "Date de la commande" msgstr "Date de la commande"
#: streetup/contracts/forms.py:87 #: streetup/contracts/forms.py:87
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:106 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:89
msgid "Date de livraison" msgid "Date de livraison"
msgstr "Date de livraison" msgstr "Date de livraison"
@ -49,7 +49,7 @@ msgid "Statut de la commande"
msgstr "Statut de la commande" msgstr "Statut de la commande"
#: streetup/contracts/forms.py:97 #: streetup/contracts/forms.py:97
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:202 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:185
msgid "Fichier de commande" msgid "Fichier de commande"
msgstr "Fichier de commande" msgstr "Fichier de commande"
@ -73,9 +73,9 @@ msgstr "Référence de commande"
#: streetup/contracts/templates/contracts/contracts_index.html:241 #: streetup/contracts/templates/contracts/contracts_index.html:241
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:71 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:71
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:102 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:85
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:497 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:480
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:585 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:568
#: streetup/contracts/views_pdf_reports.py:580 #: streetup/contracts/views_pdf_reports.py:580
#: streetup/contracts/views_pdf_reports.py:718 #: streetup/contracts/views_pdf_reports.py:718
#: streetup/contracts/views_pdf_reports.py:946 #: streetup/contracts/views_pdf_reports.py:946
@ -85,8 +85,8 @@ msgstr "Description"
#: streetup/contracts/forms.py:136 streetup/contracts/models.py:950 #: streetup/contracts/forms.py:136 streetup/contracts/models.py:950
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:74 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:74
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:588 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:571
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:709 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:692
#: streetup/contracts/views_pdf_reports.py:949 #: streetup/contracts/views_pdf_reports.py:949
#: streetup/contracts/views_pdf_reports.py:1142 #: streetup/contracts/views_pdf_reports.py:1142
msgid "Quantité" msgid "Quantité"
@ -219,8 +219,6 @@ msgid ""
msgstr "" msgstr ""
#: streetup/contracts/models.py:114 #: streetup/contracts/models.py:114
#, fuzzy
#| msgid "Voir les interventions assignées"
msgid "Restreindre aux interventions assignées" msgid "Restreindre aux interventions assignées"
msgstr "Voir les interventions assignées" msgstr "Voir les interventions assignées"
@ -239,8 +237,6 @@ msgid "Équipes de sociétés/organismes"
msgstr "Équipes de sociétés/organismes" msgstr "Équipes de sociétés/organismes"
#: streetup/contracts/models.py:135 #: streetup/contracts/models.py:135
#, fuzzy
#| msgid "Équipe"
msgid "Équipes" msgid "Équipes"
msgstr "Équipe" msgstr "Équipe"
@ -420,8 +416,6 @@ msgstr ""
"occupations (temps passé par membre)." "occupations (temps passé par membre)."
#: streetup/contracts/models.py:316 #: streetup/contracts/models.py:316
#, fuzzy
#| msgid "Impossible de charger les interventions."
msgid "Utilise les gestionnaires d'intervention" msgid "Utilise les gestionnaires d'intervention"
msgstr "Impossible de charger les interventions." msgstr "Impossible de charger les interventions."
@ -544,7 +538,7 @@ msgstr "Déclaration de créance"
#: streetup/contracts/models.py:856 #: streetup/contracts/models.py:856
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:52 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:52
#: streetup/contracts/templates/contracts/contracts_detail.html:116 #: streetup/contracts/templates/contracts/contracts_detail.html:116
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:768 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:751
#: streetup/contracts/templates/contracts/teams_index.html:741 #: streetup/contracts/templates/contracts/teams_index.html:741
msgid "Titre" msgid "Titre"
msgstr "Titre" msgstr "Titre"
@ -582,37 +576,37 @@ msgid "En attente de validation"
msgstr "En attente de validation" msgstr "En attente de validation"
#: streetup/contracts/models.py:927 #: streetup/contracts/models.py:927
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:616 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:599
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:926 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:909
msgid "Approuvé" msgid "Approuvé"
msgstr "Approuvé" msgstr "Approuvé"
#: streetup/contracts/models.py:928 #: streetup/contracts/models.py:928
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:621 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:604
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:927 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:910
msgid "Refusé" msgid "Refusé"
msgstr "Refusé" msgstr "Refusé"
#: streetup/contracts/models.py:929 #: streetup/contracts/models.py:929
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:623 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:606
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:929 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:912
msgid "Facturé" msgid "Facturé"
msgstr "Facturé" msgstr "Facturé"
#: streetup/contracts/models.py:939 streetup/contracts/models.py:1029 #: streetup/contracts/models.py:939 streetup/contracts/models.py:1029
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:53 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:53
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:86 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:69
msgid "Commande" msgid "Commande"
msgstr "Commande" msgstr "Commande"
#: streetup/contracts/models.py:945 #: streetup/contracts/models.py:945
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:699 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:682
msgid "Poste de marché" msgid "Poste de marché"
msgstr "Poste de marché" msgstr "Poste de marché"
#: streetup/contracts/models.py:952 #: streetup/contracts/models.py:952
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:591 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:574
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:713 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:696
msgid "Commentaire" msgid "Commentaire"
msgstr "Commentaire" msgstr "Commentaire"
@ -694,7 +688,7 @@ msgid "Contrats"
msgstr "Contrats" msgstr "Contrats"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:29 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:29
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:474 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:457
msgid "Justificatifs" msgid "Justificatifs"
msgstr "Justificatifs" msgstr "Justificatifs"
@ -711,7 +705,7 @@ msgid "Ajouter un justificatif"
msgstr "Ajouter un justificatif" msgstr "Ajouter un justificatif"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:90 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:90
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:313 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:296
#: streetup/contracts/templates/contracts/teams_index.html:324 #: streetup/contracts/templates/contracts/teams_index.html:324
msgid "Ajouter" msgid "Ajouter"
msgstr "Ajouter" msgstr "Ajouter"
@ -721,7 +715,7 @@ msgid "Justificatifs attachés"
msgstr "Justificatifs attachés" msgstr "Justificatifs attachés"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:117 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:117
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:466 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:449
msgid "Ajouté le" msgid "Ajouté le"
msgstr "Ajouté le" msgstr "Ajouté le"
@ -730,8 +724,8 @@ msgid "par"
msgstr "par" msgstr "par"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:125 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:125
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:332 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:315
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:333 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:316
msgid "Télécharger" msgid "Télécharger"
msgstr "Télécharger" msgstr "Télécharger"
@ -740,11 +734,11 @@ msgid "Êtes-vous sûr de vouloir supprimer ce justificatif ?"
msgstr "Êtes-vous sûr de vouloir supprimer ce justificatif ?" msgstr "Êtes-vous sûr de vouloir supprimer ce justificatif ?"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:130 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:130
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:238 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:221
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:290 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:273
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:642 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:625
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:743 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:726
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:949 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:932
msgid "Supprimer" msgid "Supprimer"
msgstr "Supprimer" msgstr "Supprimer"
@ -767,7 +761,7 @@ msgstr "Détail du marché / contrat"
#: streetup/contracts/templates/contracts/contracts_detail.html:28 #: streetup/contracts/templates/contracts/contracts_detail.html:28
#: streetup/contracts/templates/contracts/contracts_detail.html:117 #: streetup/contracts/templates/contracts/contracts_detail.html:117
#: streetup/contracts/templates/contracts/contracts_index.html:243 #: streetup/contracts/templates/contracts/contracts_index.html:243
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:769 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:752
msgid "Début" msgid "Début"
msgstr "Début" msgstr "Début"
@ -801,7 +795,7 @@ msgid "Montant \"presté"
msgstr "Montant \"presté" msgstr "Montant \"presté"
#: streetup/contracts/templates/contracts/contracts_detail.html:67 #: streetup/contracts/templates/contracts/contracts_detail.html:67
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:135 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:118
msgid "Montant validé" msgid "Montant validé"
msgstr "Montant validé" msgstr "Montant validé"
@ -810,7 +804,7 @@ msgid "Dossier interne"
msgstr "Dossier interne" msgstr "Dossier interne"
#: streetup/contracts/templates/contracts/contracts_detail.html:96 #: streetup/contracts/templates/contracts/contracts_detail.html:96
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:212 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:195
msgid "Retour à la liste" msgid "Retour à la liste"
msgstr "Retour à la liste" msgstr "Retour à la liste"
@ -821,9 +815,9 @@ msgstr "Commandes"
#: streetup/contracts/templates/contracts/contracts_detail.html:115 #: streetup/contracts/templates/contracts/contracts_detail.html:115
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:70 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:70
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:496 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:479
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:584 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:567
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:767 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:750
#: streetup/contracts/templates/contracts/teams_index.html:740 #: streetup/contracts/templates/contracts/teams_index.html:740
#: streetup/contracts/views_pdf_reports.py:945 #: streetup/contracts/views_pdf_reports.py:945
#: streetup/contracts/views_pdf_reports.py:1138 #: streetup/contracts/views_pdf_reports.py:1138
@ -832,8 +826,8 @@ msgstr "Code"
#: streetup/contracts/templates/contracts/contracts_detail.html:118 #: streetup/contracts/templates/contracts/contracts_detail.html:118
#: streetup/contracts/templates/contracts/contracts_index.html:200 #: streetup/contracts/templates/contracts/contracts_index.html:200
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:592 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:575
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:770 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:753
#: streetup/contracts/templates/contracts/teams_index.html:743 #: streetup/contracts/templates/contracts/teams_index.html:743
msgid "Statut" msgid "Statut"
msgstr "Statut" msgstr "Statut"
@ -912,7 +906,7 @@ msgid "Devis pour la commande %(order_code)s"
msgstr "Devis pour la commande %(order_code)s" msgstr "Devis pour la commande %(order_code)s"
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:112 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:95
msgid "Non renseigné" msgid "Non renseigné"
msgstr "Non renseigné" msgstr "Non renseigné"
@ -935,20 +929,20 @@ msgid "N° de poste"
msgstr "N° de poste" msgstr "N° de poste"
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:72 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:72
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:586 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:569
#: streetup/contracts/views_pdf_reports.py:947 #: streetup/contracts/views_pdf_reports.py:947
#: streetup/contracts/views_pdf_reports.py:1140 #: streetup/contracts/views_pdf_reports.py:1140
msgid "Unité" msgid "Unité"
msgstr "Unité" msgstr "Unité"
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:73 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:73
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:510 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:493
#: streetup/contracts/views_pdf_reports.py:1141 #: streetup/contracts/views_pdf_reports.py:1141
msgid "Prix unitaire" msgid "Prix unitaire"
msgstr "Prix unitaire" msgstr "Prix unitaire"
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:75 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:75
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:590 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:573
#: streetup/contracts/views_pdf_reports.py:1143 #: streetup/contracts/views_pdf_reports.py:1143
msgid "Montant" msgid "Montant"
msgstr "Montant" msgstr "Montant"
@ -979,39 +973,39 @@ msgstr "Enregistrer le devis"
msgid "Figer le devis" msgid "Figer le devis"
msgstr "Figer le devis" msgstr "Figer le devis"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:64 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:47
msgid "Détail de la commande" msgid "Détail de la commande"
msgstr "Détail de la commande" msgstr "Détail de la commande"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:99 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:82
msgid "Contrat lié" msgid "Contrat lié"
msgstr "Contrat lié" msgstr "Contrat lié"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:105 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:88
msgid "Date de commande" msgid "Date de commande"
msgstr "Date de commande" msgstr "Date de commande"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:111 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:94
msgid "Montant prévisionnel" msgid "Montant prévisionnel"
msgstr "Montant prévisionnel" msgstr "Montant prévisionnel"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:117 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:100
msgid "Montant \"presté\"" msgid "Montant \"presté\""
msgstr "Montant \"presté\"" msgstr "Montant \"presté\""
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:156 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:139
msgid "Devis" msgid "Devis"
msgstr "Devis" msgstr "Devis"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:158 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:141
msgid "Montant total" msgid "Montant total"
msgstr "Montant total" msgstr "Montant total"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:160 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:143
msgid "Devis figé" msgid "Devis figé"
msgstr "Devis figé" msgstr "Devis figé"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:167 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:150
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -1030,85 +1024,85 @@ msgstr[1] ""
" %(item_count)s postes dans le devis.\n" " %(item_count)s postes dans le devis.\n"
" " " "
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:174 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:157
msgid "Aucun devis n'est associé à cette commande pour le moment." msgid "Aucun devis n'est associé à cette commande pour le moment."
msgstr "Aucun devis n'est associé à cette commande pour le moment." msgstr "Aucun devis n'est associé à cette commande pour le moment."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:181 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:164
msgid "Voir le devis" msgid "Voir le devis"
msgstr "Voir le devis" msgstr "Voir le devis"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:183 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:166
msgid "Gérer le devis" msgid "Gérer le devis"
msgstr "Gérer le devis" msgstr "Gérer le devis"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:189 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:172
msgid "Exporter en PDF" msgid "Exporter en PDF"
msgstr "Exporter en PDF" msgstr "Exporter en PDF"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:198 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:181
msgid "Commandé par" msgid "Commandé par"
msgstr "Commandé par" msgstr "Commandé par"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:205 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:188
msgid "Télécharger le fichier" msgid "Télécharger le fichier"
msgstr "Télécharger le fichier" msgstr "Télécharger le fichier"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:219 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:202
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:329 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:312
msgid "Documents" msgid "Documents"
msgstr "Documents" msgstr "Documents"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:221 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:204
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:301 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:284
msgid "Ajouter des documents" msgid "Ajouter des documents"
msgstr "Ajouter des documents" msgstr "Ajouter des documents"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:270 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:253
msgid "Aucun document associé à cette commande." msgid "Aucun document associé à cette commande."
msgstr "Aucun document associé à cette commande." msgstr "Aucun document associé à cette commande."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:281 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:264
msgid "Supprimer le document" msgid "Supprimer le document"
msgstr "Supprimer le document" msgstr "Supprimer le document"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:285 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:268
msgid "Voulez-vous supprimer ce document ?" msgid "Voulez-vous supprimer ce document ?"
msgstr "Voulez-vous supprimer ce document ?" msgstr "Voulez-vous supprimer ce document ?"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:289 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:272
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:719 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:702
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:742 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:725
#: streetup/contracts/templates/contracts/teams_index.html:322 #: streetup/contracts/templates/contracts/teams_index.html:322
#: streetup/contracts/templates/contracts/teams_index.html:363 #: streetup/contracts/templates/contracts/teams_index.html:363
msgid "Annuler" msgid "Annuler"
msgstr "Annuler" msgstr "Annuler"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:374 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:357
msgid "Précédent" msgid "Précédent"
msgstr "Précédent" msgstr "Précédent"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:379 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:362
msgid "Suivant" msgid "Suivant"
msgstr "Suivant" msgstr "Suivant"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:416 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:399
msgid "Documents financiers" msgid "Documents financiers"
msgstr "Documents financiers" msgstr "Documents financiers"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:421 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:404
msgid "Générer un draft de l'état de travaux" msgid "Générer un draft de l'état de travaux"
msgstr "Générer un draft de l'état de travaux" msgstr "Générer un draft de l'état de travaux"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:424 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:407
msgid "Générer un draft de la déclaration de créance" msgid "Générer un draft de la déclaration de créance"
msgstr "Générer un draft de la déclaration de créance" msgstr "Générer un draft de la déclaration de créance"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:430 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:413
msgid "Générer la déclaration de créance" msgid "Générer la déclaration de créance"
msgstr "Générer la déclaration de créance" msgstr "Générer la déclaration de créance"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:434 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:417
msgid "" msgid ""
"Cette opération va générer un état de travaux, une déclaration de créance et " "Cette opération va générer un état de travaux, une déclaration de créance et "
"faire passer les interventions validées au statut \"comptabilisé\"." "faire passer les interventions validées au statut \"comptabilisé\"."
@ -1116,79 +1110,79 @@ msgstr ""
"Cette opération va générer un état de travaux, une déclaration de créance et " "Cette opération va générer un état de travaux, une déclaration de créance et "
"faire passer les interventions validées au statut \"comptabilisé\"." "faire passer les interventions validées au statut \"comptabilisé\"."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:444 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:427
#, python-format #, python-format
msgid "État d'avancement %(num)s" msgid "État d'avancement %(num)s"
msgstr "État d'avancement %(num)s" msgstr "État d'avancement %(num)s"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:448 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:431
msgid "Autres documents" msgid "Autres documents"
msgstr "Autres documents" msgstr "Autres documents"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:465 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:448
msgid "Type" msgid "Type"
msgstr "Type" msgstr "Type"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:468 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:451
msgid "justificatif(s)" msgid "justificatif(s)"
msgstr "justificatif(s)" msgstr "justificatif(s)"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:473 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:456
msgid "Gérer les justificatifs" msgid "Gérer les justificatifs"
msgstr "Gérer les justificatifs" msgstr "Gérer les justificatifs"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:487 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:470
msgid "Consommation des postes" msgid "Consommation des postes"
msgstr "Consommation des postes" msgstr "Consommation des postes"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:495 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:478
msgid "Poste" msgid "Poste"
msgstr "Poste" msgstr "Poste"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:499 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:482
msgid "Quantité prévue" msgid "Quantité prévue"
msgstr "Quantité prévue" msgstr "Quantité prévue"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:502 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:485
msgid "État d avancement du" msgid "État d avancement du"
msgstr "État d avancement du" msgstr "État d avancement du"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:503 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:486
msgid "État" msgid "État"
msgstr "État" msgstr "État"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:506 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:489
msgid "Quantité consommée" msgid "Quantité consommée"
msgstr "Quantité consommée" msgstr "Quantité consommée"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:508 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:491
msgid "Quantité restante" msgid "Quantité restante"
msgstr "Quantité restante" msgstr "Quantité restante"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:511 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:494
msgid "Montant consommé" msgid "Montant consommé"
msgstr "Montant consommé" msgstr "Montant consommé"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:550 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:533
msgid "Aucune consommation enregistrée pour cette commande." msgid "Aucune consommation enregistrée pour cette commande."
msgstr "Aucune consommation enregistrée pour cette commande." msgstr "Aucune consommation enregistrée pour cette commande."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:557 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:540
msgid "Projets associés" msgid "Projets associés"
msgstr "Projets associés" msgstr "Projets associés"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:567 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:550
#: streetup/contracts/views_pdf_reports.py:713 #: streetup/contracts/views_pdf_reports.py:713
msgid "Commandes directes" msgid "Commandes directes"
msgstr "Commandes directes" msgstr "Commandes directes"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:569 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:552
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:693 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:676
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1110 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1093
msgid "Ajouter un poste direct" msgid "Ajouter un poste direct"
msgstr "Ajouter un poste direct" msgstr "Ajouter un poste direct"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:577 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:560
msgid "" msgid ""
"Les commandes directes permettent d'enregistrer des postes de marché " "Les commandes directes permettent d'enregistrer des postes de marché "
"consommés sans passer par une intervention. Les postes validés sont inclus " "consommés sans passer par une intervention. Les postes validés sont inclus "
@ -1198,83 +1192,83 @@ msgstr ""
"consommés sans passer par une intervention. Les postes validés sont inclus " "consommés sans passer par une intervention. Les postes validés sont inclus "
"dans les totaux, les états d'avancement et les déclarations de créance." "dans les totaux, les états d'avancement et les déclarations de créance."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:583 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:566
msgid "N°" msgid "N°"
msgstr "N°" msgstr "N°"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:587 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:570
msgid "Prix unit." msgid "Prix unit."
msgstr "Prix unit." msgstr "Prix unit."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:589 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:572
msgid "Qté facturée" msgid "Qté facturée"
msgstr "Qté facturée" msgstr "Qté facturée"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:593 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:576
msgid "Actions" msgid "Actions"
msgstr "Actions" msgstr "Actions"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:625 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:608
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:928 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:911
msgid "En attente" msgid "En attente"
msgstr "En attente" msgstr "En attente"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:635 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:618
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:944 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:927
msgid "Modifier" msgid "Modifier"
msgstr "Modifier" msgstr "Modifier"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:652 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:635
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:956 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:939
msgid "Valider" msgid "Valider"
msgstr "Valider" msgstr "Valider"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:660 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:643
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:961 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:944
msgid "Refuser" msgid "Refuser"
msgstr "Refuser" msgstr "Refuser"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:668 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:651
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:966 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:949
msgid "Remettre en attente" msgid "Remettre en attente"
msgstr "Remettre en attente" msgstr "Remettre en attente"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:678 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:661
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1213 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1196
msgid "Aucune commande directe enregistrée." msgid "Aucune commande directe enregistrée."
msgstr "Aucune commande directe enregistrée." msgstr "Aucune commande directe enregistrée."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:700 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:683
msgid "Rechercher par code, numéro ou description…" msgid "Rechercher par code, numéro ou description…"
msgstr "Rechercher par code, numéro ou description…" msgstr "Rechercher par code, numéro ou description…"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:721 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:704
#: streetup/contracts/templates/contracts/teams_index.html:365 #: streetup/contracts/templates/contracts/teams_index.html:365
msgid "Enregistrer" msgid "Enregistrer"
msgstr "Enregistrer" msgstr "Enregistrer"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:735 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:718
msgid "Supprimer le poste direct" msgid "Supprimer le poste direct"
msgstr "Supprimer le poste direct" msgstr "Supprimer le poste direct"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:739 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:722
msgid "Voulez-vous supprimer ce poste direct ?" msgid "Voulez-vous supprimer ce poste direct ?"
msgstr "Voulez-vous supprimer ce poste direct ?" msgstr "Voulez-vous supprimer ce poste direct ?"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:755 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:738
msgid "Ajouter une intervention" msgid "Ajouter une intervention"
msgstr "Ajouter une intervention" msgstr "Ajouter une intervention"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1107 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1090
msgid "Modifier le poste direct" msgid "Modifier le poste direct"
msgstr "Modifier le poste direct" msgstr "Modifier le poste direct"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1143 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1126
msgid "Veuillez sélectionner un poste de marché." msgid "Veuillez sélectionner un poste de marché."
msgstr "Veuillez sélectionner un poste de marché." msgstr "Veuillez sélectionner un poste de marché."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1173 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1156
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1178 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1161
#: streetup/contracts/templates/contracts/teams_index.html:436 #: streetup/contracts/templates/contracts/teams_index.html:436
#: streetup/contracts/templates/contracts/teams_index.html:440 #: streetup/contracts/templates/contracts/teams_index.html:440
msgid "Une erreur est survenue." msgid "Une erreur est survenue."
@ -1611,51 +1605,51 @@ msgstr "Impossible de supprimer un poste déjà validé."
msgid "Impossible de supprimer un poste déjà facturé." msgid "Impossible de supprimer un poste déjà facturé."
msgstr "Impossible de supprimer un poste déjà facturé." msgstr "Impossible de supprimer un poste déjà facturé."
#: streetup/contracts/views_daily_report.py:227 #: streetup/contracts/views_daily_report.py:241
msgid "Rapport journalier des interventions" msgid "Rapport journalier des interventions"
msgstr "Rapport journalier des interventions" msgstr "Rapport journalier des interventions"
#: streetup/contracts/views_daily_report.py:248 #: streetup/contracts/views_daily_report.py:262
#: streetup/contracts/views_daily_report.py:393 #: streetup/contracts/views_daily_report.py:407
msgid "Synthèse par thématique" msgid "Synthèse par thématique"
msgstr "Synthèse par thématique" msgstr "Synthèse par thématique"
#: streetup/contracts/views_daily_report.py:251 #: streetup/contracts/views_daily_report.py:265
#: streetup/contracts/views_daily_report.py:403 #: streetup/contracts/views_daily_report.py:417
msgid "Thématique" msgid "Thématique"
msgstr "Thématique" msgstr "Thématique"
#: streetup/contracts/views_daily_report.py:302 #: streetup/contracts/views_daily_report.py:316
#: streetup/contracts/views_daily_report.py:344 #: streetup/contracts/views_daily_report.py:358
#: streetup/contracts/views_daily_report.py:433 #: streetup/contracts/views_daily_report.py:447
#: streetup/contracts/views_daily_report.py:481 #: streetup/contracts/views_daily_report.py:495
msgid "TOTAL" msgid "TOTAL"
msgstr "TOTAL" msgstr "TOTAL"
#: streetup/contracts/views_daily_report.py:314 #: streetup/contracts/views_daily_report.py:328
msgid "Nombre d'interventions par personne" msgid "Nombre d'interventions par personne"
msgstr "Nombre d'interventions par personne" msgstr "Nombre d'interventions par personne"
#: streetup/contracts/views_daily_report.py:317 #: streetup/contracts/views_daily_report.py:331
#: streetup/contracts/views_daily_report.py:459 #: streetup/contracts/views_daily_report.py:473
msgid "Personne" msgid "Personne"
msgstr "Personne" msgstr "Personne"
#: streetup/contracts/views_daily_report.py:317 #: streetup/contracts/views_daily_report.py:331
#: streetup/contracts/views_daily_report.py:459 #: streetup/contracts/views_daily_report.py:473
msgid "Nombre" msgid "Nombre"
msgstr "Nombre" msgstr "Nombre"
#: streetup/contracts/views_daily_report.py:397 #: streetup/contracts/views_daily_report.py:411
#: streetup/contracts/views_daily_report.py:454 #: streetup/contracts/views_daily_report.py:468
msgid "Rapport journalier" msgid "Rapport journalier"
msgstr "Rapport journalier" msgstr "Rapport journalier"
#: streetup/contracts/views_daily_report.py:451 #: streetup/contracts/views_daily_report.py:465
msgid "Par personne" msgid "Par personne"
msgstr "Par personne" msgstr "Par personne"
#: streetup/contracts/views_daily_report.py:518 #: streetup/contracts/views_daily_report.py:532
msgid "Aucune de vos équipes n'est autorisée à générer ce rapport." msgid "Aucune de vos équipes n'est autorisée à générer ce rapport."
msgstr "Aucune de vos équipes n'est autorisée à générer ce rapport." msgstr "Aucune de vos équipes n'est autorisée à générer ce rapport."

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-10 11:25+0200\n" "POT-Creation-Date: 2026-07-27 09:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -41,7 +41,7 @@ msgid "Date de la commande"
msgstr "Besteldatum" msgstr "Besteldatum"
#: streetup/contracts/forms.py:87 #: streetup/contracts/forms.py:87
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:106 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:89
msgid "Date de livraison" msgid "Date de livraison"
msgstr "Leverdatum" msgstr "Leverdatum"
@ -50,7 +50,7 @@ msgid "Statut de la commande"
msgstr "Status van de bestelling" msgstr "Status van de bestelling"
#: streetup/contracts/forms.py:97 #: streetup/contracts/forms.py:97
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:202 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:185
msgid "Fichier de commande" msgid "Fichier de commande"
msgstr "Bestandsstuk van de bestelling" msgstr "Bestandsstuk van de bestelling"
@ -74,9 +74,9 @@ msgstr "Bestelreferentie"
#: streetup/contracts/templates/contracts/contracts_index.html:241 #: streetup/contracts/templates/contracts/contracts_index.html:241
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:71 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:71
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:102 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:85
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:497 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:480
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:585 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:568
#: streetup/contracts/views_pdf_reports.py:580 #: streetup/contracts/views_pdf_reports.py:580
#: streetup/contracts/views_pdf_reports.py:718 #: streetup/contracts/views_pdf_reports.py:718
#: streetup/contracts/views_pdf_reports.py:946 #: streetup/contracts/views_pdf_reports.py:946
@ -86,8 +86,8 @@ msgstr "Beschrijving"
#: streetup/contracts/forms.py:136 streetup/contracts/models.py:950 #: streetup/contracts/forms.py:136 streetup/contracts/models.py:950
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:74 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:74
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:588 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:571
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:709 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:692
#: streetup/contracts/views_pdf_reports.py:949 #: streetup/contracts/views_pdf_reports.py:949
#: streetup/contracts/views_pdf_reports.py:1142 #: streetup/contracts/views_pdf_reports.py:1142
msgid "Quantité" msgid "Quantité"
@ -217,8 +217,6 @@ msgid ""
msgstr "" msgstr ""
#: streetup/contracts/models.py:114 #: streetup/contracts/models.py:114
#, fuzzy
#| msgid "Nombre d'interventions par personne"
msgid "Restreindre aux interventions assignées" msgid "Restreindre aux interventions assignées"
msgstr "Aantal interventies per persoon" msgstr "Aantal interventies per persoon"
@ -237,8 +235,6 @@ msgid "Équipes de sociétés/organismes"
msgstr "Teams van bedrijven/organisaties" msgstr "Teams van bedrijven/organisaties"
#: streetup/contracts/models.py:135 #: streetup/contracts/models.py:135
#, fuzzy
#| msgid "Équipe"
msgid "Équipes" msgid "Équipes"
msgstr "Team" msgstr "Team"
@ -261,8 +257,6 @@ msgstr ""
"altijd zichtbaar." "altijd zichtbaar."
#: streetup/contracts/models.py:178 #: streetup/contracts/models.py:178
#, fuzzy
#| msgid "Nombre"
msgid "Membre" msgid "Membre"
msgstr "Aantal" msgstr "Aantal"
@ -272,20 +266,14 @@ msgid "Équipe"
msgstr "Team" msgstr "Team"
#: streetup/contracts/models.py:188 #: streetup/contracts/models.py:188
#, fuzzy
#| msgid "Date de création"
msgid "Date d'entrée" msgid "Date d'entrée"
msgstr "Aanmaakdatum" msgstr "Aanmaakdatum"
#: streetup/contracts/models.py:189 #: streetup/contracts/models.py:189
#, fuzzy
#| msgid "Date de création"
msgid "Date de sortie" msgid "Date de sortie"
msgstr "Aanmaakdatum" msgstr "Aanmaakdatum"
#: streetup/contracts/models.py:196 #: streetup/contracts/models.py:196
#, fuzzy
#| msgid "Commandé par"
msgid "Modifié par" msgid "Modifié par"
msgstr "Besteld door" msgstr "Besteld door"
@ -418,8 +406,6 @@ msgid ""
msgstr "" msgstr ""
#: streetup/contracts/models.py:316 #: streetup/contracts/models.py:316
#, fuzzy
#| msgid "Impossible de figer un devis vide."
msgid "Utilise les gestionnaires d'intervention" msgid "Utilise les gestionnaires d'intervention"
msgstr "Kan een lege offerte niet bevriezen." msgstr "Kan een lege offerte niet bevriezen."
@ -450,8 +436,6 @@ msgid ""
msgstr "" msgstr ""
#: streetup/contracts/models.py:331 #: streetup/contracts/models.py:331
#, fuzzy
#| msgid "Rapport journalier"
msgid "Envoi groupé journalier" msgid "Envoi groupé journalier"
msgstr "Dagelijks rapport" msgstr "Dagelijks rapport"
@ -528,7 +512,7 @@ msgstr "Schuldvordering"
#: streetup/contracts/models.py:856 #: streetup/contracts/models.py:856
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:52 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:52
#: streetup/contracts/templates/contracts/contracts_detail.html:116 #: streetup/contracts/templates/contracts/contracts_detail.html:116
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:768 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:751
#: streetup/contracts/templates/contracts/teams_index.html:741 #: streetup/contracts/templates/contracts/teams_index.html:741
msgid "Titre" msgid "Titre"
msgstr "Titel" msgstr "Titel"
@ -562,43 +546,41 @@ msgid "Quantité consommée à cette date"
msgstr "Op deze datum verbruikte hoeveelheid" msgstr "Op deze datum verbruikte hoeveelheid"
#: streetup/contracts/models.py:926 #: streetup/contracts/models.py:926
#, fuzzy
#| msgid "Date de livraison"
msgid "En attente de validation" msgid "En attente de validation"
msgstr "Leverdatum" msgstr "Leverdatum"
#: streetup/contracts/models.py:927 #: streetup/contracts/models.py:927
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:616 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:599
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:926 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:909
msgid "Approuvé" msgid "Approuvé"
msgstr "" msgstr ""
#: streetup/contracts/models.py:928 #: streetup/contracts/models.py:928
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:621 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:604
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:927 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:910
msgid "Refusé" msgid "Refusé"
msgstr "" msgstr ""
#: streetup/contracts/models.py:929 #: streetup/contracts/models.py:929
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:623 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:606
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:929 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:912
msgid "Facturé" msgid "Facturé"
msgstr "Gefactureerd" msgstr "Gefactureerd"
#: streetup/contracts/models.py:939 streetup/contracts/models.py:1029 #: streetup/contracts/models.py:939 streetup/contracts/models.py:1029
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:53 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:53
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:86 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:69
msgid "Commande" msgid "Commande"
msgstr "Bestelling" msgstr "Bestelling"
#: streetup/contracts/models.py:945 #: streetup/contracts/models.py:945
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:699 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:682
msgid "Poste de marché" msgid "Poste de marché"
msgstr "" msgstr ""
#: streetup/contracts/models.py:952 #: streetup/contracts/models.py:952
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:591 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:574
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:713 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:696
msgid "Commentaire" msgid "Commentaire"
msgstr "Opmerking" msgstr "Opmerking"
@ -616,8 +598,6 @@ msgid "Créé par"
msgstr "Aangemaakt door" msgstr "Aangemaakt door"
#: streetup/contracts/models.py:968 #: streetup/contracts/models.py:968
#, fuzzy
#| msgid "Date de livraison"
msgid "Date de validation" msgid "Date de validation"
msgstr "Leverdatum" msgstr "Leverdatum"
@ -626,26 +606,18 @@ msgid "Validé par"
msgstr "Gevalideerd door" msgstr "Gevalideerd door"
#: streetup/contracts/models.py:981 #: streetup/contracts/models.py:981
#, fuzzy
#| msgid "Quantité prévue"
msgid "Quantité déjà facturée" msgid "Quantité déjà facturée"
msgstr "Geplande hoeveelheid" msgstr "Geplande hoeveelheid"
#: streetup/contracts/models.py:982 #: streetup/contracts/models.py:982
#, fuzzy
#| msgid "Générer la déclaration de créance"
msgid "Quantité cumulée sur les déclarations de créance précédentes." msgid "Quantité cumulée sur les déclarations de créance précédentes."
msgstr "Schuldvordering genereren" msgstr "Schuldvordering genereren"
#: streetup/contracts/models.py:1017 #: streetup/contracts/models.py:1017
#, fuzzy
#| msgid "Consommation des postes"
msgid "Commande directe – poste" msgid "Commande directe – poste"
msgstr "Verbruik van de posten" msgstr "Verbruik van de posten"
#: streetup/contracts/models.py:1018 #: streetup/contracts/models.py:1018
#, fuzzy
#| msgid "Consommation des postes"
msgid "Commandes directes – postes" msgid "Commandes directes – postes"
msgstr "Verbruik van de posten" msgstr "Verbruik van de posten"
@ -690,7 +662,7 @@ msgid "Contrats"
msgstr "Contracten" msgstr "Contracten"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:29 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:29
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:474 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:457
msgid "Justificatifs" msgid "Justificatifs"
msgstr "Bewijsstukken" msgstr "Bewijsstukken"
@ -707,7 +679,7 @@ msgid "Ajouter un justificatif"
msgstr "Bewijsstuk toevoegen" msgstr "Bewijsstuk toevoegen"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:90 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:90
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:313 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:296
#: streetup/contracts/templates/contracts/teams_index.html:324 #: streetup/contracts/templates/contracts/teams_index.html:324
msgid "Ajouter" msgid "Ajouter"
msgstr "Toevoegen" msgstr "Toevoegen"
@ -717,7 +689,7 @@ msgid "Justificatifs attachés"
msgstr "Bijgevoegde bewijsstukken" msgstr "Bijgevoegde bewijsstukken"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:117 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:117
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:466 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:449
msgid "Ajouté le" msgid "Ajouté le"
msgstr "Toegevoegd op" msgstr "Toegevoegd op"
@ -726,8 +698,8 @@ msgid "par"
msgstr "door" msgstr "door"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:125 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:125
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:332 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:315
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:333 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:316
msgid "Télécharger" msgid "Télécharger"
msgstr "Downloaden" msgstr "Downloaden"
@ -736,11 +708,11 @@ msgid "Êtes-vous sûr de vouloir supprimer ce justificatif ?"
msgstr "Weet u zeker dat u dit bewijsstuk wilt verwijderen?" msgstr "Weet u zeker dat u dit bewijsstuk wilt verwijderen?"
#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:130 #: streetup/contracts/templates/contracts/claim_declaration_attachments.html:130
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:238 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:221
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:290 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:273
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:642 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:625
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:743 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:726
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:949 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:932
msgid "Supprimer" msgid "Supprimer"
msgstr "Verwijderen" msgstr "Verwijderen"
@ -762,7 +734,7 @@ msgstr "Detail van opdracht/contract"
#: streetup/contracts/templates/contracts/contracts_detail.html:28 #: streetup/contracts/templates/contracts/contracts_detail.html:28
#: streetup/contracts/templates/contracts/contracts_detail.html:117 #: streetup/contracts/templates/contracts/contracts_detail.html:117
#: streetup/contracts/templates/contracts/contracts_index.html:243 #: streetup/contracts/templates/contracts/contracts_index.html:243
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:769 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:752
msgid "Début" msgid "Début"
msgstr "Begin" msgstr "Begin"
@ -796,7 +768,7 @@ msgid "Montant \"presté"
msgstr "Gepresteerd bedrag" msgstr "Gepresteerd bedrag"
#: streetup/contracts/templates/contracts/contracts_detail.html:67 #: streetup/contracts/templates/contracts/contracts_detail.html:67
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:135 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:118
msgid "Montant validé" msgid "Montant validé"
msgstr "Goedgekeurd bedrag" msgstr "Goedgekeurd bedrag"
@ -805,7 +777,7 @@ msgid "Dossier interne"
msgstr "Intern dossier" msgstr "Intern dossier"
#: streetup/contracts/templates/contracts/contracts_detail.html:96 #: streetup/contracts/templates/contracts/contracts_detail.html:96
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:212 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:195
msgid "Retour à la liste" msgid "Retour à la liste"
msgstr "Terug naar de lijst" msgstr "Terug naar de lijst"
@ -816,9 +788,9 @@ msgstr "Bestellingen"
#: streetup/contracts/templates/contracts/contracts_detail.html:115 #: streetup/contracts/templates/contracts/contracts_detail.html:115
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:70 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:70
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:496 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:479
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:584 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:567
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:767 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:750
#: streetup/contracts/templates/contracts/teams_index.html:740 #: streetup/contracts/templates/contracts/teams_index.html:740
#: streetup/contracts/views_pdf_reports.py:945 #: streetup/contracts/views_pdf_reports.py:945
#: streetup/contracts/views_pdf_reports.py:1138 #: streetup/contracts/views_pdf_reports.py:1138
@ -827,8 +799,8 @@ msgstr "Code"
#: streetup/contracts/templates/contracts/contracts_detail.html:118 #: streetup/contracts/templates/contracts/contracts_detail.html:118
#: streetup/contracts/templates/contracts/contracts_index.html:200 #: streetup/contracts/templates/contracts/contracts_index.html:200
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:592 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:575
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:770 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:753
#: streetup/contracts/templates/contracts/teams_index.html:743 #: streetup/contracts/templates/contracts/teams_index.html:743
msgid "Statut" msgid "Statut"
msgstr "Status" msgstr "Status"
@ -907,7 +879,7 @@ msgid "Devis pour la commande %(order_code)s"
msgstr "Offerte voor bestelling %(order_code)s" msgstr "Offerte voor bestelling %(order_code)s"
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:112 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:95
msgid "Non renseigné" msgid "Non renseigné"
msgstr "Niet gespecificeerd" msgstr "Niet gespecificeerd"
@ -930,20 +902,20 @@ msgid "N° de poste"
msgstr "Postnr." msgstr "Postnr."
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:72 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:72
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:586 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:569
#: streetup/contracts/views_pdf_reports.py:947 #: streetup/contracts/views_pdf_reports.py:947
#: streetup/contracts/views_pdf_reports.py:1140 #: streetup/contracts/views_pdf_reports.py:1140
msgid "Unité" msgid "Unité"
msgstr "Eenheid" msgstr "Eenheid"
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:73 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:73
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:510 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:493
#: streetup/contracts/views_pdf_reports.py:1141 #: streetup/contracts/views_pdf_reports.py:1141
msgid "Prix unitaire" msgid "Prix unitaire"
msgstr "Eenheidsprijs" msgstr "Eenheidsprijs"
#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:75 #: streetup/contracts/templates/contracts/contracts_order_quote_form.html:75
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:590 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:573
#: streetup/contracts/views_pdf_reports.py:1143 #: streetup/contracts/views_pdf_reports.py:1143
msgid "Montant" msgid "Montant"
msgstr "Bedrag" msgstr "Bedrag"
@ -974,39 +946,39 @@ msgstr "Offerte opslaan"
msgid "Figer le devis" msgid "Figer le devis"
msgstr "Offerte bevriezen" msgstr "Offerte bevriezen"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:64 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:47
msgid "Détail de la commande" msgid "Détail de la commande"
msgstr "Detail van de bestelling" msgstr "Detail van de bestelling"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:99 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:82
msgid "Contrat lié" msgid "Contrat lié"
msgstr "Gekoppeld contract" msgstr "Gekoppeld contract"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:105 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:88
msgid "Date de commande" msgid "Date de commande"
msgstr "Besteldatum" msgstr "Besteldatum"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:111 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:94
msgid "Montant prévisionnel" msgid "Montant prévisionnel"
msgstr "Raming bedrag" msgstr "Raming bedrag"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:117 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:100
msgid "Montant \"presté\"" msgid "Montant \"presté\""
msgstr "Gepresteerd bedrag" msgstr "Gepresteerd bedrag"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:156 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:139
msgid "Devis" msgid "Devis"
msgstr "Offerte" msgstr "Offerte"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:158 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:141
msgid "Montant total" msgid "Montant total"
msgstr "Totaalbedrag" msgstr "Totaalbedrag"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:160 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:143
msgid "Devis figé" msgid "Devis figé"
msgstr "Bevroren offerte" msgstr "Bevroren offerte"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:167 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:150
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -1025,85 +997,85 @@ msgstr[1] ""
" %(item_count)s posten in de offerte.\n" " %(item_count)s posten in de offerte.\n"
" " " "
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:174 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:157
msgid "Aucun devis n'est associé à cette commande pour le moment." msgid "Aucun devis n'est associé à cette commande pour le moment."
msgstr "Momenteel is er geen offerte gekoppeld aan deze bestelling." msgstr "Momenteel is er geen offerte gekoppeld aan deze bestelling."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:181 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:164
msgid "Voir le devis" msgid "Voir le devis"
msgstr "Offerte bekijken" msgstr "Offerte bekijken"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:183 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:166
msgid "Gérer le devis" msgid "Gérer le devis"
msgstr "Offerte beheren" msgstr "Offerte beheren"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:189 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:172
msgid "Exporter en PDF" msgid "Exporter en PDF"
msgstr "Exporteren naar PDF" msgstr "Exporteren naar PDF"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:198 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:181
msgid "Commandé par" msgid "Commandé par"
msgstr "Besteld door" msgstr "Besteld door"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:205 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:188
msgid "Télécharger le fichier" msgid "Télécharger le fichier"
msgstr "Bestand downloaden" msgstr "Bestand downloaden"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:219 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:202
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:329 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:312
msgid "Documents" msgid "Documents"
msgstr "Documenten" msgstr "Documenten"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:221 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:204
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:301 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:284
msgid "Ajouter des documents" msgid "Ajouter des documents"
msgstr "Documenten toevoegen" msgstr "Documenten toevoegen"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:270 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:253
msgid "Aucun document associé à cette commande." msgid "Aucun document associé à cette commande."
msgstr "Geen document gekoppeld aan deze bestelling." msgstr "Geen document gekoppeld aan deze bestelling."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:281 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:264
msgid "Supprimer le document" msgid "Supprimer le document"
msgstr "Document verwijderen" msgstr "Document verwijderen"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:285 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:268
msgid "Voulez-vous supprimer ce document ?" msgid "Voulez-vous supprimer ce document ?"
msgstr "Wilt u dit document verwijderen?" msgstr "Wilt u dit document verwijderen?"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:289 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:272
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:719 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:702
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:742 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:725
#: streetup/contracts/templates/contracts/teams_index.html:322 #: streetup/contracts/templates/contracts/teams_index.html:322
#: streetup/contracts/templates/contracts/teams_index.html:363 #: streetup/contracts/templates/contracts/teams_index.html:363
msgid "Annuler" msgid "Annuler"
msgstr "Annuleren" msgstr "Annuleren"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:374 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:357
msgid "Précédent" msgid "Précédent"
msgstr "Vorige" msgstr "Vorige"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:379 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:362
msgid "Suivant" msgid "Suivant"
msgstr "Volgende" msgstr "Volgende"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:416 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:399
msgid "Documents financiers" msgid "Documents financiers"
msgstr "Financiële documenten" msgstr "Financiële documenten"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:421 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:404
msgid "Générer un draft de l'état de travaux" msgid "Générer un draft de l'état de travaux"
msgstr "Draft van de staat van werken genereren" msgstr "Draft van de staat van werken genereren"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:424 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:407
msgid "Générer un draft de la déclaration de créance" msgid "Générer un draft de la déclaration de créance"
msgstr "Draft van de schuldvordering genereren" msgstr "Draft van de schuldvordering genereren"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:430 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:413
msgid "Générer la déclaration de créance" msgid "Générer la déclaration de créance"
msgstr "Schuldvordering genereren" msgstr "Schuldvordering genereren"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:434 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:417
msgid "" msgid ""
"Cette opération va générer un état de travaux, une déclaration de créance et " "Cette opération va générer un état de travaux, une déclaration de créance et "
"faire passer les interventions validées au statut \"comptabilisé\"." "faire passer les interventions validées au statut \"comptabilisé\"."
@ -1111,186 +1083,168 @@ msgstr ""
"Deze actie zal een staat van werken en een schuldvordering genereren en de " "Deze actie zal een staat van werken en een schuldvordering genereren en de "
"gevalideerde interventies op de status \"gefactureerd\" zetten." "gevalideerde interventies op de status \"gefactureerd\" zetten."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:444 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:427
#, python-format #, python-format
msgid "État d'avancement %(num)s" msgid "État d'avancement %(num)s"
msgstr "Voortgangsrapport %(num)s" msgstr "Voortgangsrapport %(num)s"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:448 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:431
msgid "Autres documents" msgid "Autres documents"
msgstr "Andere documenten" msgstr "Andere documenten"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:465 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:448
msgid "Type" msgid "Type"
msgstr "Type" msgstr "Type"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:468 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:451
msgid "justificatif(s)" msgid "justificatif(s)"
msgstr "bewijsstuk(ken)" msgstr "bewijsstuk(ken)"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:473 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:456
msgid "Gérer les justificatifs" msgid "Gérer les justificatifs"
msgstr "Bewijsstukken beheren" msgstr "Bewijsstukken beheren"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:487 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:470
msgid "Consommation des postes" msgid "Consommation des postes"
msgstr "Verbruik van de posten" msgstr "Verbruik van de posten"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:495 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:478
msgid "Poste" msgid "Poste"
msgstr "Post" msgstr "Post"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:499 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:482
msgid "Quantité prévue" msgid "Quantité prévue"
msgstr "Geplande hoeveelheid" msgstr "Geplande hoeveelheid"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:502 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:485
msgid "État d avancement du" msgid "État d avancement du"
msgstr "Voortgangsstatus van" msgstr "Voortgangsstatus van"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:503 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:486
msgid "État" msgid "État"
msgstr "Status" msgstr "Status"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:506 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:489
msgid "Quantité consommée" msgid "Quantité consommée"
msgstr "Verbruikte hoeveelheid" msgstr "Verbruikte hoeveelheid"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:508 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:491
msgid "Quantité restante" msgid "Quantité restante"
msgstr "Resterende hoeveelheid" msgstr "Resterende hoeveelheid"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:511 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:494
msgid "Montant consommé" msgid "Montant consommé"
msgstr "Verbruikt bedrag" msgstr "Verbruikt bedrag"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:550 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:533
msgid "Aucune consommation enregistrée pour cette commande." msgid "Aucune consommation enregistrée pour cette commande."
msgstr "Geen verbruik geregistreerd voor deze bestelling." msgstr "Geen verbruik geregistreerd voor deze bestelling."
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:557 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:540
msgid "Projets associés" msgid "Projets associés"
msgstr "Gekoppelde projecten" msgstr "Gekoppelde projecten"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:567 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:550
#: streetup/contracts/views_pdf_reports.py:713 #: streetup/contracts/views_pdf_reports.py:713
#, fuzzy
#| msgid "Commandes"
msgid "Commandes directes" msgid "Commandes directes"
msgstr "Bestellingen" msgstr "Bestellingen"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:569 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:552
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:693 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:676
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1110 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1093
#, fuzzy
#| msgid "Ajouter un justificatif"
msgid "Ajouter un poste direct" msgid "Ajouter un poste direct"
msgstr "Bewijsstuk toevoegen" msgstr "Bewijsstuk toevoegen"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:577 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:560
msgid "" msgid ""
"Les commandes directes permettent d'enregistrer des postes de marché " "Les commandes directes permettent d'enregistrer des postes de marché "
"consommés sans passer par une intervention. Les postes validés sont inclus " "consommés sans passer par une intervention. Les postes validés sont inclus "
"dans les totaux, les états d'avancement et les déclarations de créance." "dans les totaux, les états d'avancement et les déclarations de créance."
msgstr "" msgstr ""
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:583 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:566
msgid "N°" msgid "N°"
msgstr "Nr" msgstr "Nr"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:587 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:570
#, fuzzy
#| msgid "Prix unitaire"
msgid "Prix unit." msgid "Prix unit."
msgstr "Eenheidsprijs" msgstr "Eenheidsprijs"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:589 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:572
#, fuzzy
#| msgid "Facturé"
msgid "Qté facturée" msgid "Qté facturée"
msgstr "Gefactureerd" msgstr "Gefactureerd"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:593 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:576
msgid "Actions" msgid "Actions"
msgstr "Acties" msgstr "Acties"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:625 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:608
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:928 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:911
msgid "En attente" msgid "En attente"
msgstr "" msgstr ""
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:635 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:618
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:944 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:927
msgid "Modifier" msgid "Modifier"
msgstr "Bewerken" msgstr "Bewerken"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:652 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:635
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:956 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:939
msgid "Valider" msgid "Valider"
msgstr "Valideren" msgstr "Valideren"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:660 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:643
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:961 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:944
msgid "Refuser" msgid "Refuser"
msgstr "" msgstr ""
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:668 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:651
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:966 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:949
msgid "Remettre en attente" msgid "Remettre en attente"
msgstr "" msgstr ""
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:678 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:661
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1213 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1196
#, fuzzy
#| msgid "Aucune commande trouvée"
msgid "Aucune commande directe enregistrée." msgid "Aucune commande directe enregistrée."
msgstr "Geen bestelling gevonden" msgstr "Geen bestelling gevonden"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:700 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:683
msgid "Rechercher par code, numéro ou description…" msgid "Rechercher par code, numéro ou description…"
msgstr "" msgstr ""
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:721 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:704
#: streetup/contracts/templates/contracts/teams_index.html:365 #: streetup/contracts/templates/contracts/teams_index.html:365
msgid "Enregistrer" msgid "Enregistrer"
msgstr "Opslaan" msgstr "Opslaan"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:735 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:718
#, fuzzy
#| msgid "Supprimer le document"
msgid "Supprimer le poste direct" msgid "Supprimer le poste direct"
msgstr "Document verwijderen" msgstr "Document verwijderen"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:739 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:722
#, fuzzy
#| msgid "Voulez-vous supprimer ce document ?"
msgid "Voulez-vous supprimer ce poste direct ?" msgid "Voulez-vous supprimer ce poste direct ?"
msgstr "Wilt u dit document verwijderen?" msgstr "Wilt u dit document verwijderen?"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:755 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:738
msgid "Ajouter une intervention" msgid "Ajouter une intervention"
msgstr "Interventie toevoegen" msgstr "Interventie toevoegen"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1107 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1090
#, fuzzy
#| msgid "Modifier la commande"
msgid "Modifier le poste direct" msgid "Modifier le poste direct"
msgstr "Bestelling bewerken" msgstr "Bestelling bewerken"
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1143 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1126
msgid "Veuillez sélectionner un poste de marché." msgid "Veuillez sélectionner un poste de marché."
msgstr "" msgstr ""
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1173 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1156
#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1178 #: streetup/contracts/templates/contracts/contracts_orders_detail.html:1161
#: streetup/contracts/templates/contracts/teams_index.html:436 #: streetup/contracts/templates/contracts/teams_index.html:436
#: streetup/contracts/templates/contracts/teams_index.html:440 #: streetup/contracts/templates/contracts/teams_index.html:440
msgid "Une erreur est survenue." msgid "Une erreur est survenue."
msgstr "Er is een fout opgetreden." msgstr "Er is een fout opgetreden."
#: streetup/contracts/templates/contracts/teams_index.html:130 #: streetup/contracts/templates/contracts/teams_index.html:130
#, fuzzy
#| msgid "Consommation des postes"
msgid "Gestion des équipes" msgid "Gestion des équipes"
msgstr "Verbruik van de posten" msgstr "Verbruik van de posten"
@ -1300,21 +1254,15 @@ msgstr ""
#: streetup/contracts/templates/contracts/teams_index.html:150 #: streetup/contracts/templates/contracts/teams_index.html:150
#: streetup/contracts/templates/contracts/teams_index.html:157 #: streetup/contracts/templates/contracts/teams_index.html:157
#, fuzzy
#| msgid "Non renseigné"
msgid "Non assignés" msgid "Non assignés"
msgstr "Niet gespecificeerd" msgstr "Niet gespecificeerd"
#: streetup/contracts/templates/contracts/teams_index.html:152 #: streetup/contracts/templates/contracts/teams_index.html:152
#, fuzzy
#| msgid "Aucun projet"
msgid "Aucune équipe" msgid "Aucune équipe"
msgstr "Geen project" msgstr "Geen project"
#: streetup/contracts/templates/contracts/teams_index.html:156 #: streetup/contracts/templates/contracts/teams_index.html:156
#: streetup/contracts/templates/contracts/teams_index.html:578 #: streetup/contracts/templates/contracts/teams_index.html:578
#, fuzzy
#| msgid "Ajouter une intervention"
msgid "Ajouter un membre non assigné" msgid "Ajouter un membre non assigné"
msgstr "Interventie toevoegen" msgstr "Interventie toevoegen"
@ -1325,8 +1273,6 @@ msgstr ""
#: streetup/contracts/templates/contracts/teams_index.html:185 #: streetup/contracts/templates/contracts/teams_index.html:185
#: streetup/contracts/templates/contracts/teams_index.html:258 #: streetup/contracts/templates/contracts/teams_index.html:258
#, fuzzy
#| msgid "Supprimer le document"
msgid "Supprimer ce membre" msgid "Supprimer ce membre"
msgstr "Document verwijderen" msgstr "Document verwijderen"
@ -1341,16 +1287,12 @@ msgid "membre(s)"
msgstr "lid(s)" msgstr "lid(s)"
#: streetup/contracts/templates/contracts/teams_index.html:219 #: streetup/contracts/templates/contracts/teams_index.html:219
#, fuzzy
#| msgid "Nombre d'interventions par personne"
msgid "Voir les interventions assignées" msgid "Voir les interventions assignées"
msgstr "Aantal interventies per persoon" msgstr "Aantal interventies per persoon"
#: streetup/contracts/templates/contracts/teams_index.html:227 #: streetup/contracts/templates/contracts/teams_index.html:227
#: streetup/contracts/templates/contracts/teams_index.html:298 #: streetup/contracts/templates/contracts/teams_index.html:298
#: streetup/contracts/templates/contracts/teams_index.html:577 #: streetup/contracts/templates/contracts/teams_index.html:577
#, fuzzy
#| msgid "Ajouter une intervention"
msgid "Ajouter un membre" msgid "Ajouter un membre"
msgstr "Interventie toevoegen" msgstr "Interventie toevoegen"
@ -1376,8 +1318,6 @@ msgid "Sélectionner un utilisateur"
msgstr "" msgstr ""
#: streetup/contracts/templates/contracts/teams_index.html:337 #: streetup/contracts/templates/contracts/teams_index.html:337
#, fuzzy
#| msgid "Modifier la commande"
msgid "Modifier le membre" msgid "Modifier le membre"
msgstr "Bestelling bewerken" msgstr "Bestelling bewerken"
@ -1401,14 +1341,10 @@ msgstr ""
#: streetup/contracts/templates/contracts/teams_index.html:402 #: streetup/contracts/templates/contracts/teams_index.html:402
#: streetup/contracts/templates/contracts/teams_index.html:403 #: streetup/contracts/templates/contracts/teams_index.html:403
#, fuzzy
#| msgid "Ajouter une intervention"
msgid "Résumé intervention" msgid "Résumé intervention"
msgstr "Interventie toevoegen" msgstr "Interventie toevoegen"
#: streetup/contracts/templates/contracts/teams_index.html:410 #: streetup/contracts/templates/contracts/teams_index.html:410
#, fuzzy
#| msgid "Ajouter une intervention"
msgid "Retour aux interventions" msgid "Retour aux interventions"
msgstr "Interventie toevoegen" msgstr "Interventie toevoegen"
@ -1418,20 +1354,14 @@ msgstr "Sluiten"
#: streetup/contracts/templates/contracts/teams_index.html:551 #: streetup/contracts/templates/contracts/teams_index.html:551
#: streetup/contracts/templates/contracts/teams_index.html:633 #: streetup/contracts/templates/contracts/teams_index.html:633
#, fuzzy
#| msgid "Le contrat est requis"
msgid "Le nom du membre est requis." msgid "Le nom du membre est requis."
msgstr "Contract is vereist" msgstr "Contract is vereist"
#: streetup/contracts/templates/contracts/teams_index.html:634 #: streetup/contracts/templates/contracts/teams_index.html:634
#, fuzzy
#| msgid "Le contrat est requis"
msgid "L\\'utilisateur est requis." msgid "L\\'utilisateur est requis."
msgstr "Contract is vereist" msgstr "Contract is vereist"
#: streetup/contracts/templates/contracts/teams_index.html:651 #: streetup/contracts/templates/contracts/teams_index.html:651
#, fuzzy
#| msgid "Supprimer le document"
msgid "Supprimer le membre" msgid "Supprimer le membre"
msgstr "Document verwijderen" msgstr "Document verwijderen"
@ -1440,26 +1370,18 @@ msgid "Cette action est irréversible."
msgstr "Deze actie is onomkeerbaar." msgstr "Deze actie is onomkeerbaar."
#: streetup/contracts/templates/contracts/teams_index.html:700 #: streetup/contracts/templates/contracts/teams_index.html:700
#, fuzzy
#| msgid "Impossible de figer un devis vide."
msgid "Impossible de charger les interventions." msgid "Impossible de charger les interventions."
msgstr "Kan een lege offerte niet bevriezen." msgstr "Kan een lege offerte niet bevriezen."
#: streetup/contracts/templates/contracts/teams_index.html:706 #: streetup/contracts/templates/contracts/teams_index.html:706
#, fuzzy
#| msgid "Ajouter une intervention"
msgid "intervention(s)" msgid "intervention(s)"
msgstr "Interventie toevoegen" msgstr "Interventie toevoegen"
#: streetup/contracts/templates/contracts/teams_index.html:709 #: streetup/contracts/templates/contracts/teams_index.html:709
#, fuzzy
#| msgid "Aucun document associé à cette commande."
msgid "Aucune intervention assignée à cette équipe." msgid "Aucune intervention assignée à cette équipe."
msgstr "Geen document gekoppeld aan deze bestelling." msgstr "Geen document gekoppeld aan deze bestelling."
#: streetup/contracts/templates/contracts/teams_index.html:721 #: streetup/contracts/templates/contracts/teams_index.html:721
#, fuzzy
#| msgid "Voir le document"
msgid "Voir le résumé" msgid "Voir le résumé"
msgstr "Document bekijken" msgstr "Document bekijken"
@ -1616,14 +1538,10 @@ msgstr ""
#: streetup/contracts/views.py:1081 streetup/contracts/views.py:1153 #: streetup/contracts/views.py:1081 streetup/contracts/views.py:1153
#: streetup/contracts/views.py:1221 #: streetup/contracts/views.py:1221
#, fuzzy
#| msgid "Données JSON invalides"
msgid "Données JSON invalides." msgid "Données JSON invalides."
msgstr "Ongeldige JSON-gegevens" msgstr "Ongeldige JSON-gegevens"
#: streetup/contracts/views.py:1088 #: streetup/contracts/views.py:1088
#, fuzzy
#| msgid "La date de commande est requise"
msgid "Le poste de marché est requis." msgid "Le poste de marché est requis."
msgstr "Besteldatum is vereist" msgstr "Besteldatum is vereist"
@ -1638,8 +1556,6 @@ msgid ""
msgstr "" msgstr ""
#: streetup/contracts/views.py:1148 #: streetup/contracts/views.py:1148
#, fuzzy
#| msgid "Impossible de figer un devis vide."
msgid "Impossible de modifier un poste déjà validé." msgid "Impossible de modifier un poste déjà validé."
msgstr "Kan een lege offerte niet bevriezen." msgstr "Kan een lege offerte niet bevriezen."
@ -1648,62 +1564,58 @@ msgid "La quantité doit être un nombre positif ou nul."
msgstr "" msgstr ""
#: streetup/contracts/views.py:1198 #: streetup/contracts/views.py:1198
#, fuzzy
#| msgid "Impossible de figer un devis vide."
msgid "Impossible de supprimer un poste déjà validé." msgid "Impossible de supprimer un poste déjà validé."
msgstr "Kan een lege offerte niet bevriezen." msgstr "Kan een lege offerte niet bevriezen."
#: streetup/contracts/views.py:1200 #: streetup/contracts/views.py:1200
#, fuzzy
#| msgid "Impossible de figer un devis vide."
msgid "Impossible de supprimer un poste déjà facturé." msgid "Impossible de supprimer un poste déjà facturé."
msgstr "Kan een lege offerte niet bevriezen." msgstr "Kan een lege offerte niet bevriezen."
#: streetup/contracts/views_daily_report.py:227 #: streetup/contracts/views_daily_report.py:241
msgid "Rapport journalier des interventions" msgid "Rapport journalier des interventions"
msgstr "Dagelijks interventierapport" msgstr "Dagelijks interventierapport"
#: streetup/contracts/views_daily_report.py:248 #: streetup/contracts/views_daily_report.py:262
#: streetup/contracts/views_daily_report.py:393 #: streetup/contracts/views_daily_report.py:407
msgid "Synthèse par thématique" msgid "Synthèse par thématique"
msgstr "Samenvatting per thema" msgstr "Samenvatting per thema"
#: streetup/contracts/views_daily_report.py:251 #: streetup/contracts/views_daily_report.py:265
#: streetup/contracts/views_daily_report.py:403 #: streetup/contracts/views_daily_report.py:417
msgid "Thématique" msgid "Thématique"
msgstr "Thema" msgstr "Thema"
#: streetup/contracts/views_daily_report.py:302 #: streetup/contracts/views_daily_report.py:316
#: streetup/contracts/views_daily_report.py:344 #: streetup/contracts/views_daily_report.py:358
#: streetup/contracts/views_daily_report.py:433 #: streetup/contracts/views_daily_report.py:447
#: streetup/contracts/views_daily_report.py:481 #: streetup/contracts/views_daily_report.py:495
msgid "TOTAL" msgid "TOTAL"
msgstr "TOTAAL" msgstr "TOTAAL"
#: streetup/contracts/views_daily_report.py:314 #: streetup/contracts/views_daily_report.py:328
msgid "Nombre d'interventions par personne" msgid "Nombre d'interventions par personne"
msgstr "Aantal interventies per persoon" msgstr "Aantal interventies per persoon"
#: streetup/contracts/views_daily_report.py:317 #: streetup/contracts/views_daily_report.py:331
#: streetup/contracts/views_daily_report.py:459 #: streetup/contracts/views_daily_report.py:473
msgid "Personne" msgid "Personne"
msgstr "Persoon" msgstr "Persoon"
#: streetup/contracts/views_daily_report.py:317 #: streetup/contracts/views_daily_report.py:331
#: streetup/contracts/views_daily_report.py:459 #: streetup/contracts/views_daily_report.py:473
msgid "Nombre" msgid "Nombre"
msgstr "Nummer" msgstr "Nummer"
#: streetup/contracts/views_daily_report.py:397 #: streetup/contracts/views_daily_report.py:411
#: streetup/contracts/views_daily_report.py:454 #: streetup/contracts/views_daily_report.py:468
msgid "Rapport journalier" msgid "Rapport journalier"
msgstr "Dagelijks rapport" msgstr "Dagelijks rapport"
#: streetup/contracts/views_daily_report.py:451 #: streetup/contracts/views_daily_report.py:465
msgid "Par personne" msgid "Par personne"
msgstr "Per persoon" msgstr "Per persoon"
#: streetup/contracts/views_daily_report.py:518 #: streetup/contracts/views_daily_report.py:532
msgid "Aucune de vos équipes n'est autorisée à générer ce rapport." msgid "Aucune de vos équipes n'est autorisée à générer ce rapport."
msgstr "Geen van uw teams is gemachtigd dit rapport te genereren." msgstr "Geen van uw teams is gemachtigd dit rapport te genereren."

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more