Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
|
|
@ -25,10 +25,10 @@ venv_old/
|
|||
*.sqlite3
|
||||
|
||||
# Django media & static
|
||||
loko/media/
|
||||
loko/private_media/
|
||||
loko/static/
|
||||
loko/django.log
|
||||
streetup/media/
|
||||
streetup/private_media/
|
||||
streetup/static/
|
||||
streetup/django.log
|
||||
*.log
|
||||
|
||||
# VS Code / Editor config
|
||||
|
|
|
|||
105
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
name: Build and Deploy StreetUp
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: 'Environnement de déploiement (test ou production)'
|
||||
required: true
|
||||
default: 'test'
|
||||
type: choice
|
||||
options:
|
||||
- test
|
||||
- production
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Log in to the Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=test,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'test' }}
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production' }}
|
||||
type=raw,value=test,enable=${{ github.event_name != 'workflow_dispatch' && github.ref == 'refs/heads/main' }}
|
||||
type=raw,value=latest,enable=${{ github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine Target Environment
|
||||
id: target
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
ENV_CHOICE="${{ github.event.inputs.environment }}"
|
||||
else
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
ENV_CHOICE="production"
|
||||
else
|
||||
ENV_CHOICE="test"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$ENV_CHOICE" == "production" ]]; then
|
||||
echo "env=production" >> $GITHUB_OUTPUT
|
||||
echo "host=${{ secrets.SSH_HOST_PROD }}" >> $GITHUB_OUTPUT
|
||||
echo "path=/home/kdeterme/streetup-prod" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "env=test" >> $GITHUB_OUTPUT
|
||||
echo "host=${{ secrets.SSH_HOST_TEST }}" >> $GITHUB_OUTPUT
|
||||
echo "path=/home/kdeterme/streetup-test" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Deploy to Server via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ steps.target.outputs.host }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd ${{ steps.target.outputs.path }}
|
||||
# Pull de la nouvelle image
|
||||
docker compose pull
|
||||
# Relance des conteneurs
|
||||
docker compose up -d
|
||||
# Exécution des migrations de base de données
|
||||
docker compose exec -T web python manage.py migrate --noinput
|
||||
# Collecte des fichiers statiques
|
||||
docker compose exec -T web python manage.py collectstatic --noinput --clear
|
||||
22
.gitignore
vendored
|
|
@ -7,9 +7,8 @@ node_modules/
|
|||
.well-known/
|
||||
*.log
|
||||
*.md
|
||||
!README.md
|
||||
!loko/common/help_docs/
|
||||
!loko/common/help_docs/**/*.md
|
||||
!streetup/common/help_docs/
|
||||
!streetup/common/help_docs/**/*.md
|
||||
|
||||
media/
|
||||
private_media/
|
||||
|
|
@ -25,26 +24,19 @@ __pycache__/
|
|||
*$py.class
|
||||
|
||||
|
||||
loko/.env
|
||||
streetup/.env
|
||||
|
||||
/scripts/
|
||||
!loko/scripts/
|
||||
!loko/scripts/**
|
||||
scripts/
|
||||
|
||||
carl_export/
|
||||
carl_export.*
|
||||
|
||||
loko/static/
|
||||
streetup/static/
|
||||
|
||||
deploy.sh
|
||||
generated_passwords.txt
|
||||
loko/generated_passwords.txt
|
||||
streetup/generated_passwords.txt
|
||||
scratch/
|
||||
Caddyfile
|
||||
Caddyfile.prod
|
||||
.vscode/
|
||||
|
||||
# AI Training & ML Artifacts
|
||||
runs/
|
||||
*.pt
|
||||
loko/sign/ai/training/
|
||||
labels_signpanels/
|
||||
96
Dockerfile
|
|
@ -26,7 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||
|
||||
# Création de l'environnement conda avec les packages cartographiques et poppler
|
||||
# gdal, geos, proj et poppler sont installés depuis conda-forge
|
||||
RUN mamba create -n loko-env -y -c conda-forge \
|
||||
RUN mamba create -n streetup-env -y -c conda-forge \
|
||||
python=3.12 \
|
||||
gdal \
|
||||
geos \
|
||||
|
|
@ -35,19 +35,19 @@ RUN mamba create -n loko-env -y -c conda-forge \
|
|||
&& mamba clean -afy
|
||||
|
||||
# Activation par défaut de l'environnement Conda dans le conteneur
|
||||
ENV PATH=/opt/conda/envs/loko-env/bin:$PATH
|
||||
ENV CONDA_DEFAULT_ENV=loko-env
|
||||
ENV PATH=/opt/conda/envs/streetup-env/bin:$PATH
|
||||
ENV CONDA_DEFAULT_ENV=streetup-env
|
||||
|
||||
# Configuration explicite des variables GIS pour GeoDjango
|
||||
ENV GDAL_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgdal.so
|
||||
ENV GEOS_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgeos_c.so
|
||||
ENV PROJ_DATA=/opt/conda/envs/loko-env/share/proj
|
||||
ENV GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so
|
||||
ENV GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so
|
||||
ENV PROJ_DATA=/opt/conda/envs/streetup-env/share/proj
|
||||
|
||||
# Argument de build pour activer l'installation des librairies IA (ultralytics, OpenCV...)
|
||||
# Passer INSTALL_AI_LIBS=true dans le docker-compose pour les inclure
|
||||
ARG INSTALL_AI_LIBS=false
|
||||
|
||||
# Copie et installation des dépendances Python (mis en cache tant que requirements/ ne change pas)
|
||||
# Copie et installation des dépendances Python
|
||||
COPY requirements/ /app/requirements/
|
||||
RUN pip install --no-cache-dir -r requirements/base.txt && \
|
||||
if [ "$INSTALL_AI_LIBS" = "true" ]; then \
|
||||
|
|
@ -55,60 +55,50 @@ RUN pip install --no-cache-dir -r requirements/base.txt && \
|
|||
fi
|
||||
|
||||
# 1. Copie des répertoires volumineux et statiques (très bonne mise en cache)
|
||||
COPY loko/scripts/ /app/scripts/
|
||||
COPY loko/common/ /app/common/
|
||||
COPY loko/home/ /app/home/
|
||||
COPY streetup/road_detection/detection_models/ /app/road_detection/detection_models/
|
||||
COPY streetup/scripts/ /app/scripts/
|
||||
COPY streetup/common/ /app/common/
|
||||
COPY streetup/home/ /app/home/
|
||||
|
||||
# 2. Copie des modules de l'application qui changent rarement
|
||||
COPY loko/locale/ /app/locale/
|
||||
COPY loko/templates/ /app/templates/
|
||||
COPY loko/analytics/ /app/analytics/
|
||||
COPY loko/api/ /app/api/
|
||||
COPY loko/authorizations/ /app/authorizations/
|
||||
COPY loko/contracts/ /app/contracts/
|
||||
COPY loko/documents/ /app/documents/
|
||||
COPY loko/maps/ /app/maps/
|
||||
COPY loko/notifications/ /app/notifications/
|
||||
COPY loko/projects/ /app/projects/
|
||||
COPY loko/reporting/ /app/reporting/
|
||||
COPY loko/bim/ /app/bim/
|
||||
COPY loko/sign/ /app/sign/
|
||||
COPY loko/stock/ /app/stock/
|
||||
COPY streetup/locale/ /app/locale/
|
||||
COPY streetup/templates/ /app/templates/
|
||||
COPY streetup/analytics/ /app/analytics/
|
||||
COPY streetup/api/ /app/api/
|
||||
COPY streetup/authorizations/ /app/authorizations/
|
||||
COPY streetup/contracts/ /app/contracts/
|
||||
COPY streetup/documents/ /app/documents/
|
||||
COPY streetup/maps/ /app/maps/
|
||||
COPY streetup/notifications/ /app/notifications/
|
||||
COPY streetup/projects/ /app/projects/
|
||||
COPY streetup/reporting/ /app/reporting/
|
||||
COPY streetup/schools/ /app/schools/
|
||||
COPY streetup/sign/ /app/sign/
|
||||
COPY streetup/stock/ /app/stock/
|
||||
|
||||
# 3. Copie des modules actifs ou modifiés fréquemment
|
||||
COPY loko/loko/ /app/loko/
|
||||
COPY loko/mobile/ /app/mobile/
|
||||
COPY loko/observations/ /app/observations/
|
||||
COPY loko/controls/ /app/controls/
|
||||
COPY loko/interventions/ /app/interventions/
|
||||
COPY loko/inspections/ /app/inspections/
|
||||
COPY loko/assets/ /app/assets/
|
||||
COPY loko/panoramax/ /app/panoramax/
|
||||
COPY streetup/streetup/ /app/streetup/
|
||||
COPY streetup/mobile/ /app/mobile/
|
||||
COPY streetup/observations/ /app/observations/
|
||||
COPY streetup/controls/ /app/controls/
|
||||
COPY streetup/interventions/ /app/interventions/
|
||||
COPY streetup/assets/ /app/assets/
|
||||
|
||||
# 4. Copie des fichiers de code de road_detection (sans réimporter les modèles de détection)
|
||||
COPY loko/road_detection/__init__.py /app/road_detection/
|
||||
COPY loko/road_detection/admin.py /app/road_detection/
|
||||
COPY loko/road_detection/apps.py /app/road_detection/
|
||||
COPY loko/road_detection/detection.py /app/road_detection/
|
||||
COPY loko/road_detection/forms.py /app/road_detection/
|
||||
COPY loko/road_detection/models.py /app/road_detection/
|
||||
COPY loko/road_detection/urls.py /app/road_detection/
|
||||
COPY loko/road_detection/views.py /app/road_detection/
|
||||
COPY loko/road_detection/migrations/ /app/road_detection/migrations/
|
||||
COPY loko/road_detection/templates/ /app/road_detection/templates/
|
||||
COPY streetup/road_detection/__init__.py /app/road_detection/
|
||||
COPY streetup/road_detection/admin.py /app/road_detection/
|
||||
COPY streetup/road_detection/apps.py /app/road_detection/
|
||||
COPY streetup/road_detection/detection.py /app/road_detection/
|
||||
COPY streetup/road_detection/forms.py /app/road_detection/
|
||||
COPY streetup/road_detection/models.py /app/road_detection/
|
||||
COPY streetup/road_detection/urls.py /app/road_detection/
|
||||
COPY streetup/road_detection/views.py /app/road_detection/
|
||||
COPY streetup/road_detection/migrations/ /app/road_detection/migrations/
|
||||
COPY streetup/road_detection/templates/ /app/road_detection/templates/
|
||||
|
||||
# 5. Copie des fichiers Python et de configuration restants à la racine
|
||||
COPY loko/*.py /app/
|
||||
COPY loko/generated_passwords.txt* /app/
|
||||
|
||||
# Métadonnées de build dynamiques (placées à la fin pour préserver le cache de build Docker)
|
||||
ARG GIT_COMMIT=unknown
|
||||
ARG BUILD_DATE=unknown
|
||||
ARG LOKO_VERSION=1.0.0
|
||||
|
||||
ENV GIT_COMMIT_SHA=${GIT_COMMIT}
|
||||
ENV BUILD_DATE=${BUILD_DATE}
|
||||
ENV LOKO_VERSION=${LOKO_VERSION}
|
||||
COPY streetup/*.py /app/
|
||||
COPY streetup/generated_passwords.txt* /app/
|
||||
|
||||
# Exposer le port par défaut
|
||||
EXPOSE 8000
|
||||
|
|
|
|||
70
README.md
|
|
@ -1,70 +0,0 @@
|
|||
# Loko — Asset Management pour l'espace public
|
||||
|
||||
<p align="center">
|
||||
<img src="loko/common/static/common/loko_logo.svg" alt="Loko Logo" width="120" height="120">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Loko</strong> est une plateforme open source de gestion de patrimoine d'infrastructures publiques (<em>Public Infrastructure Asset Management</em>), produite et maintenue par <strong><a href="https://www.valogeo.be" target="_blank">Valogeo</a></strong>.
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Présentation
|
||||
|
||||
**Loko** permet aux administrations publiques, collectivités territoriales, gestionnaires d'infrastructures et prestataires de maintenance d'inventorier, de cartographier, de planifier la maintenance et de valoriser l'ensemble des équipements et réseaux 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.
|
||||
|
||||
---
|
||||
|
||||
## 🚦 Thématiques Gérées
|
||||
|
||||
Loko permet de modéliser et de localiser précisément les équipements liés à plusieurs domaines de la voirie :
|
||||
|
||||
* **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.
|
||||
* **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.
|
||||
* **Espaces verts / Nature** : Zones naturelles, arbres isolés, surfaces végétalisées.
|
||||
* **Systèmes de transport intelligents (ITS)** : Capteurs de trafic, caméras, panneaux à messages variables.
|
||||
* **Chaussées, trottoirs et mobilier urbain** : Description géométrique des rues, des trottoirs et du mobilier.
|
||||
* **Patrimoine artistique** : Statues, monuments historiques, fontaines publiques.
|
||||
* **Centres de contrôle** : Supervision et gestion des équipements centralisés.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Modules Fonctionnels Majeurs
|
||||
|
||||
L'application est structurée en plusieurs modules Django spécialisés :
|
||||
|
||||
1. **Gestion des Actifs (`assets`)** : Modélisation physique, classification et géolocalisation de tous les équipements sur l'espace public.
|
||||
2. **Maintenance & Interventions (`interventions`)** : Suivi du cycle de vie des interventions (correctives, préventives et amélioratives). Planification des tournées, relevés de mesures sur le terrain et fiches réflexes pour les techniciens.
|
||||
3. **Contrats & Commandes (`contracts`)** : Gestion des entreprises partenaires, des contrats de maintenance, des bordereaux de prix (postes), des devis et des commandes de travaux.
|
||||
4. **Projets (`projects`)** : Regroupement et planification d'interventions sous forme de projets globaux structurés en phases.
|
||||
5. **Observations Terrain (`observations`)** : Permet aux agents ou utilisateurs de remonter des anomalies ou des signalements directement depuis le terrain avec des photos et des commentaires géolocalisés.
|
||||
6. **Contrôles de Chantier (`controls`)** : Formulaires de contrôle qualité, rapports de visites de chantier et listes de vérification (checklists).
|
||||
7. **Gestion Documentaire (`documents`)** : Système de partage de documents et dossiers avec historique des versions, workflows de validation et gestion des droits d'accès.
|
||||
8. **Gestion des Stocks (`stock`)** : Gestion des entrepôts, des approvisionnements, des mouvements de pièces détachées et des alertes de seuil critique.
|
||||
9. **API REST (`api`) & Mobile (`mobile`)** : Points d'accès pour l'intégration de systèmes tiers et interface optimisée pour les terminaux mobiles utilisés par les agents sur le terrain.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Architecture Technique
|
||||
|
||||
* **Backend** : Python 3, Django, Django REST Framework
|
||||
* **Base de données** : PostgreSQL + PostGIS (données géographiques et spatiales)
|
||||
* **Conteneurisation** : Environnement complet basé sur Docker et Docker Compose
|
||||
|
||||
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Documentation Loko</title>
|
||||
<title>Documentation StreetUp</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
|
|
@ -112,13 +112,13 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Documentation de l'application Loko</h1></p><p><h2>Table des matières</h2></p><p>1. <a href="#vue-densemble">Vue d'ensemble</a>
|
||||
<h1>Documentation de l'application StreetUp</h1></p><p><h2>Table des matières</h2></p><p>1. <a href="#vue-densemble">Vue d'ensemble</a>
|
||||
2. <a href="#architecture-de-lapplication">Architecture de l'application</a>
|
||||
3. <a href="#modules-django">Modules Django</a>
|
||||
4. <a href="#schéma-des-modèles-et-relations">Schéma des modèles et relations</a>
|
||||
5. <a href="#détail-des-modèles-par-module">Détail des modèles par module</a>
|
||||
6. <a href="#flux-de-données">Flux de données</a>
|
||||
7. <a href="#diagrammes">Diagrammes</a></p><p>---</p><p><h2>Vue d'ensemble</h2></p><p><strong>Loko</strong> est un système complet de gestion des actifs de voirie (Street Asset Management System). L'application permet de gérer, suivre et maintenir différents types d'infrastructures de rue telles que :</p><p><ul>
|
||||
7. <a href="#diagrammes">Diagrammes</a></p><p>---</p><p><h2>Vue d'ensemble</h2></p><p><strong>StreetUp</strong> est un système complet de gestion des actifs de voirie (Street Asset Management System). L'application permet de gérer, suivre et maintenir différents types d'infrastructures de rue telles que :</p><p><ul>
|
||||
<li><strong>Feux de signalisation</strong> (Traffic Lights)</li>
|
||||
<li><strong>Éclairage public</strong> (Public Lighting)</li>
|
||||
<li><strong>Panneaux de signalisation</strong> (Road Signs)</li>
|
||||
|
|
@ -128,8 +128,8 @@
|
|||
<li><strong>Patrimoine artistique</strong> (Artworks - monuments, fontaines)</li>
|
||||
<li><strong>ITS - Systèmes de transport intelligents</strong> (Intelligent Transport Systems)</li>
|
||||
<li><strong>Chaussées, trottoirs et mobilier urbain</strong> (Roads)</li>
|
||||
</ul></p><p>L'application est construite avec <strong>Django</strong> et utilise <strong>PostGIS</strong> pour la gestion des données géospatiales.</p><p>---</p><p><h2>Architecture de l'application</h2></p><p><pre><code class="">loko/
|
||||
├── loko/ # Configuration principale Django
|
||||
</ul></p><p>L'application est construite avec <strong>Django</strong> et utilise <strong>PostGIS</strong> pour la gestion des données géospatiales.</p><p>---</p><p><h2>Architecture de l'application</h2></p><p><pre><code class="">streetup/
|
||||
├── streetup/ # Configuration principale Django
|
||||
│ ├── settings.py # Paramètres de l'application
|
||||
│ ├── urls.py # Routes principales
|
||||
│ └── wsgi.py # Point d'entrée WSGI
|
||||
|
|
@ -697,7 +697,7 @@
|
|||
│ContractPost │◄──────┘
|
||||
└─────────────┘
|
||||
</code></pre></p><p>---</p><p><h2>Diagrammes</h2></p><p><h3>Architecture globale</h3></p><p><pre><code class="">┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ LOKO │
|
||||
│ STREETUP │
|
||||
│ Système de gestion des actifs de voirie │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
|
|
@ -847,7 +847,7 @@
|
|||
</ul></p><p><h3>API</h3></p><p><ul>
|
||||
<li>Module <code>api/</code> pour les endpoints REST</li>
|
||||
<li>Support de l'interface mobile</li>
|
||||
</ul></p><p>---</p><p><h2>Conclusion</h2></p><p>Loko est une application Django complète et modulaire qui permet de gérer l'ensemble du cycle de vie des actifs de voirie. La structure en modules permet une maintenance facile et une évolution progressive des fonctionnalités.</p><p>Les principaux points forts de l'architecture sont :</p><p>1. <strong>Modularité</strong> : Chaque domaine métier est isolé dans son propre module Django
|
||||
</ul></p><p>---</p><p><h2>Conclusion</h2></p><p>StreetUp est une application Django complète et modulaire qui permet de gérer l'ensemble du cycle de vie des actifs de voirie. La structure en modules permet une maintenance facile et une évolution progressive des fonctionnalités.</p><p>Les principaux points forts de l'architecture sont :</p><p>1. <strong>Modularité</strong> : Chaque domaine métier est isolé dans son propre module Django
|
||||
2. <strong>Extensibilité</strong> : Les classes abstraites permettent d'ajouter facilement de nouveaux types d'actifs
|
||||
3. <strong>Traçabilité</strong> : Historique complet des interventions et des modifications
|
||||
4. <strong>Géolocalisation</strong> : Support natif des données géospatiales avec PostGIS
|
||||
|
|
|
|||
135
docker-compose.panoramax.yml
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
services:
|
||||
panoramax_db:
|
||||
image: postgis/postgis:16-3.4
|
||||
container_name: panoramax_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: panoramax
|
||||
POSTGRES_USER: panoramax_user
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- ${SRV_DIR:-/srv/panoramax}/db_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- panoramax_network
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U panoramax_user -d panoramax" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
panoramax_api:
|
||||
image: panoramax/api:latest
|
||||
container_name: panoramax_api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
panoramax_db:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DB_HOST=panoramax_db
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=panoramax
|
||||
- DB_USERNAME=panoramax_user
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
# Désactive le traitement interne pour déléguer au picture-worker
|
||||
- PICTURE_PROCESS_THREADS_LIMIT=0
|
||||
# Permet à Gunicorn de faire confiance aux en-têtes de proxy (comme X-Forwarded-Proto) envoyés par Caddy
|
||||
- GUNICORN_CMD_ARGS=--forwarded-allow-ips=*
|
||||
# Visibilité par défaut publique pour les métadonnées (images protégées au niveau du proxy)
|
||||
- API_DEFAULT_PICTURE_VISIBILITY=anyone
|
||||
# ports:
|
||||
# Optionnel : Exposer localement pour le débug si besoin (décommenter si nécessaire)
|
||||
# - "127.0.0.1:5001:5000"
|
||||
# - "5000:5000"
|
||||
networks:
|
||||
- panoramax_network
|
||||
- streetup_network
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0'
|
||||
memory: 2G
|
||||
|
||||
panoramax_worker:
|
||||
image: panoramax/api:latest
|
||||
restart: unless-stopped
|
||||
command: picture-worker
|
||||
depends_on:
|
||||
panoramax_db:
|
||||
condition: service_healthy
|
||||
panoramax_api:
|
||||
condition: service_started
|
||||
panoramax_blur:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DB_HOST=panoramax_db
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=panoramax
|
||||
- DB_USERNAME=panoramax_user
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
# Visibilité par défaut publique pour les métadonnées (images protégées au niveau du proxy)
|
||||
- API_DEFAULT_PICTURE_VISIBILITY=anyone
|
||||
networks:
|
||||
- panoramax_network
|
||||
deploy:
|
||||
replicas: 4
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0'
|
||||
memory: 4G
|
||||
|
||||
panoramax_blur:
|
||||
image: panoramax/sgblur:latest
|
||||
container_name: panoramax_blur
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- WEB_CONCURRENCY=4 # Limite le nombre de workers concurrents pour préserver le CPU
|
||||
- DETECT_URL=http://panoramax_detect:8001
|
||||
depends_on:
|
||||
panoramax_detect:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- panoramax_network
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0'
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: python -c "import requests; requests.get('http://localhost:8000/').raise_for_status()"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 2s
|
||||
|
||||
panoramax_detect:
|
||||
image: panoramax/sgblur:latest
|
||||
container_name: panoramax_detect
|
||||
entrypoint: ["uvicorn", "src.detect.detect_api:app", "--host", "0.0.0.0", "--port", "8001", "--workers", "4"]
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- OMP_NUM_THREADS=1 # 1 seul thread PyTorch par processus pour éviter la surcharge de synchronisation
|
||||
- MKL_NUM_THREADS=1 # 1 seul thread MKL par processus
|
||||
- MKL_DYNAMIC=FALSE # Désactive la mise à l'échelle dynamique des threads de MKL
|
||||
networks:
|
||||
- panoramax_network
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '4.0'
|
||||
memory: 6G
|
||||
healthcheck:
|
||||
test: python -c "import requests; requests.get('http://localhost:8001/').raise_for_status()"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 2s
|
||||
|
||||
networks:
|
||||
panoramax_network:
|
||||
driver: bridge
|
||||
streetup_network:
|
||||
external: true
|
||||
102
docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
services:
|
||||
db:
|
||||
image: postgis/postgis:18-3.6
|
||||
container_name: ${CONTAINER_PREFIX}_db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:${DB_HOST_PORT:-5438}:5432" # Conserve l'accès local sécurisé pour l'hôte (port personnalisable)
|
||||
environment:
|
||||
POSTGRES_DB: streetup
|
||||
POSTGRES_USER: streetup_admin
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- ${SRV_DIR:-/srv/streetup}/db_data:/var/lib/postgresql
|
||||
networks:
|
||||
- streetup_network
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U streetup_admin -d streetup" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
web:
|
||||
# Utilise l'image avec le tag approprié (latest en prod, test en environnement de test)
|
||||
image: ghcr.io/kdeterme/streetup:${IMAGE_TAG:-latest}
|
||||
container_name: ${CONTAINER_PREFIX}_web
|
||||
restart: unless-stopped
|
||||
command: /docker-entrypoint.sh
|
||||
networks:
|
||||
- streetup_network
|
||||
volumes:
|
||||
- ${SRV_DIR:-/srv/streetup}/media:/app/media
|
||||
- ${SRV_DIR:-/srv/streetup}/private_media:/app/private_media
|
||||
- ${SRV_DIR:-/srv/streetup}/static:/app/static
|
||||
- ${SRV_DIR:-/srv/streetup}/backups:/app/backups
|
||||
- ${BACKUP_HOST_SSH_KEY:-${HOME}/.ssh/id_ed25519}:/app/backup_key:ro
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so
|
||||
- GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so
|
||||
- PROJ_DATA=/opt/conda/envs/streetup-env/share/proj
|
||||
- LOG_FILE_PATH=/app/django.log
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
rclone-backup:
|
||||
image: rclone/rclone:latest
|
||||
container_name: ${CONTAINER_PREFIX}_backup_rclone
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ${SRV_DIR:-/srv/streetup}/media:/data/media:ro
|
||||
- ${SRV_DIR:-/srv/streetup}/private_media:/data/private_media:ro
|
||||
- ./rclone:/config/rclone
|
||||
environment:
|
||||
- BACKUP_INTERVAL=86400
|
||||
- DESTINATION=gdrive:streetup_backup
|
||||
entrypoint: >
|
||||
sh -c '
|
||||
echo "=== Démarrage du daemon de sauvegarde Rclone ===";
|
||||
while true; do
|
||||
echo "=== Début de la synchronisation : $$(date) ===";
|
||||
rclone sync /data/media $${DESTINATION}/media --verbose;
|
||||
rclone sync /data/private_media $${DESTINATION}/private_media --verbose;
|
||||
echo "=== Fin de la synchronisation : $$(date) ===";
|
||||
echo "Sommeil de $${BACKUP_INTERVAL} secondes...";
|
||||
sleep $${BACKUP_INTERVAL};
|
||||
done
|
||||
'
|
||||
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: ${CONTAINER_PREFIX}_caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- ${CONTAINER_PREFIX}_caddy_data:/data
|
||||
- ${CONTAINER_PREFIX}_caddy_config:/config
|
||||
- ${SRV_DIR:-/srv/streetup}/static:/srv/static:ro
|
||||
- ${SRV_DIR:-/srv/streetup}/media:/srv/media:ro
|
||||
- /srv/streetup-test/static:/srv/static_test:ro
|
||||
- /srv/streetup-test/media:/srv/media_test:ro
|
||||
depends_on:
|
||||
- web
|
||||
networks:
|
||||
- streetup_network
|
||||
|
||||
volumes:
|
||||
streetup_prod_caddy_data:
|
||||
streetup_prod_caddy_config:
|
||||
streetup_test_caddy_data:
|
||||
streetup_test_caddy_config:
|
||||
|
||||
networks:
|
||||
streetup_network:
|
||||
external: true
|
||||
|
|
@ -1,21 +1,19 @@
|
|||
services:
|
||||
db:
|
||||
image: postgis/postgis:18-3.6
|
||||
container_name: loko_db
|
||||
container_name: streetup_db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5436:5432" # Mappé sur le port 5436 de l'hôte
|
||||
env_file:
|
||||
- ./loko/.env
|
||||
- "5434:5432" # Mappé sur le port 5434 de l'hôte pour éviter tout conflit avec PostgreSQL local
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-loko}
|
||||
POSTGRES_USER: ${DB_USER:-admin}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?La variable DB_PASSWORD est obligatoire dans le fichier .env}
|
||||
POSTGRES_DB: streetup
|
||||
POSTGRES_USER: admin
|
||||
POSTGRES_PASSWORD: replace_by_your_pwd
|
||||
volumes:
|
||||
- loko_db_data:/var/lib/postgresql
|
||||
- ./loko/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||
- streetup_db_data:/var/lib/postgresql
|
||||
- ./streetup/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-admin} -d ${DB_NAME:-loko}" ]
|
||||
test: [ "CMD-SHELL", "pg_isready -U admin -d streetup" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
|
@ -27,25 +25,25 @@ services:
|
|||
args:
|
||||
# Mettre "true" pour installer ultralytics, OpenCV et PyTorch dans l'image
|
||||
INSTALL_AI_LIBS: "false"
|
||||
container_name: loko_web
|
||||
container_name: streetup_web
|
||||
restart: unless-stopped
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
ports:
|
||||
- "8003:8000" # Mappé sur le port 8003 de l'hôte
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./loko:/app # Volume mount pour le hot-reload local
|
||||
- loko_media:/app/media
|
||||
- loko_private_media:/app/private_media
|
||||
- ./streetup:/app # Volume mount pour le hot-reload local
|
||||
- streetup_media:/app/media
|
||||
- streetup_private_media:/app/private_media
|
||||
env_file:
|
||||
- ./loko/.env # Chargement du fichier d'environnement local
|
||||
- ./streetup/.env # Chargement du fichier d'environnement local
|
||||
environment:
|
||||
# Surcharges d'environnement internes au conteneur pour cibler la base du conteneur "db"
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
# Surcharges des chemins GIS internes au conteneur pour écraser ceux de l'hôte issus du .env
|
||||
- GDAL_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgdal.so
|
||||
- GEOS_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgeos_c.so
|
||||
- PROJ_DATA=/opt/conda/envs/loko-env/share/proj
|
||||
- GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so
|
||||
- GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so
|
||||
- PROJ_DATA=/opt/conda/envs/streetup-env/share/proj
|
||||
# Correction du chemin des logs pour rester dans le conteneur
|
||||
- LOG_FILE_PATH=/app/django.log
|
||||
depends_on:
|
||||
|
|
@ -54,20 +52,22 @@ services:
|
|||
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: loko_caddy
|
||||
container_name: streetup_caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8083:80" # Mappé sur le port 8083 de l'hôte
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- loko_caddy_data:/data
|
||||
- loko_caddy_config:/config
|
||||
- streetup_caddy_data:/data
|
||||
- streetup_caddy_config:/config
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
volumes:
|
||||
loko_db_data:
|
||||
loko_media:
|
||||
loko_private_media:
|
||||
loko_caddy_data:
|
||||
loko_caddy_config:
|
||||
streetup_db_data:
|
||||
streetup_media:
|
||||
streetup_private_media:
|
||||
streetup_caddy_data:
|
||||
streetup_caddy_config:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# "command: python manage.py runserver ..." dans docker-compose.yml.
|
||||
set -e
|
||||
|
||||
exec gunicorn loko.wsgi:application \
|
||||
exec gunicorn streetup.wsgi:application \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--workers "${GUNICORN_WORKERS:-4}" \
|
||||
--threads "${GUNICORN_THREADS:-2}" \
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
import subprocess
|
||||
from django.http import JsonResponse
|
||||
from django.utils import timezone
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
|
||||
def get_version_info():
|
||||
version = os.environ.get('LOKO_VERSION', '1.0.0')
|
||||
commit_sha = os.environ.get('GIT_COMMIT_SHA') or os.environ.get('GIT_COMMIT')
|
||||
branch = os.environ.get('GIT_BRANCH', 'main')
|
||||
build_date = os.environ.get('BUILD_DATE')
|
||||
|
||||
if not commit_sha:
|
||||
for possible_path in ['/app/build_info.json', 'build_info.json']:
|
||||
if os.path.exists(possible_path):
|
||||
try:
|
||||
with open(possible_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
commit_sha = data.get('git_commit') or data.get('commit_sha')
|
||||
version = data.get('version', version)
|
||||
branch = data.get('branch', branch)
|
||||
build_date = data.get('build_date', build_date)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not commit_sha:
|
||||
try:
|
||||
commit_sha = subprocess.check_output(
|
||||
['git', 'rev-parse', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=1
|
||||
).decode('utf-8').strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
commit_short = commit_sha[:7] if commit_sha else None
|
||||
|
||||
return {
|
||||
'status': 'online',
|
||||
'app': 'loko',
|
||||
'version': version,
|
||||
'git_commit': commit_sha,
|
||||
'git_commit_short': commit_short,
|
||||
'branch': branch,
|
||||
'build_date': build_date,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
class VersionInfoView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return JsonResponse(get_version_info())
|
||||
|
|
@ -1,558 +0,0 @@
|
|||
from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon
|
||||
from django import forms
|
||||
|
||||
from assets.models import TrafficLightIntersectionDocument, PublicLightingStreetDocument, StructureDocument, ControlCenterDocument
|
||||
from assets.models import SignStreetDocument, RoadStreetDocument, ITSLocationDocument
|
||||
from assets.models import TrafficLightIntersection
|
||||
from assets.models.its import ITSLocation
|
||||
from assets.models.buildings import PublicBuilding
|
||||
from assets.models.trafficlights import TL_INTERSECTION_STATUS_CHOICES, TL_VOLTAGE_CHOICES
|
||||
|
||||
|
||||
class MultipleFileInput(forms.ClearableFileInput):
|
||||
allow_multiple_selected = True
|
||||
|
||||
|
||||
class MultipleFileField(forms.FileField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("widget", MultipleFileInput())
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self, data, initial=None):
|
||||
single_file_clean = super().clean
|
||||
if isinstance(data, (list, tuple)):
|
||||
result = [single_file_clean(d, initial) for d in data]
|
||||
else:
|
||||
result = [single_file_clean(data, initial)]
|
||||
return result
|
||||
|
||||
|
||||
class IntersectionDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = TrafficLightIntersectionDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IntersectionDocumentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
|
||||
class StreetDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = PublicLightingStreetDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StreetDocumentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
|
||||
class StructureDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = StructureDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StructureDocumentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
|
||||
class ControlCenterDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = ControlCenterDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ControlCenterDocumentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
|
||||
class SignStreetDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = SignStreetDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SignStreetDocumentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
|
||||
class RoadStreetDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = RoadStreetDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RoadStreetDocumentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
|
||||
class ITSLocationDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = ITSLocationDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ITSLocationDocumentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generic Location Forms (for create / edit location functionality)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TrafficLightIntersectionForm(forms.ModelForm):
|
||||
"""Form to create or edit a TrafficLightIntersection (localisation Feux)."""
|
||||
|
||||
# Not a model field — computed from the linked controller.
|
||||
# Presented as a 3-way choice (Yes / No / Unknown).
|
||||
connected_to_control_center = forms.ChoiceField(
|
||||
choices=[('', '—'), ('true', 'Oui'), ('false', 'Non')],
|
||||
required=False,
|
||||
label='Connexion à la centrale',
|
||||
widget=forms.Select(attrs={'class': 'form-select'}),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TrafficLightIntersection
|
||||
fields = [
|
||||
'code', 'status',
|
||||
'name_fr', 'name_nl',
|
||||
'voltage_type', 'is_radar',
|
||||
'municipality', 'master',
|
||||
'current_index', 'begin_time',
|
||||
'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'}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'voltage_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'is_radar': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'municipality': forms.Select(attrs={'class': 'form-select'}),
|
||||
'master': forms.Select(attrs={'class': 'form-select'}),
|
||||
'current_index': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'begin_time': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'),
|
||||
'lat': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
||||
'lon': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
||||
}
|
||||
labels = {
|
||||
'code': 'Code',
|
||||
'name_fr': 'Nom (FR)',
|
||||
'name_nl': 'Naam (NL)',
|
||||
'status': 'Statut',
|
||||
'voltage_type': 'Voltage',
|
||||
'is_radar': 'Est un radar ?',
|
||||
'municipality': 'Commune',
|
||||
'master': 'Carrefour principal',
|
||||
'current_index': 'Index',
|
||||
'begin_time': 'Date de mise en service',
|
||||
'lat': 'Latitude',
|
||||
'lon': 'Longitude',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['municipality'].required = False
|
||||
self.fields['master'].required = False
|
||||
self.fields['current_index'].required = False
|
||||
self.fields['begin_time'].required = False
|
||||
self.fields['lat'].required = False
|
||||
self.fields['lon'].required = False
|
||||
self.fields['name_fr'].required = False
|
||||
self.fields['name_nl'].required = False
|
||||
# Exclude current instance from master choices to prevent self-referencing loops
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields['master'].queryset = TrafficLightIntersection.objects.exclude(pk=self.instance.pk).order_by('code')
|
||||
else:
|
||||
self.fields['master'].queryset = TrafficLightIntersection.objects.all().order_by('code')
|
||||
# Pre-populate connected_to_control_center from the computed property
|
||||
if self.instance and self.instance.pk:
|
||||
val = self.instance.is_connected_to_control_center
|
||||
if val is True:
|
||||
self.initial['connected_to_control_center'] = 'true'
|
||||
elif val is False:
|
||||
self.initial['connected_to_control_center'] = 'false'
|
||||
else:
|
||||
self.initial['connected_to_control_center'] = ''
|
||||
# Enforce display order
|
||||
from collections import OrderedDict
|
||||
desired_order = [
|
||||
'code', 'status',
|
||||
'name_fr', 'name_nl',
|
||||
'voltage_type', 'is_radar', 'connected_to_control_center',
|
||||
'municipality', 'master',
|
||||
'current_index', 'begin_time',
|
||||
'lat', 'lon',
|
||||
]
|
||||
self.fields = OrderedDict(
|
||||
(k, self.fields[k]) for k in desired_order if k in self.fields
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=commit)
|
||||
# Propagate connected_to_control_center to the latest active controller
|
||||
raw = self.cleaned_data.get('connected_to_control_center', '')
|
||||
if raw != '' and instance.pk:
|
||||
controller = instance.controllers.filter(status='active').order_by('-installation_date').first()
|
||||
if controller is not None:
|
||||
controller.connected_to_control_center = (raw == 'true')
|
||||
controller.save(update_fields=['connected_to_control_center'])
|
||||
return instance
|
||||
|
||||
|
||||
class ITSLocationForm(forms.ModelForm):
|
||||
"""Form to create or edit an ITSLocation."""
|
||||
|
||||
# Hidden field: WGS84 GeoJSON polygon drawn by the user on the map.
|
||||
# Accepted values: a GeoJSON Polygon or MultiPolygon string, or empty.
|
||||
geom_geojson = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
class Meta:
|
||||
model = ITSLocation
|
||||
fields = [
|
||||
'code', 'name_fr', 'name_nl',
|
||||
'is_main_location', 'parent_location',
|
||||
]
|
||||
widgets = {
|
||||
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'is_main_location': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'parent_location': forms.Select(attrs={'class': 'form-select'}),
|
||||
}
|
||||
labels = {
|
||||
'code': 'Code',
|
||||
'name_fr': 'Nom (FR)',
|
||||
'name_nl': 'Naam (NL)',
|
||||
'is_main_location': 'Localisation principale',
|
||||
'parent_location': 'Localisation parente',
|
||||
}
|
||||
|
||||
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
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields['parent_location'].queryset = ITSLocation.objects.exclude(pk=self.instance.pk).order_by('code')
|
||||
# Pre-populate geom_geojson with the existing geometry in WGS84
|
||||
if self.instance.geom:
|
||||
geom_wgs84 = self.instance.geom.transform(4326, clone=True)
|
||||
self.initial['geom_geojson'] = geom_wgs84.geojson
|
||||
else:
|
||||
self.fields['parent_location'].queryset = ITSLocation.objects.all().order_by('code')
|
||||
|
||||
|
||||
# Registry: maps location_type slug → form class + thematic code
|
||||
# Used by the generic location_editing views.
|
||||
LOCATION_FORM_REGISTRY = {
|
||||
'trafficlightintersection': {
|
||||
'form_class': TrafficLightIntersectionForm,
|
||||
'thematic_code': 'trafficlights',
|
||||
'label': 'Intersection',
|
||||
},
|
||||
'itslocation': {
|
||||
'form_class': ITSLocationForm,
|
||||
'thematic_code': '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',
|
||||
'room_number', 'ceiling_height_m', 'is_heated', 'fire_compartment', 'floor_type', 'floor_plan',
|
||||
'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'}),
|
||||
'room_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: B-204, RDC-01'}),
|
||||
'ceiling_height_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'floor_plan': forms.FileInput(attrs={'class': 'form-control', 'accept': '.png,.jpg,.jpeg,.svg,.pdf,.webp'}),
|
||||
'is_heated': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'fire_compartment': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'floor_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'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)
|
||||
for field_name in [
|
||||
'name_fr', 'name_nl', 'parent_location', 'location_type', 'address', 'description',
|
||||
'owner', 'manager', 'occupant_entity', 'construction_year', 'floors_count',
|
||||
'basement_count', 'total_surface_m2', 'usable_surface_m2', 'surface_m2', 'level_number',
|
||||
'room_number', 'ceiling_height_m', 'fire_compartment', 'floor_type',
|
||||
'erp_category', 'erp_type', 'max_capacity', 'dpe_rating', 'ges_rating', 'lat', 'lon'
|
||||
]:
|
||||
if field_name in self.fields:
|
||||
self.fields[field_name].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',
|
||||
'dimensions', 'surface_m2', 'material',
|
||||
'power_kw', 'energy_source', 'refrigerant_type', 'refrigerant_charge_kg',
|
||||
'glazing_type', 'fire_rating', 'opening_type',
|
||||
'installation_date', 'serial_number',
|
||||
'maintenance_contract_ref', 'contractor_name',
|
||||
'last_control_date', 'next_control_date', 'regulatory_periodicity_months',
|
||||
'replacement_cost_estimate', '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'}),
|
||||
'dimensions': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: 120x140 cm, 350 m²'}),
|
||||
'surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'material': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: Aluminium, PVC, Zinc, Acier'}),
|
||||
'power_kw': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
|
||||
'energy_source': forms.Select(attrs={'class': 'form-select'}),
|
||||
'refrigerant_type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: R32, R410A'}),
|
||||
'refrigerant_charge_kg': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'glazing_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'fire_rating': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: EI30, EI60, Coupe-feu 1h'}),
|
||||
'opening_type': 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'}),
|
||||
'maintenance_contract_ref': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'contractor_name': 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'}),
|
||||
'regulatory_periodicity_months': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'replacement_cost_estimate': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'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',
|
||||
'dimensions', 'surface_m2', 'material', 'power_kw', 'energy_source',
|
||||
'refrigerant_type', 'refrigerant_charge_kg', 'glazing_type', 'fire_rating', 'opening_type',
|
||||
'installation_date', 'serial_number', 'maintenance_contract_ref', 'contractor_name',
|
||||
'last_control_date', 'next_control_date', 'regulatory_periodicity_months',
|
||||
'replacement_cost_estimate', '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)
|
||||
|
||||
|
||||
class StructureExceptionalTransportForm(forms.ModelForm):
|
||||
"""Formulaire d'édition des paramètres de transport exceptionnel sur un ouvrage."""
|
||||
class Meta:
|
||||
from assets.models.structures import StructureExceptionalTransport
|
||||
model = StructureExceptionalTransport
|
||||
fields = [
|
||||
'passage_status', 'fod_vnr', 'national_id', 'bridge_class', 'is_railway_metro_crossing', 'number_of_spans',
|
||||
'max_authorized_weight_tonnes', 'min_guaranteed_capacity_tonnes', 'max_calculated_weight_tonnes',
|
||||
'max_axle_load_tonnes', 'min_axle_spacing_m',
|
||||
'clearance_height_left_m', 'clearance_height_center_m', 'clearance_height_right_m', 'usable_width_m',
|
||||
'max_speed_kmh', 'is_braking_forbidden', 'is_isolated_crossing_only', 'safety_distance_m',
|
||||
'traffic_interruption_required', 'lane_positioning', 'is_escort_required',
|
||||
'prescriptions_fr', 'prescriptions_nl',
|
||||
'last_study_date', 'last_study_ref',
|
||||
]
|
||||
widgets = {
|
||||
'passage_status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'fod_vnr': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'national_id': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'bridge_class': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'is_railway_metro_crossing': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'number_of_spans': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'max_authorized_weight_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'min_guaranteed_capacity_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'max_calculated_weight_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'max_axle_load_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'min_axle_spacing_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'clearance_height_left_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'clearance_height_center_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'clearance_height_right_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'usable_width_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'max_speed_kmh': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'is_braking_forbidden': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'is_isolated_crossing_only': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'safety_distance_m': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'traffic_interruption_required': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'lane_positioning': forms.Select(attrs={'class': 'form-select'}),
|
||||
'is_escort_required': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'prescriptions_fr': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'prescriptions_nl': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'last_study_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'last_study_ref': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for name, field in self.fields.items():
|
||||
if not isinstance(field.widget, forms.CheckboxInput):
|
||||
field.required = False
|
||||
|
||||
# ─── Management Zones Forms ───────────────────────────────────────────────────
|
||||
|
||||
from assets.models import ManagementZone, ManagementZoneType, ManagementZoneDocument
|
||||
|
||||
|
||||
class ManagementZoneForm(forms.ModelForm):
|
||||
"""Formulaire de création et d'édition d'une Zone de gestion."""
|
||||
geom_geojson = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
class Meta:
|
||||
model = ManagementZone
|
||||
fields = [
|
||||
'code', 'name_fr', 'name_nl', 'thematic', 'zone_type', 'color',
|
||||
'target_frequency_days', 'seasonality_notes', 'instructions', 'description', 'is_active'
|
||||
]
|
||||
widgets = {
|
||||
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'thematic': forms.Select(attrs={'class': 'form-select'}),
|
||||
'zone_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'color': forms.TextInput(attrs={'class': 'form-control form-control-color', 'type': 'color'}),
|
||||
'target_frequency_days': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
|
||||
'seasonality_notes': forms.TextInput(attrs={'class': 'form-control', 'placeholder': "Ex: Tonte d'avril à octobre; fauche tardive en automne"}),
|
||||
'instructions': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': "Consignes d'exploitation, précautions tramway, biodiversité..."}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in ['name_nl', 'zone_type', 'color', 'target_frequency_days', 'seasonality_notes', 'instructions', 'description']:
|
||||
if field in self.fields:
|
||||
self.fields[field].required = False
|
||||
|
||||
if not self.instance.pk and not self.initial.get('color'):
|
||||
self.initial['color'] = '#198754'
|
||||
|
||||
if self.instance and self.instance.pk and self.instance.geom:
|
||||
geom_wgs84 = self.instance.geom.transform(4326, clone=True)
|
||||
self.initial['geom_geojson'] = geom_wgs84.geojson
|
||||
|
||||
if user:
|
||||
from assets.permissions import get_editable_thematics_for_management_zones
|
||||
editable_thematics = get_editable_thematics_for_management_zones(user)
|
||||
self.fields['thematic'].queryset = editable_thematics
|
||||
if not self.instance.pk and editable_thematics.count() == 1:
|
||||
self.fields['thematic'].initial = editable_thematics.first()
|
||||
|
||||
thematic_id = None
|
||||
if self.data.get('thematic'):
|
||||
thematic_id = self.data.get('thematic')
|
||||
elif self.instance.pk and self.instance.thematic_id:
|
||||
thematic_id = self.instance.thematic_id
|
||||
elif self.initial.get('thematic'):
|
||||
thematic_id = self.initial.get('thematic')
|
||||
if hasattr(thematic_id, 'pk'):
|
||||
thematic_id = thematic_id.pk
|
||||
|
||||
if thematic_id:
|
||||
self.fields['zone_type'].queryset = ManagementZoneType.objects.filter(thematic_id=thematic_id)
|
||||
else:
|
||||
self.fields['zone_type'].queryset = ManagementZoneType.objects.all()
|
||||
|
||||
def save(self, commit=True):
|
||||
zone = super().save(commit=False)
|
||||
geojson_str = (self.cleaned_data.get('geom_geojson') or '').strip()
|
||||
if geojson_str:
|
||||
try:
|
||||
geom = GEOSGeometry(geojson_str, srid=4326)
|
||||
if isinstance(geom, Polygon):
|
||||
geom = MultiPolygon(geom, srid=4326)
|
||||
geom.transform(3812)
|
||||
zone.geom = geom
|
||||
except Exception:
|
||||
pass
|
||||
elif 'geom_geojson' in self.cleaned_data and not geojson_str:
|
||||
zone.geom = None
|
||||
|
||||
if commit:
|
||||
zone.save()
|
||||
return zone
|
||||
|
||||
|
||||
class ManagementZoneDocumentForm(forms.ModelForm):
|
||||
documents = MultipleFileField(required=True, label="Documents")
|
||||
|
||||
class Meta:
|
||||
model = ManagementZoneDocument
|
||||
fields = ['documents']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
|
|
@ -1,746 +0,0 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-17 11:57+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:51
|
||||
#: loko/assets/static/assets/asset_add_modal.js:139
|
||||
#: loko/assets/static/assets/asset_add_modal.js:190
|
||||
#: loko/assets/static/assets/asset_add_modal.js:615
|
||||
#: loko/assets/static/assets/unified_index.js:118
|
||||
#: loko/assets/static/assets/unified_index.js:224
|
||||
#: loko/assets/static/assets/unified_index.js:266
|
||||
msgid "Sélectionner d'abord une catégorie..."
|
||||
msgstr "Select a category first..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:59
|
||||
#: loko/assets/static/assets/asset_add_modal.js:86
|
||||
#: loko/assets/static/assets/asset_add_modal.js:101
|
||||
#: loko/assets/static/assets/asset_add_modal.js:608
|
||||
msgid "Sélectionner un type..."
|
||||
msgstr "Select a type..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:186
|
||||
#: loko/assets/static/assets/asset_location_edit.js:59
|
||||
#: loko/assets/static/assets/unified_index.js:119
|
||||
#: loko/assets/static/assets/unified_index.js:172
|
||||
#: loko/assets/static/assets/unified_index.js:228
|
||||
msgid "Chargement..."
|
||||
msgstr "Loading..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:199
|
||||
#: loko/assets/static/assets/unified_index.js:234
|
||||
msgid "Sélectionner un modèle..."
|
||||
msgstr "Select a model..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:223
|
||||
msgid "Aucun modèle disponible pour ce type"
|
||||
msgstr "No model available for this type"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:226
|
||||
#: loko/assets/static/assets/unified_index.js:247
|
||||
msgid "Aucun modèle disponible"
|
||||
msgstr "No model available"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:231
|
||||
#: loko/assets/static/assets/asset_location_edit.js:102
|
||||
#: loko/assets/static/assets/unified_index.js:138
|
||||
#: loko/assets/static/assets/unified_index.js:209
|
||||
#: loko/assets/static/assets/unified_index.js:251
|
||||
msgid "Erreur de chargement"
|
||||
msgstr "Loading error"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:239
|
||||
#: loko/assets/static/assets/unified_index.js:924
|
||||
#: loko/assets/static/assets/unified_index.js:1011
|
||||
msgid "Création..."
|
||||
msgstr "Creating..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:319
|
||||
#: loko/assets/static/assets/asset_add_modal.js:523
|
||||
msgid "Erreur lors de la création"
|
||||
msgstr "Error during creation"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:329
|
||||
#: loko/assets/static/assets/asset_add_modal.js:532
|
||||
msgid "Erreur de connexion"
|
||||
msgstr "Connection error"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:394
|
||||
msgid "Sélection de template d'opérations"
|
||||
msgstr "Operation template selection"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:396
|
||||
msgid "Fermer"
|
||||
msgstr "Close"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:399
|
||||
msgid ""
|
||||
"Choisissez le(s) template(s) d'opérations à appliquer à cet équipement."
|
||||
msgstr "Choose the operation template(s) to apply to this asset."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:401
|
||||
msgid "Choisir le(s) template(s) d'opérations :"
|
||||
msgstr "Choose operation template(s):"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:415
|
||||
msgid "Tout sélectionner"
|
||||
msgstr "Select all"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:418
|
||||
msgid "Tout désélectionner"
|
||||
msgstr "Deselect all"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:426
|
||||
msgid "Ignorer et continuer"
|
||||
msgstr "Ignore and continue"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:429
|
||||
msgid "Créer les opérations"
|
||||
msgstr "Create operations"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:490
|
||||
msgid "Création en cours..."
|
||||
msgstr "Creating..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:37
|
||||
msgid "Saisissez au moins 2 caractères pour rechercher."
|
||||
msgstr "Enter at least 2 characters to search."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:76
|
||||
#: loko/assets/static/assets/unified_index.js:181
|
||||
msgid "Sélectionner une localisation..."
|
||||
msgstr "Select location..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:87
|
||||
msgid "Résultats limités. Affinez la recherche pour trouver plus facilement."
|
||||
msgstr "Limited results. Refine your search to find more easily."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:92
|
||||
#: loko/assets/static/assets/unified_index.js:200
|
||||
msgid "Aucune localisation disponible"
|
||||
msgstr "No location available"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:94
|
||||
msgid "Aucun résultat pour cette recherche."
|
||||
msgstr "No results for this search."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:104
|
||||
msgid "Erreur lors du chargement des localisations"
|
||||
msgstr "Error during loading locations"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:117
|
||||
msgid "Veuillez sélectionner une localisation"
|
||||
msgstr "Please select a location"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:124
|
||||
msgid "Mise à jour..."
|
||||
msgstr "Updating..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:149
|
||||
msgid "Localisation mise à jour"
|
||||
msgstr "Location updated"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:154
|
||||
#: loko/assets/static/assets/asset_location_edit.js:161
|
||||
msgid "Erreur lors de la mise à jour"
|
||||
msgstr "Error during updating"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:568
|
||||
#: loko/assets/static/assets/assets_index.js:592
|
||||
#: loko/assets/static/assets/unified_index.js:506
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:594
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:956
|
||||
#: loko/assets/static/assets/unified_index.js:518
|
||||
msgid "Statut"
|
||||
msgstr "Status"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:777
|
||||
msgid "%(count)s localisation sélectionnée"
|
||||
msgid_plural "%(count)s localisations sélectionnées"
|
||||
msgstr[0] "%(count)s location selected"
|
||||
msgstr[1] "%(count)s locations selected"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:789
|
||||
msgid "%(count)s asset sélectionné"
|
||||
msgid_plural "%(count)s assets sélectionnés"
|
||||
msgstr[0] "%(count)s asset selected"
|
||||
msgstr[1] "%(count)s assets selected"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:880
|
||||
msgid "Sélectionner l'asset"
|
||||
msgstr "Select asset"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:886
|
||||
msgid "Désélectionner l'asset"
|
||||
msgstr "Deselect asset"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:997
|
||||
#: loko/assets/static/assets/assets_index.js:1068
|
||||
msgid "Erreur lors de la récupération des données."
|
||||
msgstr "Error while retrieving data."
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1100
|
||||
msgid ""
|
||||
"Sélectionnez des localisations ou lancez une recherche pour afficher les "
|
||||
"assets."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1102
|
||||
msgid "Aucun asset ne correspond aux critères de recherche."
|
||||
msgstr "No asset matches the search criteria."
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1309
|
||||
#: loko/assets/static/assets/unified_index.js:2980
|
||||
#: loko/assets/static/assets/unified_index.js:3743
|
||||
#: loko/assets/static/assets/unified_index.js:3811
|
||||
msgid "Sans catégorie"
|
||||
msgstr "Uncategorized"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1330
|
||||
#: loko/assets/static/assets/unified_index.js:3837
|
||||
msgid "%(count)s élément pris en compte"
|
||||
msgid_plural "%(count)s éléments pris en compte"
|
||||
msgstr[0] "%(count)s item included"
|
||||
msgstr[1] "%(count)s items included"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1338
|
||||
#: loko/assets/static/assets/unified_index.js:3842
|
||||
msgid "(sélection)"
|
||||
msgstr "(selection)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1339
|
||||
#: loko/assets/static/assets/unified_index.js:3842
|
||||
msgid "(table)"
|
||||
msgstr "(table)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1350
|
||||
#: loko/assets/static/assets/unified_index.js:3847
|
||||
msgid "Aucune donnée disponible"
|
||||
msgstr "No data available"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1383
|
||||
#: loko/assets/static/assets/unified_index.js:3878
|
||||
msgid "au total"
|
||||
msgstr "in total"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1389
|
||||
#: loko/assets/static/assets/unified_index.js:516
|
||||
#: loko/assets/static/assets/unified_index.js:3884
|
||||
msgid "Modèle"
|
||||
msgstr "Model"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1390
|
||||
#: loko/assets/static/assets/unified_index.js:3885
|
||||
msgid "Nombre"
|
||||
msgstr "Number"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1391
|
||||
#: loko/assets/static/assets/unified_index.js:3886
|
||||
msgid "Longueur (m)"
|
||||
msgstr "Length (m)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1392
|
||||
#: loko/assets/static/assets/unified_index.js:3887
|
||||
msgid "Surface (m²)"
|
||||
msgstr "Area (m²)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1396
|
||||
#: loko/assets/static/assets/unified_index.js:3890
|
||||
msgid "Aucun modèle"
|
||||
msgstr "No model"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1400
|
||||
#: loko/assets/static/assets/unified_index.js:3893
|
||||
msgid "Total catégorie"
|
||||
msgstr "Category total"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:476
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:905
|
||||
msgid "Nouveau commentaire"
|
||||
msgstr "New comment"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:483
|
||||
msgid "Textes rapides"
|
||||
msgstr "Quick texts"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:485
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:486
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:959
|
||||
msgid "Mémo"
|
||||
msgstr "Memo"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:488
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:489
|
||||
msgid "Point d'attention"
|
||||
msgstr "Point of attention"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:491
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:492
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:965
|
||||
msgid "À faire"
|
||||
msgstr "To do"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:494
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:495
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:970
|
||||
msgid "OK"
|
||||
msgstr "Okay"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:497
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:498
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:973
|
||||
msgid "Problème"
|
||||
msgstr "Problem"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:504
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:980
|
||||
msgid "Commentaire"
|
||||
msgstr "Comment"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:505
|
||||
msgid "Votre commentaire..."
|
||||
msgstr "Your comment..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:509
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:993
|
||||
msgid "Photos"
|
||||
msgstr "Photos"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:512
|
||||
msgid "Prendre des photos (mode continu)"
|
||||
msgstr "Take photos (continuous mode)"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:515
|
||||
msgid "Sélectionner des fichiers"
|
||||
msgstr "Select files"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:524
|
||||
msgid "Position"
|
||||
msgstr "Position"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:528
|
||||
msgid "Position actuelle sera utilisée"
|
||||
msgstr "Current position will be used"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:531
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:988
|
||||
msgid "Repositionner"
|
||||
msgstr "Reposition"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:539
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1011
|
||||
msgid "Annuler"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:541
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1008
|
||||
msgid "Enregistrer"
|
||||
msgstr "Save"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:573
|
||||
msgid "Caméra"
|
||||
msgstr "Camera"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:588
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1130
|
||||
msgid "Terminer"
|
||||
msgstr "Finish"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:591
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1127
|
||||
msgid "Capturer"
|
||||
msgstr "Capture"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:962
|
||||
msgid "Attention"
|
||||
msgstr "Warning"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:980
|
||||
msgid "(optionnel)"
|
||||
msgstr "(optional)"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:981
|
||||
msgid "Entrez un commentaire optionnel..."
|
||||
msgstr "Enter an optional comment..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:985
|
||||
msgid "Localisation"
|
||||
msgstr "Location"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:986
|
||||
msgid "Calcul en cours..."
|
||||
msgstr "Calculating..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:996
|
||||
msgid "Appareil photo"
|
||||
msgstr "Camera"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:999
|
||||
msgid "Sélectionner fichiers"
|
||||
msgstr "Select files"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:125
|
||||
#: loko/assets/static/assets/unified_index.js:262
|
||||
msgid "Sélectionner une catégorie..."
|
||||
msgstr "Select a category..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:134
|
||||
msgid "Aucune catégorie disponible"
|
||||
msgstr "No category available"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:174
|
||||
msgid "Sélectionnez la localisation liée à ce nouvel asset."
|
||||
msgstr "Select the location linked to this new asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:204
|
||||
msgid "Aucune localisation disponible pour créer cet asset."
|
||||
msgstr "No location available to create this asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:213
|
||||
msgid "Impossible de charger les localisations pour cette thématique."
|
||||
msgstr "Unable to load locations for this topic."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:298
|
||||
msgid "Créer un asset"
|
||||
msgstr "Create an asset"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:312
|
||||
msgid "Le code est généré automatiquement."
|
||||
msgstr "The code is generated automatically."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:449
|
||||
#: loko/assets/static/assets/unified_index.js:454
|
||||
msgid "Erreur lors du chargement du formulaire."
|
||||
msgstr "Error while loading the form."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:471
|
||||
msgid "Créer une localisation"
|
||||
msgstr "Create a location"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:472
|
||||
#: loko/assets/static/assets/unified_index.js:513
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:496
|
||||
msgid "Zoom"
|
||||
msgstr "Zoom"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:512
|
||||
msgid "Nom"
|
||||
msgstr "Name"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:514
|
||||
msgid "Thématique"
|
||||
msgstr "Thematic"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:515
|
||||
msgid "Catégorie"
|
||||
msgstr "Category"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:522
|
||||
msgid "Statut de validation"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:525
|
||||
msgid "À valider"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:526
|
||||
msgid "Validé"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:527
|
||||
msgid "À compléter"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:528
|
||||
msgid "Rejeté"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:841
|
||||
#: loko/assets/static/assets/unified_index.js:842
|
||||
msgid "Sélectionner une thématique..."
|
||||
msgstr "Select a topic..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:841
|
||||
#: loko/assets/static/assets/unified_index.js:842
|
||||
msgid "Aucune thématique disponible"
|
||||
msgstr "No topic available"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:849
|
||||
msgid "Vous n'avez pas de droits de création"
|
||||
msgstr "You do not have creation rights"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:867
|
||||
msgid "Sélectionnez une thématique pour créer un asset."
|
||||
msgstr "Select a topic to create an asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:888
|
||||
msgid "Sélectionnez une thématique pour créer une localisation."
|
||||
msgstr "Select a topic to create a location."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:916
|
||||
msgid "Sélectionnez une localisation avant de créer cet asset."
|
||||
msgstr "Select a location before creating this asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:947
|
||||
msgid "Type d'asset introuvable pour ce modèle."
|
||||
msgstr "Asset type not found for this model."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:977
|
||||
msgid "Asset créé."
|
||||
msgstr "Asset created."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:988
|
||||
#: loko/assets/static/assets/unified_index.js:1035
|
||||
msgid "Erreur lors de la création."
|
||||
msgstr "Error during creation."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:992
|
||||
#: loko/assets/static/assets/unified_index.js:1039
|
||||
msgid "Erreur de connexion."
|
||||
msgstr "Connection error."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1028
|
||||
msgid "Localisation créée."
|
||||
msgstr "Location created."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1499
|
||||
#: loko/assets/static/assets/unified_index.js:1579
|
||||
#: loko/assets/static/assets/unified_index.js:1639
|
||||
msgid "Veuillez sélectionner au moins un asset."
|
||||
msgstr "Please select at least one asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1608
|
||||
#: loko/assets/static/assets/unified_index.js:1615
|
||||
#: loko/assets/static/assets/unified_index.js:1668
|
||||
#: loko/assets/static/assets/unified_index.js:1675
|
||||
msgid "Une erreur est survenue."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1700
|
||||
msgid ""
|
||||
"Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre "
|
||||
"pour charger le tableau et la carte."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1955
|
||||
#: loko/assets/static/assets/unified_index.js:1986
|
||||
msgid "Voir dans le tableau"
|
||||
msgstr "View in table"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1995
|
||||
msgid "autre(s) asset(s)"
|
||||
msgstr "other asset(s)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1999
|
||||
msgid "autre(s) localisation(s)"
|
||||
msgstr "other location(s)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2004
|
||||
msgid "Assets trouvés"
|
||||
msgstr "Assets found"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2012
|
||||
msgid "Localisations trouvées"
|
||||
msgstr "Locations found"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2600
|
||||
msgid "Vue initiale (Bruxelles)"
|
||||
msgstr "Initial view (Brussels)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2758
|
||||
msgid "sélectionné(s)"
|
||||
msgstr "selected"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3302
|
||||
msgid "Aucun type sélectionné"
|
||||
msgstr "No type selected"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3306
|
||||
msgid "Aucun type disponible."
|
||||
msgstr "No type available."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3447
|
||||
#: loko/assets/static/assets/unified_index.js:3617
|
||||
#: loko/assets/static/assets/unified_index.js:3675
|
||||
msgid "Sélectionnez d'abord une ou plusieurs thématiques."
|
||||
msgstr "Select one or more topics first."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3517
|
||||
msgid "Tous"
|
||||
msgstr "All"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3682
|
||||
msgid "Sélectionnez d'abord une ou plusieurs catégories."
|
||||
msgstr "Select one or more categories first."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3698
|
||||
msgid "Chargement des modèles..."
|
||||
msgstr "Loading models..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3709
|
||||
msgid "Aucun modèle disponible pour les catégories sélectionnées."
|
||||
msgstr "No model available for the selected categories."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3720
|
||||
msgid "Afficher les modèles"
|
||||
msgstr "Show models"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3721
|
||||
msgid "Replier les modèles"
|
||||
msgstr "Collapse models"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3726
|
||||
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:93
|
||||
msgid "Rechercher une adresse…"
|
||||
msgstr "Search an address…"
|
||||
|
||||
msgid "Rechercher une adresse..."
|
||||
msgstr "Search an address..."
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:748
|
||||
msgid "Géolocalisation..."
|
||||
msgstr "Geolocation..."
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:769
|
||||
msgid "Position actualisée"
|
||||
msgstr "Position updated"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:773
|
||||
msgid "GPS indisponible"
|
||||
msgstr "GPS unavailable"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:935
|
||||
msgid "Inspections (Désactivé)"
|
||||
msgstr "Inspections (Disabled)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:936
|
||||
msgid "Inspections (1 mois)"
|
||||
msgstr "Inspections (1 month)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:937
|
||||
msgid "Inspections (3 mois)"
|
||||
msgstr "Inspections (3 months)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:938
|
||||
msgid "Inspections (6 mois)"
|
||||
msgstr "Inspections (6 months)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:939
|
||||
msgid "Inspections (1 an)"
|
||||
msgstr "Inspections (1 year)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:940
|
||||
msgid "Inspections (Tout)"
|
||||
msgstr "Inspections (All)"
|
||||
|
||||
msgid "Panneau"
|
||||
msgstr "Sign"
|
||||
|
||||
msgid "Supprimer ce panneau"
|
||||
msgstr "Delete this sign"
|
||||
|
||||
msgid "Mention (ex: Sauf riverains)"
|
||||
msgstr "Mention (e.g. Except local traffic)"
|
||||
|
||||
msgid "Propreté :"
|
||||
msgstr "Cleanliness:"
|
||||
|
||||
msgid "Propre"
|
||||
msgstr "Clean"
|
||||
|
||||
msgid "Sale"
|
||||
msgstr "Dirty"
|
||||
|
||||
msgid "Végétation"
|
||||
msgstr "Vegetation"
|
||||
|
||||
msgid "Dégradé"
|
||||
msgstr "Damaged"
|
||||
|
||||
msgid "Coordonnées GPS manquantes."
|
||||
msgstr "Missing GPS coordinates."
|
||||
|
||||
msgid "Veuillez ajouter au moins un panneau."
|
||||
msgstr "Please add at least one sign."
|
||||
|
||||
msgid "Panneaux créés avec succès !"
|
||||
msgstr "Signs created successfully!"
|
||||
|
||||
msgid "Erreur : "
|
||||
msgstr "Error: "
|
||||
|
||||
msgid "Impossible de créer les panneaux."
|
||||
msgstr "Unable to create signs."
|
||||
|
||||
msgid "Erreur lors de la création : "
|
||||
msgstr "Error during creation: "
|
||||
|
||||
msgid "Créer et Enregistrer"
|
||||
msgstr "Create and Save"
|
||||
|
||||
msgid "Inspection enregistrée !"
|
||||
msgstr "Inspection saved!"
|
||||
|
||||
msgid "Intervention"
|
||||
msgstr "Intervention"
|
||||
|
||||
msgid "créée."
|
||||
msgstr "created."
|
||||
|
||||
msgid "Erreur lors de la validation."
|
||||
msgstr "Validation error."
|
||||
|
||||
msgid "Erreur réseau."
|
||||
msgstr "Network error."
|
||||
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Management zones"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "No management zone defined for this thematic."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Show / hide all zones"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Opacity"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Target frequency:"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "days"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Instructions & points of attention"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Center on zone"
|
||||
|
|
@ -1,751 +0,0 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-17 11:57+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:51
|
||||
#: loko/assets/static/assets/asset_add_modal.js:139
|
||||
#: loko/assets/static/assets/asset_add_modal.js:190
|
||||
#: loko/assets/static/assets/asset_add_modal.js:615
|
||||
#: loko/assets/static/assets/unified_index.js:118
|
||||
#: loko/assets/static/assets/unified_index.js:224
|
||||
#: loko/assets/static/assets/unified_index.js:266
|
||||
msgid "Sélectionner d'abord une catégorie..."
|
||||
msgstr "Sélectionner d'abord une catégorie..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:59
|
||||
#: loko/assets/static/assets/asset_add_modal.js:86
|
||||
#: loko/assets/static/assets/asset_add_modal.js:101
|
||||
#: loko/assets/static/assets/asset_add_modal.js:608
|
||||
msgid "Sélectionner un type..."
|
||||
msgstr "Sélectionner un type..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:186
|
||||
#: loko/assets/static/assets/asset_location_edit.js:59
|
||||
#: loko/assets/static/assets/unified_index.js:119
|
||||
#: loko/assets/static/assets/unified_index.js:172
|
||||
#: loko/assets/static/assets/unified_index.js:228
|
||||
msgid "Chargement..."
|
||||
msgstr "Chargement..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:199
|
||||
#: loko/assets/static/assets/unified_index.js:234
|
||||
msgid "Sélectionner un modèle..."
|
||||
msgstr "Sélectionner un modèle..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:223
|
||||
msgid "Aucun modèle disponible pour ce type"
|
||||
msgstr "Aucun modèle disponible pour ce type"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:226
|
||||
#: loko/assets/static/assets/unified_index.js:247
|
||||
msgid "Aucun modèle disponible"
|
||||
msgstr "Aucun modèle disponible"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:231
|
||||
#: loko/assets/static/assets/asset_location_edit.js:102
|
||||
#: loko/assets/static/assets/unified_index.js:138
|
||||
#: loko/assets/static/assets/unified_index.js:209
|
||||
#: loko/assets/static/assets/unified_index.js:251
|
||||
msgid "Erreur de chargement"
|
||||
msgstr "Erreur de chargement"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:239
|
||||
#: loko/assets/static/assets/unified_index.js:924
|
||||
#: loko/assets/static/assets/unified_index.js:1011
|
||||
msgid "Création..."
|
||||
msgstr "Création..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:319
|
||||
#: loko/assets/static/assets/asset_add_modal.js:523
|
||||
msgid "Erreur lors de la création"
|
||||
msgstr "Erreur lors de la création"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:329
|
||||
#: loko/assets/static/assets/asset_add_modal.js:532
|
||||
msgid "Erreur de connexion"
|
||||
msgstr "Erreur de connexion"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:394
|
||||
msgid "Sélection de template d'opérations"
|
||||
msgstr "Sélection de template d'opérations"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:396
|
||||
msgid "Fermer"
|
||||
msgstr "Fermer"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:399
|
||||
msgid ""
|
||||
"Choisissez le(s) template(s) d'opérations à appliquer à cet équipement."
|
||||
msgstr ""
|
||||
"Choisissez le(s) template(s) d'opérations à appliquer à cet équipement."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:401
|
||||
msgid "Choisir le(s) template(s) d'opérations :"
|
||||
msgstr "Choisir le(s) template(s) d'opérations :"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:415
|
||||
msgid "Tout sélectionner"
|
||||
msgstr "Tout sélectionner"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:418
|
||||
msgid "Tout désélectionner"
|
||||
msgstr "Tout désélectionner"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:426
|
||||
msgid "Ignorer et continuer"
|
||||
msgstr "Ignorer et continuer"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:429
|
||||
msgid "Créer les opérations"
|
||||
msgstr "Créer les opérations"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:490
|
||||
msgid "Création en cours..."
|
||||
msgstr "Création en cours..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:37
|
||||
msgid "Saisissez au moins 2 caractères pour rechercher."
|
||||
msgstr "Saisissez au moins 2 caractères pour rechercher."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:76
|
||||
#: loko/assets/static/assets/unified_index.js:181
|
||||
msgid "Sélectionner une localisation..."
|
||||
msgstr "Sélectionner une localisation..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:87
|
||||
msgid "Résultats limités. Affinez la recherche pour trouver plus facilement."
|
||||
msgstr "Résultats limités. Affinez la recherche pour trouver plus facilement."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:92
|
||||
#: loko/assets/static/assets/unified_index.js:200
|
||||
msgid "Aucune localisation disponible"
|
||||
msgstr "Aucune localisation disponible"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:94
|
||||
msgid "Aucun résultat pour cette recherche."
|
||||
msgstr "Aucun résultat pour cette recherche."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:104
|
||||
msgid "Erreur lors du chargement des localisations"
|
||||
msgstr "Erreur lors du chargement des localisations"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:117
|
||||
msgid "Veuillez sélectionner une localisation"
|
||||
msgstr "Veuillez sélectionner une localisation"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:124
|
||||
msgid "Mise à jour..."
|
||||
msgstr "Mise à jour..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:149
|
||||
msgid "Localisation mise à jour"
|
||||
msgstr "Localisation mise à jour"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:154
|
||||
#: loko/assets/static/assets/asset_location_edit.js:161
|
||||
msgid "Erreur lors de la mise à jour"
|
||||
msgstr "Erreur lors de la mise à jour"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:568
|
||||
#: loko/assets/static/assets/assets_index.js:592
|
||||
#: loko/assets/static/assets/unified_index.js:506
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:594
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:956
|
||||
#: loko/assets/static/assets/unified_index.js:518
|
||||
msgid "Statut"
|
||||
msgstr "Statut"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:777
|
||||
msgid "%(count)s localisation sélectionnée"
|
||||
msgid_plural "%(count)s localisations sélectionnées"
|
||||
msgstr[0] "%(count)s localisation sélectionnée"
|
||||
msgstr[1] "%(count)s localisations sélectionnées"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:789
|
||||
msgid "%(count)s asset sélectionné"
|
||||
msgid_plural "%(count)s assets sélectionnés"
|
||||
msgstr[0] "%(count)s asset sélectionné"
|
||||
msgstr[1] "%(count)s assets sélectionnés"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:880
|
||||
msgid "Sélectionner l'asset"
|
||||
msgstr "Sélectionner l'asset"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:886
|
||||
msgid "Désélectionner l'asset"
|
||||
msgstr "Désélectionner l'asset"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:997
|
||||
#: loko/assets/static/assets/assets_index.js:1068
|
||||
msgid "Erreur lors de la récupération des données."
|
||||
msgstr "Erreur lors de la récupération des données."
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1100
|
||||
msgid ""
|
||||
"Sélectionnez des localisations ou lancez une recherche pour afficher les "
|
||||
"assets."
|
||||
msgstr ""
|
||||
"Sélectionnez des localisations ou lancez une recherche pour afficher les "
|
||||
"assets."
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1102
|
||||
msgid "Aucun asset ne correspond aux critères de recherche."
|
||||
msgstr "Aucun asset ne correspond aux critères de recherche."
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1309
|
||||
#: loko/assets/static/assets/unified_index.js:2980
|
||||
#: loko/assets/static/assets/unified_index.js:3743
|
||||
#: loko/assets/static/assets/unified_index.js:3811
|
||||
msgid "Sans catégorie"
|
||||
msgstr "Sans catégorie"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1330
|
||||
#: loko/assets/static/assets/unified_index.js:3837
|
||||
msgid "%(count)s élément pris en compte"
|
||||
msgid_plural "%(count)s éléments pris en compte"
|
||||
msgstr[0] "%(count)s élément pris en compte"
|
||||
msgstr[1] "%(count)s éléments pris en compte"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1338
|
||||
#: loko/assets/static/assets/unified_index.js:3842
|
||||
msgid "(sélection)"
|
||||
msgstr "(sélection)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1339
|
||||
#: loko/assets/static/assets/unified_index.js:3842
|
||||
msgid "(table)"
|
||||
msgstr "(table)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1350
|
||||
#: loko/assets/static/assets/unified_index.js:3847
|
||||
msgid "Aucune donnée disponible"
|
||||
msgstr "Aucune donnée disponible"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1383
|
||||
#: loko/assets/static/assets/unified_index.js:3878
|
||||
msgid "au total"
|
||||
msgstr "au total"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1389
|
||||
#: loko/assets/static/assets/unified_index.js:516
|
||||
#: loko/assets/static/assets/unified_index.js:3884
|
||||
msgid "Modèle"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1390
|
||||
#: loko/assets/static/assets/unified_index.js:3885
|
||||
msgid "Nombre"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1391
|
||||
#: loko/assets/static/assets/unified_index.js:3886
|
||||
msgid "Longueur (m)"
|
||||
msgstr "Longueur (m)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1392
|
||||
#: loko/assets/static/assets/unified_index.js:3887
|
||||
msgid "Surface (m²)"
|
||||
msgstr "Surface (m²)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1396
|
||||
#: loko/assets/static/assets/unified_index.js:3890
|
||||
msgid "Aucun modèle"
|
||||
msgstr "Aucun modèle"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1400
|
||||
#: loko/assets/static/assets/unified_index.js:3893
|
||||
msgid "Total catégorie"
|
||||
msgstr "Total catégorie"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:476
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:905
|
||||
msgid "Nouveau commentaire"
|
||||
msgstr "Nouveau commentaire"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:483
|
||||
msgid "Textes rapides"
|
||||
msgstr "Textes rapides"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:485
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:486
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:959
|
||||
msgid "Mémo"
|
||||
msgstr "Mémo"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:488
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:489
|
||||
msgid "Point d'attention"
|
||||
msgstr "Point d'attention"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:491
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:492
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:965
|
||||
msgid "À faire"
|
||||
msgstr "À faire"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:494
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:495
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:970
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:497
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:498
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:973
|
||||
msgid "Problème"
|
||||
msgstr "Problème"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:504
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:980
|
||||
msgid "Commentaire"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:505
|
||||
msgid "Votre commentaire..."
|
||||
msgstr "Votre commentaire..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:509
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:993
|
||||
msgid "Photos"
|
||||
msgstr "Photos"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:512
|
||||
msgid "Prendre des photos (mode continu)"
|
||||
msgstr "Prendre des photos (mode continu)"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:515
|
||||
msgid "Sélectionner des fichiers"
|
||||
msgstr "Sélectionner des fichiers"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:524
|
||||
msgid "Position"
|
||||
msgstr "Position"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:528
|
||||
msgid "Position actuelle sera utilisée"
|
||||
msgstr "Position actuelle sera utilisée"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:531
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:988
|
||||
msgid "Repositionner"
|
||||
msgstr "Repositionner"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:539
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1011
|
||||
msgid "Annuler"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:541
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1008
|
||||
msgid "Enregistrer"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:573
|
||||
msgid "Caméra"
|
||||
msgstr "Caméra"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:588
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1130
|
||||
msgid "Terminer"
|
||||
msgstr "Terminer"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:591
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1127
|
||||
msgid "Capturer"
|
||||
msgstr "Capturer"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:962
|
||||
msgid "Attention"
|
||||
msgstr "Attention"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:980
|
||||
msgid "(optionnel)"
|
||||
msgstr "(optionnel)"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:981
|
||||
msgid "Entrez un commentaire optionnel..."
|
||||
msgstr "Entrez un commentaire optionnel..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:985
|
||||
msgid "Localisation"
|
||||
msgstr "Localisation"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:986
|
||||
msgid "Calcul en cours..."
|
||||
msgstr "Calcul en cours..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:996
|
||||
msgid "Appareil photo"
|
||||
msgstr "Appareil photo"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:999
|
||||
msgid "Sélectionner fichiers"
|
||||
msgstr "Sélectionner fichiers"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:125
|
||||
#: loko/assets/static/assets/unified_index.js:262
|
||||
msgid "Sélectionner une catégorie..."
|
||||
msgstr "Sélectionner une catégorie..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:134
|
||||
msgid "Aucune catégorie disponible"
|
||||
msgstr "Aucune catégorie disponible"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:174
|
||||
msgid "Sélectionnez la localisation liée à ce nouvel asset."
|
||||
msgstr "Sélectionnez la localisation liée à ce nouvel asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:204
|
||||
msgid "Aucune localisation disponible pour créer cet asset."
|
||||
msgstr "Aucune localisation disponible pour créer cet asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:213
|
||||
msgid "Impossible de charger les localisations pour cette thématique."
|
||||
msgstr "Impossible de charger les localisations pour cette thématique."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:298
|
||||
msgid "Créer un asset"
|
||||
msgstr "Créer un asset"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:312
|
||||
msgid "Le code est généré automatiquement."
|
||||
msgstr "Le code est généré automatiquement."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:449
|
||||
#: loko/assets/static/assets/unified_index.js:454
|
||||
msgid "Erreur lors du chargement du formulaire."
|
||||
msgstr "Erreur lors du chargement du formulaire."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:471
|
||||
msgid "Créer une localisation"
|
||||
msgstr "Créer une localisation"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:472
|
||||
#: loko/assets/static/assets/unified_index.js:513
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:496
|
||||
msgid "Zoom"
|
||||
msgstr "Zoom"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:512
|
||||
msgid "Nom"
|
||||
msgstr "Nom"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:514
|
||||
msgid "Thématique"
|
||||
msgstr "Thématique"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:515
|
||||
msgid "Catégorie"
|
||||
msgstr "Catégorie"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:522
|
||||
msgid "Statut de validation"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:525
|
||||
msgid "À valider"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:526
|
||||
msgid "Validé"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:527
|
||||
msgid "À compléter"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:528
|
||||
msgid "Rejeté"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:841
|
||||
#: loko/assets/static/assets/unified_index.js:842
|
||||
msgid "Sélectionner une thématique..."
|
||||
msgstr "Sélectionner une thématique..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:841
|
||||
#: loko/assets/static/assets/unified_index.js:842
|
||||
msgid "Aucune thématique disponible"
|
||||
msgstr "Aucune thématique disponible"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:849
|
||||
msgid "Vous n'avez pas de droits de création"
|
||||
msgstr "Vous n'avez pas de droits de création"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:867
|
||||
msgid "Sélectionnez une thématique pour créer un asset."
|
||||
msgstr "Sélectionnez une thématique pour créer un asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:888
|
||||
msgid "Sélectionnez une thématique pour créer une localisation."
|
||||
msgstr "Sélectionnez une thématique pour créer une localisation."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:916
|
||||
msgid "Sélectionnez une localisation avant de créer cet asset."
|
||||
msgstr "Sélectionnez une localisation avant de créer cet asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:947
|
||||
msgid "Type d'asset introuvable pour ce modèle."
|
||||
msgstr "Type d'asset introuvable pour ce modèle."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:977
|
||||
msgid "Asset créé."
|
||||
msgstr "Asset créé."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:988
|
||||
#: loko/assets/static/assets/unified_index.js:1035
|
||||
msgid "Erreur lors de la création."
|
||||
msgstr "Erreur lors de la création."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:992
|
||||
#: loko/assets/static/assets/unified_index.js:1039
|
||||
msgid "Erreur de connexion."
|
||||
msgstr "Erreur de connexion."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1028
|
||||
msgid "Localisation créée."
|
||||
msgstr "Localisation créée."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1499
|
||||
#: loko/assets/static/assets/unified_index.js:1579
|
||||
#: loko/assets/static/assets/unified_index.js:1639
|
||||
msgid "Veuillez sélectionner au moins un asset."
|
||||
msgstr "Veuillez sélectionner au moins un asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1608
|
||||
#: loko/assets/static/assets/unified_index.js:1615
|
||||
#: loko/assets/static/assets/unified_index.js:1668
|
||||
#: loko/assets/static/assets/unified_index.js:1675
|
||||
msgid "Une erreur est survenue."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1700
|
||||
msgid ""
|
||||
"Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre "
|
||||
"pour charger le tableau et la carte."
|
||||
msgstr ""
|
||||
"Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre "
|
||||
"pour charger le tableau et la carte."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1955
|
||||
#: loko/assets/static/assets/unified_index.js:1986
|
||||
msgid "Voir dans le tableau"
|
||||
msgstr "Voir dans le tableau"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1995
|
||||
msgid "autre(s) asset(s)"
|
||||
msgstr "autre(s) asset(s)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1999
|
||||
msgid "autre(s) localisation(s)"
|
||||
msgstr "autre(s) localisation(s)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2004
|
||||
msgid "Assets trouvés"
|
||||
msgstr "Assets trouvés"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2012
|
||||
msgid "Localisations trouvées"
|
||||
msgstr "Localisations trouvées"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2600
|
||||
msgid "Vue initiale (Bruxelles)"
|
||||
msgstr "Vue initiale (Bruxelles)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2758
|
||||
msgid "sélectionné(s)"
|
||||
msgstr "sélectionné(s)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3302
|
||||
msgid "Aucun type sélectionné"
|
||||
msgstr "Aucun type sélectionné"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3306
|
||||
msgid "Aucun type disponible."
|
||||
msgstr "Aucun type disponible."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3447
|
||||
#: loko/assets/static/assets/unified_index.js:3617
|
||||
#: loko/assets/static/assets/unified_index.js:3675
|
||||
msgid "Sélectionnez d'abord une ou plusieurs thématiques."
|
||||
msgstr "Sélectionnez d'abord une ou plusieurs thématiques."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3517
|
||||
msgid "Tous"
|
||||
msgstr "Tous"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3682
|
||||
msgid "Sélectionnez d'abord une ou plusieurs catégories."
|
||||
msgstr "Sélectionnez d'abord une ou plusieurs catégories."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3698
|
||||
msgid "Chargement des modèles..."
|
||||
msgstr "Chargement des modèles..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3709
|
||||
msgid "Aucun modèle disponible pour les catégories sélectionnées."
|
||||
msgstr "Aucun modèle disponible pour les catégories sélectionnées."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3720
|
||||
msgid "Afficher les modèles"
|
||||
msgstr "Afficher les modèles"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3721
|
||||
msgid "Replier les modèles"
|
||||
msgstr "Replier les modèles"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3726
|
||||
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||
msgstr "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:93
|
||||
msgid "Rechercher une adresse…"
|
||||
msgstr "Rechercher une adresse…"
|
||||
|
||||
msgid "Rechercher une adresse..."
|
||||
msgstr "Rechercher une adresse..."
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:748
|
||||
msgid "Géolocalisation..."
|
||||
msgstr "Géolocalisation..."
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:769
|
||||
msgid "Position actualisée"
|
||||
msgstr "Position actualisée"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:773
|
||||
msgid "GPS indisponible"
|
||||
msgstr "GPS indisponible"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:935
|
||||
msgid "Inspections (Désactivé)"
|
||||
msgstr "Inspections (Désactivé)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:936
|
||||
msgid "Inspections (1 mois)"
|
||||
msgstr "Inspections (1 mois)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:937
|
||||
msgid "Inspections (3 mois)"
|
||||
msgstr "Inspections (3 mois)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:938
|
||||
msgid "Inspections (6 mois)"
|
||||
msgstr "Inspections (6 mois)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:939
|
||||
msgid "Inspections (1 an)"
|
||||
msgstr "Inspections (1 an)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:940
|
||||
msgid "Inspections (Tout)"
|
||||
msgstr "Inspections (Tout)"
|
||||
|
||||
msgid "Panneau"
|
||||
msgstr "Panneau"
|
||||
|
||||
msgid "Supprimer ce panneau"
|
||||
msgstr "Supprimer ce panneau"
|
||||
|
||||
msgid "Mention (ex: Sauf riverains)"
|
||||
msgstr "Mention (ex: Sauf riverains)"
|
||||
|
||||
msgid "Propreté :"
|
||||
msgstr "Propreté :"
|
||||
|
||||
msgid "Propre"
|
||||
msgstr "Propre"
|
||||
|
||||
msgid "Sale"
|
||||
msgstr "Sale"
|
||||
|
||||
msgid "Végétation"
|
||||
msgstr "Végétation"
|
||||
|
||||
msgid "Dégradé"
|
||||
msgstr "Dégradé"
|
||||
|
||||
msgid "Coordonnées GPS manquantes."
|
||||
msgstr "Coordonnées GPS manquantes."
|
||||
|
||||
msgid "Veuillez ajouter au moins un panneau."
|
||||
msgstr "Veuillez ajouter au moins un panneau."
|
||||
|
||||
msgid "Panneaux créés avec succès !"
|
||||
msgstr "Panneaux créés avec succès !"
|
||||
|
||||
msgid "Erreur : "
|
||||
msgstr "Erreur : "
|
||||
|
||||
msgid "Impossible de créer les panneaux."
|
||||
msgstr "Impossible de créer les panneaux."
|
||||
|
||||
msgid "Erreur lors de la création : "
|
||||
msgstr "Erreur lors de la création : "
|
||||
|
||||
msgid "Créer et Enregistrer"
|
||||
msgstr "Créer et Enregistrer"
|
||||
|
||||
msgid "Inspection enregistrée !"
|
||||
msgstr "Inspection enregistrée !"
|
||||
|
||||
msgid "Intervention"
|
||||
msgstr "Intervention"
|
||||
|
||||
msgid "créée."
|
||||
msgstr "créée."
|
||||
|
||||
msgid "Erreur lors de la validation."
|
||||
msgstr "Erreur lors de la validation."
|
||||
|
||||
msgid "Erreur réseau."
|
||||
msgstr "Erreur réseau."
|
||||
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Zones de gestion"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "Aucune zone de gestion définie pour cette thématique."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Afficher / masquer toutes les zones"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Opacité"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Fréquence cible :"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "jours"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Consignes & points d'attention"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Centrer sur la zone"
|
||||
|
|
@ -1,748 +0,0 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-17 11:57+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:51
|
||||
#: loko/assets/static/assets/asset_add_modal.js:139
|
||||
#: loko/assets/static/assets/asset_add_modal.js:190
|
||||
#: loko/assets/static/assets/asset_add_modal.js:615
|
||||
#: loko/assets/static/assets/unified_index.js:118
|
||||
#: loko/assets/static/assets/unified_index.js:224
|
||||
#: loko/assets/static/assets/unified_index.js:266
|
||||
msgid "Sélectionner d'abord une catégorie..."
|
||||
msgstr "Selecteer eerst een categorie..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:59
|
||||
#: loko/assets/static/assets/asset_add_modal.js:86
|
||||
#: loko/assets/static/assets/asset_add_modal.js:101
|
||||
#: loko/assets/static/assets/asset_add_modal.js:608
|
||||
msgid "Sélectionner un type..."
|
||||
msgstr "Selecteer een type..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:186
|
||||
#: loko/assets/static/assets/asset_location_edit.js:59
|
||||
#: loko/assets/static/assets/unified_index.js:119
|
||||
#: loko/assets/static/assets/unified_index.js:172
|
||||
#: loko/assets/static/assets/unified_index.js:228
|
||||
msgid "Chargement..."
|
||||
msgstr "Laden..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:199
|
||||
#: loko/assets/static/assets/unified_index.js:234
|
||||
msgid "Sélectionner un modèle..."
|
||||
msgstr "Selecteer een model..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:223
|
||||
msgid "Aucun modèle disponible pour ce type"
|
||||
msgstr "Geen model beschikbaar voor dit type"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:226
|
||||
#: loko/assets/static/assets/unified_index.js:247
|
||||
msgid "Aucun modèle disponible"
|
||||
msgstr "Geen model beschikbaar"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:231
|
||||
#: loko/assets/static/assets/asset_location_edit.js:102
|
||||
#: loko/assets/static/assets/unified_index.js:138
|
||||
#: loko/assets/static/assets/unified_index.js:209
|
||||
#: loko/assets/static/assets/unified_index.js:251
|
||||
msgid "Erreur de chargement"
|
||||
msgstr "Laadfout"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:239
|
||||
#: loko/assets/static/assets/unified_index.js:924
|
||||
#: loko/assets/static/assets/unified_index.js:1011
|
||||
msgid "Création..."
|
||||
msgstr "Bezig met aanmaken..."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:319
|
||||
#: loko/assets/static/assets/asset_add_modal.js:523
|
||||
msgid "Erreur lors de la création"
|
||||
msgstr "Fout bij het aanmaken"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:329
|
||||
#: loko/assets/static/assets/asset_add_modal.js:532
|
||||
msgid "Erreur de connexion"
|
||||
msgstr "Verbindingsfout"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:394
|
||||
msgid "Sélection de template d'opérations"
|
||||
msgstr "Selectie van bewerkingssjabloon"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:396
|
||||
msgid "Fermer"
|
||||
msgstr "Sluiten"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:399
|
||||
msgid ""
|
||||
"Choisissez le(s) template(s) d'opérations à appliquer à cet équipement."
|
||||
msgstr ""
|
||||
"Kies de bedieningssjabloon(en) die u op deze apparatuur wilt toepassen."
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:401
|
||||
msgid "Choisir le(s) template(s) d'opérations :"
|
||||
msgstr "Kies de bewerkingssjabloon(s) :"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:415
|
||||
msgid "Tout sélectionner"
|
||||
msgstr "Alles selecteren"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:418
|
||||
msgid "Tout désélectionner"
|
||||
msgstr "Alles deselecteren"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:426
|
||||
msgid "Ignorer et continuer"
|
||||
msgstr "Negeren en doorgaan"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:429
|
||||
msgid "Créer les opérations"
|
||||
msgstr "Bewerkingen aanmaken"
|
||||
|
||||
#: loko/assets/static/assets/asset_add_modal.js:490
|
||||
msgid "Création en cours..."
|
||||
msgstr "Bezig met aanmaken..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:37
|
||||
msgid "Saisissez au moins 2 caractères pour rechercher."
|
||||
msgstr "Typ ten minste 2 tekens om te zoeken."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:76
|
||||
#: loko/assets/static/assets/unified_index.js:181
|
||||
msgid "Sélectionner une localisation..."
|
||||
msgstr "Selecteer een locatie..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:87
|
||||
msgid "Résultats limités. Affinez la recherche pour trouver plus facilement."
|
||||
msgstr ""
|
||||
"Beperkte resultaten. Verfijn de zoekopdracht om gemakkelijker te vinden."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:92
|
||||
#: loko/assets/static/assets/unified_index.js:200
|
||||
msgid "Aucune localisation disponible"
|
||||
msgstr "Geen locatie beschikbaar"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:94
|
||||
msgid "Aucun résultat pour cette recherche."
|
||||
msgstr "Geen resultaten voor deze zoekopdracht."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:104
|
||||
msgid "Erreur lors du chargement des localisations"
|
||||
msgstr "Fout bij het laden van locaties"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:117
|
||||
msgid "Veuillez sélectionner une localisation"
|
||||
msgstr "Selecteer een locatie"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:124
|
||||
msgid "Mise à jour..."
|
||||
msgstr "Bezig met bijwerken..."
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:149
|
||||
msgid "Localisation mise à jour"
|
||||
msgstr "Locatie bijgewerkt"
|
||||
|
||||
#: loko/assets/static/assets/asset_location_edit.js:154
|
||||
#: loko/assets/static/assets/asset_location_edit.js:161
|
||||
msgid "Erreur lors de la mise à jour"
|
||||
msgstr "Fout bij het bijwerken"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:568
|
||||
#: loko/assets/static/assets/assets_index.js:592
|
||||
#: loko/assets/static/assets/unified_index.js:506
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:594
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:956
|
||||
#: loko/assets/static/assets/unified_index.js:518
|
||||
msgid "Statut"
|
||||
msgstr "Status"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:777
|
||||
msgid "%(count)s localisation sélectionnée"
|
||||
msgid_plural "%(count)s localisations sélectionnées"
|
||||
msgstr[0] "%(count)s locatie geselecteerd"
|
||||
msgstr[1] "%(count)s locaties geselecteerd"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:789
|
||||
msgid "%(count)s asset sélectionné"
|
||||
msgid_plural "%(count)s assets sélectionnés"
|
||||
msgstr[0] "%(count)s asset geselecteerd"
|
||||
msgstr[1] "%(count)s assets geselecteerd"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:880
|
||||
msgid "Sélectionner l'asset"
|
||||
msgstr "Asset selecteren"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:886
|
||||
msgid "Désélectionner l'asset"
|
||||
msgstr "Asset deselecteren"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:997
|
||||
#: loko/assets/static/assets/assets_index.js:1068
|
||||
msgid "Erreur lors de la récupération des données."
|
||||
msgstr "Fout bij het ophalen van de gegevens."
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1100
|
||||
msgid ""
|
||||
"Sélectionnez des localisations ou lancez une recherche pour afficher les "
|
||||
"assets."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1102
|
||||
msgid "Aucun asset ne correspond aux critères de recherche."
|
||||
msgstr "Geen enkele asset voldoet aan de zoekcriteria."
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1309
|
||||
#: loko/assets/static/assets/unified_index.js:2980
|
||||
#: loko/assets/static/assets/unified_index.js:3743
|
||||
#: loko/assets/static/assets/unified_index.js:3811
|
||||
msgid "Sans catégorie"
|
||||
msgstr "Zonder categorie"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1330
|
||||
#: loko/assets/static/assets/unified_index.js:3837
|
||||
msgid "%(count)s élément pris en compte"
|
||||
msgid_plural "%(count)s éléments pris en compte"
|
||||
msgstr[0] "%(count)s item meegenomen"
|
||||
msgstr[1] "%(count)s items meegenomen"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1338
|
||||
#: loko/assets/static/assets/unified_index.js:3842
|
||||
msgid "(sélection)"
|
||||
msgstr "(selectie)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1339
|
||||
#: loko/assets/static/assets/unified_index.js:3842
|
||||
msgid "(table)"
|
||||
msgstr "(tabel)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1350
|
||||
#: loko/assets/static/assets/unified_index.js:3847
|
||||
msgid "Aucune donnée disponible"
|
||||
msgstr "Geen gegevens beschikbaar"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1383
|
||||
#: loko/assets/static/assets/unified_index.js:3878
|
||||
msgid "au total"
|
||||
msgstr "in totaal"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1389
|
||||
#: loko/assets/static/assets/unified_index.js:516
|
||||
#: loko/assets/static/assets/unified_index.js:3884
|
||||
msgid "Modèle"
|
||||
msgstr "Model"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1390
|
||||
#: loko/assets/static/assets/unified_index.js:3885
|
||||
msgid "Nombre"
|
||||
msgstr "Nummer"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1391
|
||||
#: loko/assets/static/assets/unified_index.js:3886
|
||||
msgid "Longueur (m)"
|
||||
msgstr "Lengte (m)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1392
|
||||
#: loko/assets/static/assets/unified_index.js:3887
|
||||
msgid "Surface (m²)"
|
||||
msgstr "Oppervlakte (m²)"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1396
|
||||
#: loko/assets/static/assets/unified_index.js:3890
|
||||
msgid "Aucun modèle"
|
||||
msgstr "Geen model"
|
||||
|
||||
#: loko/assets/static/assets/assets_index.js:1400
|
||||
#: loko/assets/static/assets/unified_index.js:3893
|
||||
msgid "Total catégorie"
|
||||
msgstr "Totaal per categorie"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:476
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:905
|
||||
msgid "Nouveau commentaire"
|
||||
msgstr "Nieuwe opmerking"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:483
|
||||
msgid "Textes rapides"
|
||||
msgstr "Snelle teksten"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:485
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:486
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:959
|
||||
msgid "Mémo"
|
||||
msgstr "Memo"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:488
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:489
|
||||
msgid "Point d'attention"
|
||||
msgstr "Aandachtspunt"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:491
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:492
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:965
|
||||
msgid "À faire"
|
||||
msgstr "Te doen"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:494
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:495
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:970
|
||||
msgid "OK"
|
||||
msgstr "Oké"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:497
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:498
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:973
|
||||
msgid "Problème"
|
||||
msgstr "Probleem"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:504
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:980
|
||||
msgid "Commentaire"
|
||||
msgstr "Opmerking"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:505
|
||||
msgid "Votre commentaire..."
|
||||
msgstr "Uw opmerking..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:509
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:993
|
||||
msgid "Photos"
|
||||
msgstr "Foto's"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:512
|
||||
msgid "Prendre des photos (mode continu)"
|
||||
msgstr "Foto's maken (continue modus)"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:515
|
||||
msgid "Sélectionner des fichiers"
|
||||
msgstr "Bestanden selecteren"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:524
|
||||
msgid "Position"
|
||||
msgstr "Positie"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:528
|
||||
msgid "Position actuelle sera utilisée"
|
||||
msgstr "Huidige positie zal worden gebruikt"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:531
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:988
|
||||
msgid "Repositionner"
|
||||
msgstr "Opnieuw positioneren"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:539
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1011
|
||||
msgid "Annuler"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:541
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1008
|
||||
msgid "Enregistrer"
|
||||
msgstr "Opslaan"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:573
|
||||
msgid "Caméra"
|
||||
msgstr "Camera"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:588
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1130
|
||||
msgid "Terminer"
|
||||
msgstr "Voltooien"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:591
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:1127
|
||||
msgid "Capturer"
|
||||
msgstr "Opnemen"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:962
|
||||
msgid "Attention"
|
||||
msgstr "Let op"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:980
|
||||
msgid "(optionnel)"
|
||||
msgstr "(optioneel)"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:981
|
||||
msgid "Entrez un commentaire optionnel..."
|
||||
msgstr "Voer een optionele opmerking in..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:985
|
||||
msgid "Localisation"
|
||||
msgstr "Locatie"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:986
|
||||
msgid "Calcul en cours..."
|
||||
msgstr "Bezig met berekenen..."
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:996
|
||||
msgid "Appareil photo"
|
||||
msgstr "Camera"
|
||||
|
||||
#: loko/assets/static/assets/green-surfaces-comments.js:999
|
||||
msgid "Sélectionner fichiers"
|
||||
msgstr "Bestanden selecteren"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:125
|
||||
#: loko/assets/static/assets/unified_index.js:262
|
||||
msgid "Sélectionner une catégorie..."
|
||||
msgstr "Selecteer een categorie..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:134
|
||||
msgid "Aucune catégorie disponible"
|
||||
msgstr "Geen categorie beschikbaar"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:174
|
||||
msgid "Sélectionnez la localisation liée à ce nouvel asset."
|
||||
msgstr "Selecteer de locatie gekoppeld aan deze nieuwe asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:204
|
||||
msgid "Aucune localisation disponible pour créer cet asset."
|
||||
msgstr "Geen locatie beschikbaar om deze asset aan te maken."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:213
|
||||
msgid "Impossible de charger les localisations pour cette thématique."
|
||||
msgstr "Kan locaties voor dit thema niet laden."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:298
|
||||
msgid "Créer un asset"
|
||||
msgstr "Een asset aanmaken"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:312
|
||||
msgid "Le code est généré automatiquement."
|
||||
msgstr "De code wordt automatisch gegenereerd."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:449
|
||||
#: loko/assets/static/assets/unified_index.js:454
|
||||
msgid "Erreur lors du chargement du formulaire."
|
||||
msgstr "Fout bij het laden van het formulier."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:471
|
||||
msgid "Créer une localisation"
|
||||
msgstr "Een locatie aanmaken"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:472
|
||||
#: loko/assets/static/assets/unified_index.js:513
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:496
|
||||
msgid "Zoom"
|
||||
msgstr "Zoom"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:512
|
||||
msgid "Nom"
|
||||
msgstr "Naam"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:514
|
||||
msgid "Thématique"
|
||||
msgstr "Thema"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:515
|
||||
msgid "Catégorie"
|
||||
msgstr "Categorie"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:522
|
||||
msgid "Statut de validation"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:525
|
||||
msgid "À valider"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:526
|
||||
msgid "Validé"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:527
|
||||
msgid "À compléter"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:528
|
||||
msgid "Rejeté"
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:841
|
||||
#: loko/assets/static/assets/unified_index.js:842
|
||||
msgid "Sélectionner une thématique..."
|
||||
msgstr "Selecteer een thema..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:841
|
||||
#: loko/assets/static/assets/unified_index.js:842
|
||||
msgid "Aucune thématique disponible"
|
||||
msgstr "Geen thema beschikbaar"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:849
|
||||
msgid "Vous n'avez pas de droits de création"
|
||||
msgstr "U heeft geen rechten om aan te maken"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:867
|
||||
msgid "Sélectionnez une thématique pour créer un asset."
|
||||
msgstr "Selecteer een thema om een asset aan te maken."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:888
|
||||
msgid "Sélectionnez une thématique pour créer une localisation."
|
||||
msgstr "Selecteer een thema om een locatie aan te maken."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:916
|
||||
msgid "Sélectionnez une localisation avant de créer cet asset."
|
||||
msgstr "Selecteer een locatie voordat u deze asset aanmaakt."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:947
|
||||
msgid "Type d'asset introuvable pour ce modèle."
|
||||
msgstr "Assettype niet gevonden voor dit model."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:977
|
||||
msgid "Asset créé."
|
||||
msgstr "Asset aangemaakt."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:988
|
||||
#: loko/assets/static/assets/unified_index.js:1035
|
||||
msgid "Erreur lors de la création."
|
||||
msgstr "Fout bij het aanmaken."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:992
|
||||
#: loko/assets/static/assets/unified_index.js:1039
|
||||
msgid "Erreur de connexion."
|
||||
msgstr "Verbindingsfout."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1028
|
||||
msgid "Localisation créée."
|
||||
msgstr "Locatie aangemaakt."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1499
|
||||
#: loko/assets/static/assets/unified_index.js:1579
|
||||
#: loko/assets/static/assets/unified_index.js:1639
|
||||
msgid "Veuillez sélectionner au moins un asset."
|
||||
msgstr "Selecteer ten minste één asset."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1608
|
||||
#: loko/assets/static/assets/unified_index.js:1615
|
||||
#: loko/assets/static/assets/unified_index.js:1668
|
||||
#: loko/assets/static/assets/unified_index.js:1675
|
||||
msgid "Une erreur est survenue."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1700
|
||||
msgid ""
|
||||
"Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre "
|
||||
"pour charger le tableau et la carte."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1955
|
||||
#: loko/assets/static/assets/unified_index.js:1986
|
||||
msgid "Voir dans le tableau"
|
||||
msgstr "Bekijk in de tabel"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1995
|
||||
msgid "autre(s) asset(s)"
|
||||
msgstr "andere asset(s)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:1999
|
||||
msgid "autre(s) localisation(s)"
|
||||
msgstr "andere locatie(s)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2004
|
||||
msgid "Assets trouvés"
|
||||
msgstr "Gevonden assets"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2012
|
||||
msgid "Localisations trouvées"
|
||||
msgstr "Gevonden locaties"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2600
|
||||
msgid "Vue initiale (Bruxelles)"
|
||||
msgstr "Eerste weergave (Brussel)"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:2758
|
||||
msgid "sélectionné(s)"
|
||||
msgstr "geselecteerd"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3302
|
||||
msgid "Aucun type sélectionné"
|
||||
msgstr "Geen type geselecteerd"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3306
|
||||
msgid "Aucun type disponible."
|
||||
msgstr "Geen type beschikbaar."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3447
|
||||
#: loko/assets/static/assets/unified_index.js:3617
|
||||
#: loko/assets/static/assets/unified_index.js:3675
|
||||
msgid "Sélectionnez d'abord une ou plusieurs thématiques."
|
||||
msgstr "Selecteer eerst een of meer thema's."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3517
|
||||
msgid "Tous"
|
||||
msgstr "Alle"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3682
|
||||
msgid "Sélectionnez d'abord une ou plusieurs catégories."
|
||||
msgstr "Selecteer eerst een of meer categorieën."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3698
|
||||
msgid "Chargement des modèles..."
|
||||
msgstr "Modellen laden..."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3709
|
||||
msgid "Aucun modèle disponible pour les catégories sélectionnées."
|
||||
msgstr "Geen model beschikbaar voor de geselecteerde categorieën."
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3720
|
||||
msgid "Afficher les modèles"
|
||||
msgstr "Modellen tonen"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3721
|
||||
msgid "Replier les modèles"
|
||||
msgstr "Modellen inklappen"
|
||||
|
||||
#: loko/assets/static/assets/unified_index.js:3726
|
||||
msgid "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options."
|
||||
msgstr ""
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:93
|
||||
msgid "Rechercher une adresse…"
|
||||
msgstr "Zoek een adres…"
|
||||
|
||||
msgid "Rechercher une adresse..."
|
||||
msgstr "Zoek een adres..."
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:748
|
||||
msgid "Géolocalisation..."
|
||||
msgstr "Geolocatie..."
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:769
|
||||
msgid "Position actualisée"
|
||||
msgstr "Positie bijgewerkt"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:773
|
||||
msgid "GPS indisponible"
|
||||
msgstr "GPS niet beschikbaar"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:935
|
||||
msgid "Inspections (Désactivé)"
|
||||
msgstr "Inspecties (Uitgeschakeld)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:936
|
||||
msgid "Inspections (1 mois)"
|
||||
msgstr "Inspecties (1 maand)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:937
|
||||
msgid "Inspections (3 mois)"
|
||||
msgstr "Inspecties (3 maanden)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:938
|
||||
msgid "Inspections (6 mois)"
|
||||
msgstr "Inspecties (6 maanden)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:939
|
||||
msgid "Inspections (1 an)"
|
||||
msgstr "Inspecties (1 jaar)"
|
||||
|
||||
#: loko/assets/static/assets/inspection_map.js:940
|
||||
msgid "Inspections (Tout)"
|
||||
msgstr "Inspecties (Alles)"
|
||||
|
||||
msgid "Panneau"
|
||||
msgstr "Bord"
|
||||
|
||||
msgid "Supprimer ce panneau"
|
||||
msgstr "Dit bord verwijderen"
|
||||
|
||||
msgid "Mention (ex: Sauf riverains)"
|
||||
msgstr "Vermelding (bijv. Uitgezonderd aangelanden)"
|
||||
|
||||
msgid "Propreté :"
|
||||
msgstr "Netheid:"
|
||||
|
||||
msgid "Propre"
|
||||
msgstr "Schoon"
|
||||
|
||||
msgid "Sale"
|
||||
msgstr "Vuil"
|
||||
|
||||
msgid "Végétation"
|
||||
msgstr "Vegetatie"
|
||||
|
||||
msgid "Dégradé"
|
||||
msgstr "Beschadigd"
|
||||
|
||||
msgid "Coordonnées GPS manquantes."
|
||||
msgstr "Ontbrekende gps-coördinaten."
|
||||
|
||||
msgid "Veuillez ajouter au moins un panneau."
|
||||
msgstr "Voeg minimaal één bord toe."
|
||||
|
||||
msgid "Panneaux créés avec succès !"
|
||||
msgstr "Borden succesvol aangemaakt!"
|
||||
|
||||
msgid "Erreur : "
|
||||
msgstr "Fout: "
|
||||
|
||||
msgid "Impossible de créer les panneaux."
|
||||
msgstr "Kan de borden niet aanmaken."
|
||||
|
||||
msgid "Erreur lors de la création : "
|
||||
msgstr "Fout bij aanmaken: "
|
||||
|
||||
msgid "Créer et Enregistrer"
|
||||
msgstr "Aanmaken en opslaan"
|
||||
|
||||
msgid "Inspection enregistrée !"
|
||||
msgstr "Inspectie opgeslagen!"
|
||||
|
||||
msgid "Intervention"
|
||||
msgstr "Interventie"
|
||||
|
||||
msgid "créée."
|
||||
msgstr "aangemaakt."
|
||||
|
||||
msgid "Erreur lors de la validation."
|
||||
msgstr "Fout bij validatie."
|
||||
|
||||
msgid "Erreur réseau."
|
||||
msgstr "Netwerkfout."
|
||||
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Beheerszones"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "Geen beheerszone gedefinieerd voor dit thema."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Alle zones tonen / verbergen"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Dekking"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Doelfrequentie:"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "dagen"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Instructies & aandachtspunten"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Centreren op zone"
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
import time
|
||||
from datetime import datetime
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection, transaction
|
||||
from assets.models import NatureLocation, NatureTree
|
||||
from common.models import Municipality
|
||||
|
||||
|
||||
MUNICIPALITY_PREFIXES = {
|
||||
'AND': 'Anderlecht',
|
||||
'AUD': 'Auderghem',
|
||||
'BRK': 'Berchem-Sainte-Agathe',
|
||||
'BSA': 'Berchem-Sainte-Agathe',
|
||||
'BXL': 'Bruxelles',
|
||||
'BRU': 'Bruxelles',
|
||||
'ETT': 'Etterbeek',
|
||||
'EVE': 'Evere',
|
||||
'FOR': 'Forest',
|
||||
'GAN': 'Ganshoren',
|
||||
'IXL': 'Ixelles',
|
||||
'IXE': 'Ixelles',
|
||||
'JET': 'Jette',
|
||||
'KOE': 'Koekelberg',
|
||||
'MOL': 'Molenbeek-Saint-Jean',
|
||||
'STG': 'Saint-Gilles',
|
||||
'STJ': 'Saint-Josse-ten-Noode',
|
||||
'SCH': 'Schaerbeek',
|
||||
'UCC': 'Uccle',
|
||||
'WAT': 'Watermael-Boitsfort',
|
||||
'WLU': 'Woluwe-Saint-Lambert',
|
||||
'WSL': 'Woluwe-Saint-Lambert',
|
||||
'WSP': 'Woluwe-Saint-Pierre',
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Rattache spatialement les arbres (NatureTree) aux tronçons des sections vertes (NatureLocation, ex: WAT_R22_008)."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--max-distance',
|
||||
type=float,
|
||||
default=25.0,
|
||||
help="Distance maximale en mètres pour le rattachement (défaut: 25.0m, 0 pour intersection stricte)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--section-code',
|
||||
type=str,
|
||||
default=None,
|
||||
help="Code d'un tronçon spécifique à cibler (ex: WAT_R22_008)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help="Simuler le rattachement sans modifier la base de données"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-sync-munis',
|
||||
action='store_true',
|
||||
help="Ne pas synchroniser la commune sur les sections vertes"
|
||||
)
|
||||
|
||||
def log(self, msg):
|
||||
self.stdout.write(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
max_dist = options['max_distance']
|
||||
target_section = options['section_code']
|
||||
dry_run = options['dry_run']
|
||||
sync_munis = not options['skip_sync_munis']
|
||||
|
||||
self.log("=== RATTACHEMENT SPATIAL DES ARBRES AUX TRONÇONS DE SECTIONS VERTES ===")
|
||||
self.log(f"Distance max : {max_dist} m {'(Intersection stricte)' if max_dist == 0 else ''}")
|
||||
if target_section:
|
||||
self.log(f"Section ciblée : {target_section}")
|
||||
if dry_run:
|
||||
self.log("[SIMULATION / DRY-RUN] Aucune modification ne sera enregistrée.")
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# 1. Synchronisation des communes sur les sections vertes
|
||||
# -----------------------------------------------------------------
|
||||
if sync_munis and not dry_run:
|
||||
self.log("1. Synchronisation des communes sur les sections vertes...")
|
||||
munis_by_name = {m.name_fr.lower(): m for m in Municipality.objects.all()}
|
||||
updated_munis = 0
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Intersections géométriques
|
||||
cursor.execute("""
|
||||
UPDATE assets_naturelocation l
|
||||
SET municipality_id = m.id
|
||||
FROM common_municipality m
|
||||
WHERE l.code ~ '^[A-Z]{3}_'
|
||||
AND l.municipality_id IS NULL
|
||||
AND l.geom IS NOT NULL
|
||||
AND ST_Intersects(ST_PointOnSurface(l.geom), m.geom);
|
||||
""")
|
||||
updated_munis += cursor.rowcount
|
||||
|
||||
# Complément par préfixe de code pour les cas frontaliers
|
||||
locs_without_muni = NatureLocation.objects.filter(code__regex=r'^[A-Z]{3}_', municipality__isnull=True)
|
||||
for loc in locs_without_muni:
|
||||
pfx = loc.code[:3].upper()
|
||||
muni_name = MUNICIPALITY_PREFIXES.get(pfx)
|
||||
if muni_name and muni_name.lower() in munis_by_name:
|
||||
loc.municipality = munis_by_name[muni_name.lower()]
|
||||
loc.save(update_fields=['municipality'])
|
||||
updated_munis += 1
|
||||
|
||||
self.log(f" Communes renseignées sur {updated_munis} sections vertes.")
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# 2. Analyse et Rattachement des Arbres
|
||||
# -----------------------------------------------------------------
|
||||
t0 = time.time()
|
||||
self.log("2. Analyse géospatiale du rattachement arbres -> sections vertes...")
|
||||
|
||||
section_filter_sql = ""
|
||||
params = [max_dist]
|
||||
if target_section:
|
||||
section_filter_sql = "AND nearest.code = %s"
|
||||
params.append(target_section)
|
||||
|
||||
count_query = f"""
|
||||
WITH nearest AS (
|
||||
SELECT
|
||||
t.id AS tree_id,
|
||||
nearest_loc.id AS loc_id,
|
||||
nearest_loc.code AS loc_code,
|
||||
nearest_loc.name_fr AS loc_name,
|
||||
nearest_loc.dist
|
||||
FROM assets_naturetree t
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT l.id, l.code, l.name_fr, ST_Distance(t.geom, l.geom) AS dist
|
||||
FROM assets_naturelocation l
|
||||
WHERE l.code ~ '^[A-Z]{{3}}_' AND l.geom IS NOT NULL
|
||||
ORDER BY t.geom <-> l.geom
|
||||
LIMIT 1
|
||||
) nearest_loc
|
||||
WHERE t.geom IS NOT NULL AND t.lat != 0 AND t.lon != 0
|
||||
AND nearest_loc.dist <= %s
|
||||
)
|
||||
SELECT
|
||||
COUNT(*) AS total_matched,
|
||||
COUNT(*) FILTER (WHERE dist = 0) AS inside,
|
||||
COUNT(*) FILTER (WHERE dist > 0 AND dist <= 5) AS within_5m,
|
||||
COUNT(*) FILTER (WHERE dist > 5 AND dist <= 15) AS within_15m,
|
||||
COUNT(*) FILTER (WHERE dist > 15 AND dist <= 25) AS within_25m,
|
||||
COUNT(*) FILTER (WHERE dist > 25) AS beyond_25m,
|
||||
COUNT(DISTINCT loc_id) AS distinct_sections
|
||||
FROM nearest
|
||||
WHERE 1=1 {section_filter_sql};
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(count_query, params)
|
||||
stats = cursor.fetchone()
|
||||
|
||||
total_matched = stats[0]
|
||||
inside = stats[1]
|
||||
w5m = stats[2]
|
||||
w15m = stats[3]
|
||||
w25m = stats[4]
|
||||
b25m = stats[5]
|
||||
distinct_sections = stats[6]
|
||||
|
||||
self.log(f" Arbres éligibles au rattachement : {total_matched}")
|
||||
self.log(f" - Directement à l'intérieur (0m) : {inside}")
|
||||
self.log(f" - Proximité immédiate (1 à 5m) : {w5m}")
|
||||
self.log(f" - Bordure de voirie (5 à 15m) : {w15m}")
|
||||
self.log(f" - Zone étendue (15 à 25m) : {w25m}")
|
||||
if b25m:
|
||||
self.log(f" - Au-delà (> 25m) : {b25m}")
|
||||
self.log(f" Nombre de tronçons / sections concernés : {distinct_sections}")
|
||||
|
||||
if dry_run:
|
||||
self.log("Simulation terminée avec succès (aucune écriture en base).")
|
||||
return
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# 3. Application de la mise à jour
|
||||
# -----------------------------------------------------------------
|
||||
self.log("3. Application de la mise à jour des liaisons (location_id)...")
|
||||
update_query = f"""
|
||||
WITH candidates AS (
|
||||
SELECT
|
||||
t.id AS tree_id,
|
||||
nearest.id AS loc_id
|
||||
FROM assets_naturetree t
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT l.id, l.code, ST_Distance(t.geom, l.geom) AS dist
|
||||
FROM assets_naturelocation l
|
||||
WHERE l.code ~ '^[A-Z]{{3}}_' AND l.geom IS NOT NULL
|
||||
ORDER BY t.geom <-> l.geom
|
||||
LIMIT 1
|
||||
) nearest
|
||||
WHERE t.geom IS NOT NULL AND t.lat != 0 AND t.lon != 0
|
||||
AND nearest.dist <= %s
|
||||
{section_filter_sql}
|
||||
)
|
||||
UPDATE assets_naturetree t
|
||||
SET location_id = c.loc_id
|
||||
FROM candidates c
|
||||
WHERE t.id = c.tree_id
|
||||
AND (t.location_id IS DISTINCT FROM c.loc_id);
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(update_query, params)
|
||||
updated_count = cursor.rowcount
|
||||
|
||||
t1 = time.time()
|
||||
self.log(f"Mise à jour terminée : {updated_count} arbres rattachés en {t1 - t0:.2f} secondes.")
|
||||
|
||||
# Vérification ciblée (par exemple WAT_R22_008)
|
||||
check_code = target_section or 'WAT_R22_008'
|
||||
check_loc = NatureLocation.objects.filter(code=check_code).first()
|
||||
if check_loc:
|
||||
attached_cnt = check_loc.nature_trees.count()
|
||||
self.log(f"Vérification sur {check_code} ({check_loc.name_fr}) : {attached_cnt} arbres rattachés.")
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
from datetime import timedelta
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection, transaction
|
||||
from django.utils import timezone
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from assets.models import NatureTree
|
||||
from interventions.models import Intervention, SourceCategory
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Annule les interventions arboricoles caduques ou périmées :\n"
|
||||
"1. Interventions prescrites avant un diagnostic plus récent sur l'arbre (remplacées par ré-inspection).\n"
|
||||
"2. Prescriptions actives dont la date limite est échue depuis plus de X mois (défaut : 12 mois).\n"
|
||||
"Met à jour les événements de timeline et recalcule automatiquement les dates de prochaine inspection."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--months',
|
||||
type=int,
|
||||
default=12,
|
||||
help="Délai en mois après échéance au-delà duquel une prescription non exécutée est annulée (défaut : 12).",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help="Simuler l'opération et afficher les comptages sans modifier la base de données.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-recalc-trees',
|
||||
action='store_true',
|
||||
help="Ne pas recalculer next_inspection_date sur les arbres après l'annulation.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
months = options['months']
|
||||
dry_run = options['dry_run']
|
||||
recalc_trees = not options['no_recalc_trees']
|
||||
|
||||
now = timezone.now()
|
||||
# Calcul de la date seuil d'échéance (ex: il y a 12 mois)
|
||||
cutoff_date = (now - timedelta(days=int(months * 30.4375))).date()
|
||||
|
||||
self.stdout.write(self.style.MIGRATE_HEADING(
|
||||
f"=== Nettoyage des interventions d'arbres caduques et échues ==="
|
||||
))
|
||||
self.stdout.write(f"Date de référence : {now.strftime('%d/%m/%Y %H:%M')}")
|
||||
self.stdout.write(f"Seuil d'échéance ({months} mois) : interventions échues avant le {cutoff_date.strftime('%d/%m/%Y')}\n")
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING("[MODE SIMULATION] Aucune modification ne sera enregistrée.\n"))
|
||||
|
||||
content_type_tree = ContentType.objects.get_for_model(NatureTree)
|
||||
source_fulcrum = SourceCategory.objects.filter(name_fr="Fulcrum").first()
|
||||
source_fulcrum_id = source_fulcrum.id if source_fulcrum else None
|
||||
|
||||
with connection.cursor() as cur:
|
||||
# -------------------------------------------------------------
|
||||
# COMPTAGE RÈGLE 1 : Diagnostic plus récent sur l'arbre
|
||||
# -------------------------------------------------------------
|
||||
cur.execute("""
|
||||
SELECT COUNT(DISTINCT i.id)
|
||||
FROM interventions_intervention i
|
||||
JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
||||
JOIN assets_naturetree t ON t.id = ia.object_id
|
||||
WHERE ia.content_type_id = %s
|
||||
AND i.status NOT IN ('finished', 'canceled')
|
||||
AND t.last_phytosanitary_date IS NOT NULL
|
||||
AND (
|
||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
||||
);
|
||||
""", [content_type_tree.id])
|
||||
count_rule_diag = cur.fetchone()[0]
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# COMPTAGE RÈGLE 2 : Échéance dépassée > X mois (sans être dans règle 1)
|
||||
# -------------------------------------------------------------
|
||||
cur.execute("""
|
||||
SELECT COUNT(DISTINCT i.id)
|
||||
FROM interventions_intervention i
|
||||
LEFT JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
||||
LEFT JOIN assets_naturetree t ON t.id = ia.object_id AND ia.content_type_id = %s
|
||||
WHERE i.status NOT IN ('finished', 'canceled')
|
||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
||||
AND i.expected_end_time IS NOT NULL
|
||||
AND i.expected_end_time::date < %s
|
||||
AND NOT (
|
||||
t.last_phytosanitary_date IS NOT NULL
|
||||
AND (
|
||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
||||
OR (i.expected_end_time::date < t.last_phytosanitary_date)
|
||||
)
|
||||
);
|
||||
""", [content_type_tree.id, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
||||
count_rule_expired = cur.fetchone()[0]
|
||||
|
||||
# Total combiné
|
||||
cur.execute("""
|
||||
SELECT COUNT(DISTINCT i.id)
|
||||
FROM interventions_intervention i
|
||||
LEFT JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
||||
LEFT JOIN assets_naturetree t ON t.id = ia.object_id AND ia.content_type_id = %s
|
||||
WHERE i.status NOT IN ('finished', 'canceled')
|
||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
||||
AND (
|
||||
(t.last_phytosanitary_date IS NOT NULL AND (
|
||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
||||
))
|
||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < %s)
|
||||
);
|
||||
""", [content_type_tree.id, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
||||
total_to_cancel = cur.fetchone()[0]
|
||||
|
||||
self.stdout.write(f"1. Prescriptions caduques (arbre ré-inspecté ultérieurement) : {count_rule_diag}")
|
||||
self.stdout.write(f"2. Prescriptions échues depuis plus de {months} mois sans nouveau diag : {count_rule_expired}")
|
||||
self.stdout.write(self.style.MIGRATE_LABEL(f"Total des interventions à annuler : {total_to_cancel}\n"))
|
||||
|
||||
if total_to_cancel == 0:
|
||||
self.stdout.write(self.style.SUCCESS("Aucune intervention à annuler. La base est déjà assainie."))
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f"[MODE SIMULATION] {total_to_cancel} interventions auraient été passées au statut 'canceled'.\n"
|
||||
"Exécutez sans --dry-run pour appliquer."
|
||||
))
|
||||
return
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# EXÉCUTION RÉELLE EN TRANSACTION
|
||||
# -------------------------------------------------------------
|
||||
self.stdout.write("Application des annulations...")
|
||||
with transaction.atomic():
|
||||
with connection.cursor() as cur:
|
||||
# 1a. Timeline events pour la règle 1 (diagnostic plus récent)
|
||||
cur.execute("""
|
||||
INSERT INTO interventions_interventiontimeline (
|
||||
intervention_id, event_time, event_type, from_status, to_status,
|
||||
event_description, field_changes
|
||||
)
|
||||
SELECT
|
||||
i.id,
|
||||
%s,
|
||||
'status_change',
|
||||
i.status,
|
||||
'canceled',
|
||||
'Intervention annulée automatiquement : prescription caduque suite au nouveau diagnostic de l''arbre du ' ||
|
||||
TO_CHAR(t.last_phytosanitary_date, 'DD/MM/YYYY') || '.',
|
||||
('{"status": ["' || i.status || '", "canceled"]}')::json
|
||||
FROM interventions_intervention i
|
||||
JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
||||
JOIN assets_naturetree t ON t.id = ia.object_id
|
||||
WHERE ia.content_type_id = %s
|
||||
AND i.status NOT IN ('finished', 'canceled')
|
||||
AND t.last_phytosanitary_date IS NOT NULL
|
||||
AND (
|
||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM interventions_interventiontimeline tl
|
||||
WHERE tl.intervention_id = i.id
|
||||
AND tl.to_status = 'canceled'
|
||||
);
|
||||
""", [now, content_type_tree.id])
|
||||
tl_diag_count = cur.rowcount
|
||||
|
||||
# 1b. Timeline events pour la règle 2 (échues > X mois)
|
||||
cur.execute("""
|
||||
INSERT INTO interventions_interventiontimeline (
|
||||
intervention_id, event_time, event_type, from_status, to_status,
|
||||
event_description, field_changes
|
||||
)
|
||||
SELECT
|
||||
i.id,
|
||||
%s,
|
||||
'status_change',
|
||||
i.status,
|
||||
'canceled',
|
||||
'Intervention annulée automatiquement : prescription échue depuis plus de ' || %s || ' mois (' ||
|
||||
TO_CHAR(i.expected_end_time, 'DD/MM/YYYY') || ') sans nouveau diagnostic.',
|
||||
('{"status": ["' || i.status || '", "canceled"]}')::json
|
||||
FROM interventions_intervention i
|
||||
LEFT JOIN interventions_interventionasset ia ON ia.intervention_id = i.id
|
||||
WHERE i.status NOT IN ('finished', 'canceled')
|
||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
||||
AND i.expected_end_time IS NOT NULL
|
||||
AND i.expected_end_time::date < %s
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM interventions_interventiontimeline tl
|
||||
WHERE tl.intervention_id = i.id
|
||||
AND tl.to_status = 'canceled'
|
||||
);
|
||||
""", [now, months, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
||||
tl_expired_count = cur.rowcount
|
||||
|
||||
# 2. Mise à jour effective du statut vers 'canceled'
|
||||
cur.execute("""
|
||||
UPDATE interventions_intervention i
|
||||
SET status = 'canceled',
|
||||
status_order = 200,
|
||||
cancellation_type = 'non_relevant'
|
||||
FROM interventions_interventionasset ia
|
||||
LEFT JOIN assets_naturetree t ON t.id = ia.object_id AND ia.content_type_id = %s
|
||||
WHERE ia.intervention_id = i.id
|
||||
AND (ia.content_type_id = %s OR i.source_category_id = %s)
|
||||
AND i.status NOT IN ('finished', 'canceled')
|
||||
AND (
|
||||
(t.last_phytosanitary_date IS NOT NULL AND (
|
||||
(i.expected_begin_time IS NOT NULL AND i.expected_begin_time::date < t.last_phytosanitary_date)
|
||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < t.last_phytosanitary_date)
|
||||
))
|
||||
OR (i.expected_end_time IS NOT NULL AND i.expected_end_time::date < %s)
|
||||
);
|
||||
""", [content_type_tree.id, content_type_tree.id, source_fulcrum_id, cutoff_date])
|
||||
updated_count = cur.rowcount
|
||||
|
||||
# Traitement des interventions orphelines sans lien ia (ex: source Fulcrum sans arbre matché)
|
||||
cur.execute("""
|
||||
UPDATE interventions_intervention i
|
||||
SET status = 'canceled',
|
||||
status_order = 200,
|
||||
cancellation_type = 'non_relevant'
|
||||
WHERE i.source_category_id = %s
|
||||
AND i.status NOT IN ('finished', 'canceled')
|
||||
AND i.expected_end_time IS NOT NULL
|
||||
AND i.expected_end_time::date < %s;
|
||||
""", [source_fulcrum_id, cutoff_date])
|
||||
updated_count += cur.rowcount
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f" {updated_count} interventions passées au statut 'canceled'."
|
||||
))
|
||||
self.stdout.write(f" {tl_diag_count + tl_expired_count} événements d'historique créés dans la timeline.")
|
||||
|
||||
# 3. Recalcul des prochaines dates d'inspection sur les arbres
|
||||
if recalc_trees:
|
||||
self.stdout.write("Recalcul des prochaines inspections sur les arbres...")
|
||||
# 3a. Arbres avec intervention active résiduelle
|
||||
cur.execute("""
|
||||
UPDATE assets_naturetree t
|
||||
SET next_inspection_date = sub.min_date
|
||||
FROM (
|
||||
SELECT ia.object_id AS tree_id, MIN(i.expected_end_time)::date AS min_date
|
||||
FROM interventions_interventionasset ia
|
||||
JOIN interventions_intervention i ON i.id = ia.intervention_id
|
||||
WHERE ia.content_type_id = %s
|
||||
AND i.status NOT IN ('finished', 'canceled')
|
||||
AND i.expected_end_time IS NOT NULL
|
||||
GROUP BY ia.object_id
|
||||
) sub
|
||||
WHERE t.id = sub.tree_id;
|
||||
""", [content_type_tree.id])
|
||||
trees_active = cur.rowcount
|
||||
|
||||
# 3b. Arbres sans intervention active : formule théorique = dernier diagnostic + périodicité
|
||||
cur.execute("""
|
||||
UPDATE assets_naturetree t
|
||||
SET next_inspection_date = (t.last_phytosanitary_date + (COALESCE(t.pruning_frequency_years, 3) || ' years')::interval)::date
|
||||
WHERE t.last_phytosanitary_date IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM interventions_interventionasset ia
|
||||
JOIN interventions_intervention i ON i.id = ia.intervention_id
|
||||
WHERE ia.content_type_id = %s
|
||||
AND ia.object_id = t.id
|
||||
AND i.status NOT IN ('finished', 'canceled')
|
||||
AND i.expected_end_time IS NOT NULL
|
||||
);
|
||||
""", [content_type_tree.id])
|
||||
trees_theo = cur.rowcount
|
||||
|
||||
self.stdout.write(f" {trees_active} arbres mis à jour avec prochaine intervention active.")
|
||||
self.stdout.write(f" {trees_theo} arbres recalculés selon cycle théorique (diag + périodicité).")
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("\nOpération terminée avec succès !"))
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
"""
|
||||
Management command: create_structure_folders
|
||||
===========================================
|
||||
Crée les répertoires par défaut de gestion documentaire pour les structures (ouvrages d'art)
|
||||
qui n'en possèdent pas encore (ex: stations et tunnels de métro récemment importés).
|
||||
|
||||
Pour chaque ouvrage d'art traité :
|
||||
1. Crée le dossier racine rattaché à l'ouvrage via DocumentFolderAttachment (is_default=True) :
|
||||
- Nom : "<code_ouvrage> - <nom_ouvrage>" (ex: "MS001 - Station Albert")
|
||||
- Slug : "<code_minuscule>" (ex: "ms001")
|
||||
- Dossier parent : "Structures" (slug: "assets-structures")
|
||||
- Thématique : "structures"
|
||||
- Visibilité : "internal"
|
||||
2. Crée l'arborescence standard de sous-dossiers :
|
||||
- Civil engineering
|
||||
├── Inspection reports
|
||||
└── Exceptional transport
|
||||
- Electromechanics
|
||||
└── Preventive maintenance
|
||||
|
||||
Usage :
|
||||
python manage.py create_structure_folders [--dry-run] [--type TYPE] [--code CODE]
|
||||
"""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
|
||||
from assets.models.structures import Structure
|
||||
from common.models import Thematic
|
||||
from documents.models import DocumentFolder, DocumentFolderAttachment, VisibilityScope
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def ensure_structure_folders(structure, creator_user=None, dry_run=False):
|
||||
"""
|
||||
Crée ou s'assure de l'existence du dossier racine et des sous-dossiers
|
||||
standards pour un ouvrage d'art (Structure).
|
||||
Idempotent : ne recrée pas ce qui existe déjà.
|
||||
"""
|
||||
structure_ct = ContentType.objects.get_for_model(Structure)
|
||||
structures_thematic = Thematic.objects.filter(code='structures').first()
|
||||
structures_parent_folder = DocumentFolder.objects.filter(slug='assets-structures').first()
|
||||
|
||||
root_slug = slugify(structure.code)
|
||||
root_folder = DocumentFolder.objects.filter(slug=root_slug).first()
|
||||
|
||||
root_attachment = DocumentFolderAttachment.objects.filter(
|
||||
content_type=structure_ct,
|
||||
object_id=structure.pk,
|
||||
is_default=True
|
||||
).select_related('folder').first()
|
||||
|
||||
if not root_folder and root_attachment:
|
||||
root_folder = root_attachment.folder
|
||||
|
||||
root_name = f"{structure.code} - {structure.get_name()}"
|
||||
root_desc = f"Dossier pour la structure {structure.code} - {structure.get_name()}"
|
||||
|
||||
created_root = False
|
||||
if not root_folder:
|
||||
if not dry_run:
|
||||
root_folder = DocumentFolder.objects.create(
|
||||
name=root_name,
|
||||
slug=root_slug,
|
||||
description=root_desc,
|
||||
visibility=VisibilityScope.INTERNAL,
|
||||
is_predefined=True,
|
||||
application_label='structures',
|
||||
created_by=creator_user
|
||||
)
|
||||
if structures_thematic:
|
||||
root_folder.thematics.add(structures_thematic)
|
||||
if structures_parent_folder:
|
||||
root_folder.parent_folders.add(structures_parent_folder)
|
||||
created_root = True
|
||||
|
||||
if not dry_run and root_folder and not root_attachment:
|
||||
DocumentFolderAttachment.objects.get_or_create(
|
||||
content_type=structure_ct,
|
||||
object_id=structure.pk,
|
||||
folder=root_folder,
|
||||
defaults={'is_default': True}
|
||||
)
|
||||
|
||||
subfolders_created = 0
|
||||
|
||||
def get_or_create_subfolder(parent, name, sub_suffix):
|
||||
nonlocal subfolders_created
|
||||
base_slug = f"{root_slug}-{sub_suffix}"[:150]
|
||||
sub = DocumentFolder.objects.filter(slug=base_slug).first()
|
||||
if not sub and parent and getattr(parent, 'pk', None):
|
||||
sub = parent.child_folders.filter(name=name).first()
|
||||
|
||||
if not sub:
|
||||
if not dry_run:
|
||||
sub = DocumentFolder.objects.create(
|
||||
name=name,
|
||||
slug=base_slug,
|
||||
visibility=VisibilityScope.INTERNAL,
|
||||
is_predefined=False,
|
||||
application_label='structures',
|
||||
created_by=creator_user
|
||||
)
|
||||
if structures_thematic:
|
||||
sub.thematics.add(structures_thematic)
|
||||
if parent and getattr(parent, 'pk', None):
|
||||
sub.parent_folders.add(parent)
|
||||
subfolders_created += 1
|
||||
return sub, True
|
||||
return sub, False
|
||||
|
||||
# 1. Civil engineering & sous-dossiers
|
||||
civil_folder, _ = get_or_create_subfolder(root_folder, "Civil engineering", "civil-engineering")
|
||||
get_or_create_subfolder(civil_folder, "Inspection reports", "civil-engineering-inspection-reports")
|
||||
get_or_create_subfolder(civil_folder, "Exceptional transport", "civil-engineering-exceptional-transport")
|
||||
|
||||
# 2. Electromechanics & sous-dossiers
|
||||
em_folder, _ = get_or_create_subfolder(root_folder, "Electromechanics", "electromechanics")
|
||||
get_or_create_subfolder(em_folder, "Preventive maintenance", "electromechanics-preventive-maintenance")
|
||||
|
||||
return {
|
||||
'root_folder': root_folder,
|
||||
'created_root': created_root,
|
||||
'subfolders_created': subfolders_created
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Crée les répertoires par défaut de gestion documentaire pour les structures."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help="Simule l'exécution sans effectuer d'écritures en base de données."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--type',
|
||||
dest='structure_type',
|
||||
help="Filtre par type de structure (ex: metro_station,metro_tunnel)."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--code',
|
||||
dest='structure_code',
|
||||
help="Cible un ouvrage spécifique via son code (ex: MS001)."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--user',
|
||||
dest='username',
|
||||
help="Nom d'utilisateur créateur des dossiers (par défaut kdeterme ou superuser)."
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options['dry_run']
|
||||
structure_type = options.get('structure_type')
|
||||
structure_code = options.get('structure_code')
|
||||
username = options.get('username')
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING("=== Mode simulation (--dry-run) activé. Aucune modification en base. ==="))
|
||||
|
||||
creator_user = None
|
||||
if username:
|
||||
creator_user = User.objects.filter(username=username).first()
|
||||
if not creator_user:
|
||||
creator_user = User.objects.filter(username="kdeterme").first() or User.objects.filter(is_superuser=True).first()
|
||||
|
||||
qs = Structure.objects.all().order_by('code')
|
||||
|
||||
if structure_code:
|
||||
qs = qs.filter(code__iexact=structure_code)
|
||||
elif structure_type:
|
||||
types = [t.strip() for t in structure_type.split(',')]
|
||||
qs = qs.filter(structure_type__in=types)
|
||||
|
||||
structure_ct = ContentType.objects.get_for_model(Structure)
|
||||
existing_attached_ids = set(
|
||||
DocumentFolderAttachment.objects.filter(
|
||||
content_type=structure_ct,
|
||||
is_default=True
|
||||
).values_list('object_id', flat=True)
|
||||
)
|
||||
|
||||
missing_structures = [s for s in qs if s.pk not in existing_attached_ids]
|
||||
|
||||
self.stdout.write(f"Structures analysées : {qs.count()} | Sans répertoire par défaut : {len(missing_structures)}")
|
||||
|
||||
total_roots = 0
|
||||
total_subfolders = 0
|
||||
|
||||
target_structures = missing_structures if not structure_code else list(qs)
|
||||
|
||||
for structure in target_structures:
|
||||
res = ensure_structure_folders(structure, creator_user=creator_user, dry_run=dry_run)
|
||||
if res['created_root']:
|
||||
total_roots += 1
|
||||
total_subfolders += res['subfolders_created']
|
||||
self.stdout.write(f" ✓ [{structure.code}] {structure.get_name()} -> Dossier racine et arborescence configurés")
|
||||
|
||||
status_label = "[Simulation] Répertoires qui seraient créés" if dry_run else "Répertoires créés avec succès"
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"\n=== {status_label} ===\n"
|
||||
f" - Dossiers racines créés : {total_roots}\n"
|
||||
f" - Sous-dossiers créés : {total_subfolders}\n"
|
||||
f" - Total dossiers : {total_roots + total_subfolders}"
|
||||
))
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
from assets.models.structures import Structure
|
||||
from documents.models import DocumentFolder, DocumentFolderAttachment, ManagedDocument, DocumentVersion, DocumentAttachment, DocumentTag
|
||||
from documents.services import DocumentManagementService
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Imports As-Built documents from a local directory, a ZIP archive, or via rclone "
|
||||
"into the 'Electromechanics / As-Built' subfolder (preserving subfolder structure) of specified structures."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--source-dir",
|
||||
default="/tmp/as_built_sync",
|
||||
help="Path to local directory containing files/folders to import (default: /tmp/as_built_sync).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source-zip",
|
||||
default=None,
|
||||
help="Path to a ZIP file downloaded from OneDrive to extract and import.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rclone-remote",
|
||||
default=None,
|
||||
help="Optional rclone remote path (e.g. 'onedrive:DPO_TUNNEL_EM_AS-BUILT') to sync before import.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--structures",
|
||||
nargs="+",
|
||||
help="One or more structure codes (e.g. OA006 OA007) to target specifically.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clean-existing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Deletes existing documents in the As-Built subfolder hierarchy before importing.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Simulates document import without saving to database.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user",
|
||||
default=None,
|
||||
help="Username of creator/uploader.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tag",
|
||||
default=None,
|
||||
help="Optional tag name or slug to attach to imported documents.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
source_dir = options["source_dir"]
|
||||
source_zip = options["source_zip"]
|
||||
rclone_remote = options["rclone_remote"]
|
||||
target_codes = options["structures"]
|
||||
clean_existing = options["clean_existing"]
|
||||
dry_run = options["dry_run"]
|
||||
username = options["user"]
|
||||
tag_name = options["tag"]
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING("DRY RUN — no changes will be committed."))
|
||||
|
||||
# Traitement d'une archive ZIP si spécifiée
|
||||
if source_zip:
|
||||
if not os.path.isfile(source_zip):
|
||||
raise CommandError(f"ZIP file '{source_zip}' does not exist.")
|
||||
self.stdout.write(self.style.NOTICE(f"Extracting ZIP archive '{source_zip}' into '{source_dir}'..."))
|
||||
if os.path.exists(source_dir):
|
||||
shutil.rmtree(source_dir, ignore_errors=True)
|
||||
os.makedirs(source_dir, exist_ok=True)
|
||||
with zipfile.ZipFile(source_zip, 'r') as zip_ref:
|
||||
zip_ref.extractall(source_dir)
|
||||
self.stdout.write(self.style.SUCCESS("ZIP extraction completed."))
|
||||
|
||||
# Synchronisation via rclone si un remote est spécifié
|
||||
elif rclone_remote:
|
||||
self.stdout.write(self.style.NOTICE(f"Syncing from rclone remote '{rclone_remote}' to '{source_dir}'..."))
|
||||
os.makedirs(source_dir, exist_ok=True)
|
||||
rclone_cmd = ["rclone", "copy", rclone_remote, source_dir]
|
||||
rclone_conf = os.path.join(os.getcwd(), "rclone.conf")
|
||||
if os.path.exists(rclone_conf):
|
||||
rclone_cmd.extend(["--config", rclone_conf])
|
||||
|
||||
res = subprocess.run(rclone_cmd, capture_output=True, text=True)
|
||||
if res.returncode != 0:
|
||||
raise CommandError(f"rclone sync failed:\n{res.stderr}")
|
||||
self.stdout.write(self.style.SUCCESS("rclone sync completed successfully."))
|
||||
|
||||
if not os.path.isdir(source_dir):
|
||||
raise CommandError(f"Source directory '{source_dir}' does not exist or is not a directory.")
|
||||
|
||||
# Creator user lookup
|
||||
creator_user = None
|
||||
if username:
|
||||
try:
|
||||
creator_user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
raise CommandError(f"User '{username}' not found.")
|
||||
else:
|
||||
creator_user = User.objects.filter(username="kdeterme").first() or User.objects.filter(is_superuser=True).first()
|
||||
|
||||
structure_ct = ContentType.objects.get_for_model(Structure)
|
||||
|
||||
# Optional tag lookup
|
||||
doc_tag = None
|
||||
if tag_name:
|
||||
doc_tag = DocumentTag.objects.filter(slug=slugify(tag_name)).first() or DocumentTag.objects.filter(name_fr__iexact=tag_name).first()
|
||||
|
||||
def get_or_create_as_built_folder(structure):
|
||||
root_att = DocumentFolderAttachment.objects.filter(
|
||||
content_type=structure_ct,
|
||||
object_id=structure.pk,
|
||||
is_default=True
|
||||
).select_related("folder").first()
|
||||
|
||||
if not root_att:
|
||||
return None
|
||||
|
||||
root_folder = root_att.folder
|
||||
|
||||
# Electromechanics subfolder
|
||||
em_folder = root_folder.child_folders.filter(name="Electromechanics").first()
|
||||
if not em_folder:
|
||||
em_slug = f"{root_folder.slug}-electromechanics"[:150]
|
||||
if dry_run:
|
||||
em_folder = DocumentFolder(name="Electromechanics", slug=em_slug)
|
||||
else:
|
||||
em_folder, _ = DocumentFolder.objects.get_or_create(
|
||||
slug=em_slug,
|
||||
defaults={
|
||||
"name": "Electromechanics",
|
||||
"application_label": root_folder.application_label,
|
||||
"created_by": creator_user
|
||||
}
|
||||
)
|
||||
em_folder.parent_folders.add(root_folder)
|
||||
|
||||
# As-Built subfolder
|
||||
ab_folder = em_folder.child_folders.filter(name="As-Built").first() if em_folder.pk else None
|
||||
if not ab_folder:
|
||||
ab_slug = f"{em_folder.slug}-as-built"[:150]
|
||||
if dry_run:
|
||||
ab_folder = DocumentFolder(name="As-Built", slug=ab_slug)
|
||||
else:
|
||||
ab_folder, _ = DocumentFolder.objects.get_or_create(
|
||||
slug=ab_slug,
|
||||
defaults={
|
||||
"name": "As-Built",
|
||||
"application_label": em_folder.application_label,
|
||||
"created_by": creator_user
|
||||
}
|
||||
)
|
||||
ab_folder.parent_folders.add(em_folder)
|
||||
|
||||
return ab_folder
|
||||
|
||||
def get_or_create_nested_folder(parent, rel_dir_path):
|
||||
if not rel_dir_path or rel_dir_path == ".":
|
||||
return parent
|
||||
|
||||
parts = [p.strip() for p in rel_dir_path.split("/") if p.strip()]
|
||||
current = parent
|
||||
|
||||
for part in parts:
|
||||
base_slug = f"{current.slug}-{slugify(part)}"
|
||||
slug = base_slug[:150]
|
||||
|
||||
folder = current.child_folders.filter(name=part).first() if current.pk else None
|
||||
if not folder:
|
||||
folder = DocumentFolder.objects.filter(slug=slug).first()
|
||||
|
||||
if not folder:
|
||||
counter = 1
|
||||
while DocumentFolder.objects.filter(slug=slug).exists():
|
||||
slug = f"{base_slug[:140]}-{counter}"
|
||||
counter += 1
|
||||
|
||||
if dry_run:
|
||||
folder = DocumentFolder(name=part, slug=slug)
|
||||
else:
|
||||
folder, _ = DocumentFolder.objects.get_or_create(
|
||||
slug=slug,
|
||||
defaults={
|
||||
"name": part,
|
||||
"application_label": current.application_label,
|
||||
"created_by": creator_user
|
||||
}
|
||||
)
|
||||
folder.thematics.set(current.thematics.all())
|
||||
folder.parent_folders.add(current)
|
||||
current = folder
|
||||
|
||||
return current
|
||||
|
||||
# Determine structures to process
|
||||
if target_codes:
|
||||
structures = Structure.objects.filter(code__in=target_codes)
|
||||
if not structures.exists():
|
||||
raise CommandError(f"No structures found matching codes: {target_codes}")
|
||||
else:
|
||||
structures = Structure.objects.all()
|
||||
|
||||
total_imported = 0
|
||||
total_skipped = 0
|
||||
|
||||
for structure in structures:
|
||||
# Check if there is a dedicated subfolder for this structure code, location codes or name in source_dir
|
||||
struct_dir = None
|
||||
possible_dirs = [
|
||||
os.path.join(source_dir, structure.code),
|
||||
os.path.join(source_dir, f"{structure.code}_{structure.name_fr}"),
|
||||
os.path.join(source_dir, structure.name_fr or ""),
|
||||
]
|
||||
|
||||
# Also check all associated location codes (e.g. WTB03)
|
||||
loc_codes = list(structure.all_locations.values_list("code", flat=True))
|
||||
for loc_code in loc_codes:
|
||||
possible_dirs.append(os.path.join(source_dir, loc_code))
|
||||
if os.path.isdir(source_dir):
|
||||
for folder_item in os.listdir(source_dir):
|
||||
if folder_item.upper().startswith(loc_code.upper()):
|
||||
possible_dirs.append(os.path.join(source_dir, folder_item))
|
||||
|
||||
for p in possible_dirs:
|
||||
if os.path.isdir(p):
|
||||
struct_dir = p
|
||||
break
|
||||
|
||||
if not struct_dir and os.path.isdir(source_dir):
|
||||
# Check if any subfolder name contains structure code or main keywords from name_fr
|
||||
name_words = [w.lower() for w in (structure.name_fr or "").replace("-", " ").replace("_", " ").replace("(", "").replace(")", "").split() if len(w) > 3 and w.lower() not in ["tunnel", "vers", "pont", "viaduc"]]
|
||||
for folder_item in os.listdir(source_dir):
|
||||
full_p = os.path.join(source_dir, folder_item)
|
||||
if os.path.isdir(full_p) and not folder_item.startswith("."):
|
||||
folder_lower = folder_item.lower()
|
||||
if structure.code.lower() in folder_lower or any(w in folder_lower for w in name_words):
|
||||
struct_dir = full_p
|
||||
break
|
||||
|
||||
# If still not found and single structure requested with single directory extracted
|
||||
if not struct_dir and target_codes and len(target_codes) == 1:
|
||||
subdirs = [os.path.join(source_dir, d) for d in os.listdir(source_dir) if os.path.isdir(os.path.join(source_dir, d)) and not d.startswith(".")]
|
||||
if len(subdirs) == 1:
|
||||
struct_dir = subdirs[0]
|
||||
|
||||
# Collect files with their relative subfolder paths
|
||||
file_entries = [] # tuples of (full_file_path, relative_dir_path)
|
||||
if struct_dir:
|
||||
for root, _, files in os.walk(struct_dir):
|
||||
rel_dir = os.path.relpath(root, struct_dir)
|
||||
for f in files:
|
||||
if not f.startswith("."):
|
||||
file_entries.append((os.path.join(root, f), rel_dir))
|
||||
elif target_codes:
|
||||
# If specific structures requested but no subfolder, import all files in source_dir for these structures
|
||||
for root, _, files in os.walk(source_dir):
|
||||
rel_dir = os.path.relpath(root, source_dir)
|
||||
for f in files:
|
||||
if not f.startswith("."):
|
||||
file_entries.append((os.path.join(root, f), rel_dir))
|
||||
|
||||
if not file_entries:
|
||||
continue
|
||||
|
||||
self.stdout.write(f"\nProcessing Structure [{structure.code}] {structure.get_name()}")
|
||||
as_built_folder = get_or_create_as_built_folder(structure)
|
||||
|
||||
if not as_built_folder:
|
||||
self.stdout.write(self.style.ERROR(f" No default folder/As-Built folder for Structure {structure.code}. Skipped."))
|
||||
continue
|
||||
|
||||
if clean_existing and as_built_folder.pk:
|
||||
all_ab_folders = DocumentManagementService.get_folder_and_descendants(as_built_folder)
|
||||
existing_docs = ManagedDocument.objects.filter(
|
||||
folders__pk__in=all_ab_folders,
|
||||
is_archived=False
|
||||
).distinct()
|
||||
deleted_count = existing_docs.count()
|
||||
if deleted_count > 0:
|
||||
self.stdout.write(self.style.WARNING(f" Cleaning {deleted_count} existing document(s) in As-Built folder..."))
|
||||
if not dry_run:
|
||||
existing_docs.delete()
|
||||
|
||||
descendant_folder_ids = [f_id for f_id in all_ab_folders if f_id != as_built_folder.pk]
|
||||
if descendant_folder_ids:
|
||||
descendant_folders = DocumentFolder.objects.filter(pk__in=descendant_folder_ids)
|
||||
f_count = descendant_folders.count()
|
||||
if f_count > 0:
|
||||
self.stdout.write(self.style.WARNING(f" Cleaning {f_count} existing subfolder(s) in As-Built folder..."))
|
||||
if not dry_run:
|
||||
descendant_folders.delete()
|
||||
|
||||
self.stdout.write(f" Target root subfolder: Electromechanics / As-Built (ID: {as_built_folder.pk or 'dry-run'})")
|
||||
self.stdout.write(f" Found {len(file_entries)} file(s) to process.")
|
||||
|
||||
for file_path, rel_dir_path in file_entries:
|
||||
rel_name = os.path.basename(file_path)
|
||||
doc_title = os.path.splitext(rel_name)[0]
|
||||
|
||||
target_folder = get_or_create_nested_folder(as_built_folder, rel_dir_path)
|
||||
|
||||
# Deduplication check
|
||||
if target_folder.pk and not clean_existing:
|
||||
existing_doc = ManagedDocument.objects.filter(
|
||||
folders=target_folder,
|
||||
title=doc_title,
|
||||
is_archived=False
|
||||
).first()
|
||||
if existing_doc:
|
||||
self.stdout.write(f" Document '{rel_name}' already exists in folder '{target_folder.name}' (Doc ID: {existing_doc.pk}). Skipped.")
|
||||
total_skipped += 1
|
||||
continue
|
||||
|
||||
folder_display = rel_dir_path if rel_dir_path != "." else "As-Built"
|
||||
if dry_run:
|
||||
self.stdout.write(f" [Dry-run] Would import '{rel_name}' into folder 'As-Built / {folder_display}'")
|
||||
total_imported += 1
|
||||
else:
|
||||
with open(file_path, "rb") as f:
|
||||
file_data = f.read()
|
||||
|
||||
with transaction.atomic():
|
||||
doc = ManagedDocument.objects.create(
|
||||
title=doc_title,
|
||||
description=f"As-Built document imported for structure {structure.code}",
|
||||
created_by=creator_user,
|
||||
)
|
||||
|
||||
version = DocumentVersion(
|
||||
document=doc,
|
||||
uploaded_by=creator_user,
|
||||
comment="Imported As-Built document",
|
||||
status=DocumentVersion.STATUS_APPROVED,
|
||||
validated_at=timezone.now(),
|
||||
)
|
||||
version.file.save(rel_name, ContentFile(file_data), save=True)
|
||||
|
||||
doc.folders.add(target_folder)
|
||||
if doc_tag:
|
||||
doc.tags.add(doc_tag)
|
||||
|
||||
DocumentAttachment.objects.create(
|
||||
document=doc,
|
||||
content_type=structure_ct,
|
||||
object_id=structure.pk,
|
||||
attached_by=creator_user
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f" Imported '{rel_name}' into 'As-Built / {folder_display}' (Doc ID: {doc.pk})"))
|
||||
total_imported += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"\nDone! Imported: {total_imported}, Skipped (already imported): {total_skipped}"))
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.gis.geos import Point, Polygon, MultiPolygon
|
||||
from django.db import transaction
|
||||
from shapely.geometry import shape, Point as ShpPoint
|
||||
|
||||
from common.models import Thematic, Municipality
|
||||
from assets.models import AssetCategory
|
||||
from assets.models.sign import SignStreet
|
||||
from assets.models.parking import ParkingLocation, ParkingSpot
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Import parking areas, bicycle parking stands and PMR/VE spots for Court-Saint-Étienne"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write("=== Importing Parking & Bicycle Spots for Court-Saint-Étienne ===")
|
||||
|
||||
parking_thematic = Thematic.objects.filter(code='parking').first()
|
||||
parking_cat = AssetCategory.objects.filter(code='parking_spot').first()
|
||||
bike_cat = AssetCategory.objects.filter(code='bike_parking_spot').first()
|
||||
|
||||
cse = Municipality.objects.get(niscode='25014')
|
||||
cse_geom_wgs84 = cse.geom.clone()
|
||||
cse_geom_wgs84.transform(4326)
|
||||
cse_poly = shape(json.loads(cse_geom_wgs84.geojson))
|
||||
|
||||
streets = list(SignStreet.objects.filter(geom__isnull=False))
|
||||
if not streets:
|
||||
self.stderr.write("No SignStreet found in database!")
|
||||
return
|
||||
|
||||
osm_json_path = Path("/tmp/osm_cse_sign_parking.json")
|
||||
if not osm_json_path.exists():
|
||||
self.stderr.write(f"OSM JSON not found: {osm_json_path}")
|
||||
return
|
||||
|
||||
with open(osm_json_path, 'r', encoding='utf-8') as f:
|
||||
osm_data = json.load(f)
|
||||
|
||||
elements = osm_data.get('elements', [])
|
||||
|
||||
# Filter strictly inside CSE
|
||||
in_cse = []
|
||||
for el in elements:
|
||||
lat = el.get('lat') or (el.get('center') and el['center'].get('lat'))
|
||||
lon = el.get('lon') or (el.get('center') and el['center'].get('lon'))
|
||||
if lat is not None and lon is not None:
|
||||
p = ShpPoint(lon, lat)
|
||||
if cse_poly.contains(p):
|
||||
el['_pt_wgs84'] = Point(lon, lat, srid=4326)
|
||||
in_cse.append(el)
|
||||
|
||||
self.stdout.write(f"Elements strictly in CSE: {len(in_cse)}")
|
||||
|
||||
# ─── 1. ParkingLocation (Zones de stationnement) ───────────────────
|
||||
self.stdout.write("--- Step 1: ParkingLocation (Aires/Parcs) ---")
|
||||
parking_elements = [e for e in in_cse if e.get('tags', {}).get('amenity') == 'parking']
|
||||
self.stdout.write(f"Parking lots in CSE: {len(parking_elements)}")
|
||||
|
||||
# Existing locations
|
||||
existing_loc_codes = set(ParkingLocation.objects.values_list('code', flat=True))
|
||||
max_loc_num = 0
|
||||
for c in existing_loc_codes:
|
||||
if c and c.startswith('CSE-PKG-'):
|
||||
try:
|
||||
num = int(c.split('-')[-1])
|
||||
if num > max_loc_num:
|
||||
max_loc_num = num
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
new_locations = []
|
||||
for p in parking_elements:
|
||||
tags = p.get('tags', {})
|
||||
pt_4326 = p['_pt_wgs84']
|
||||
pt_3812 = pt_4326.transform(3812, clone=True)
|
||||
|
||||
closest_street = min(streets, key=lambda st: st.geom.distance(pt_3812))
|
||||
|
||||
# Determine Name
|
||||
osm_name = tags.get('name')
|
||||
if osm_name:
|
||||
name_fr = osm_name.strip()
|
||||
else:
|
||||
name_fr = f"Parking {closest_street.name_fr or 'public'}"
|
||||
|
||||
# Capacity
|
||||
try:
|
||||
cap = int(tags.get('capacity', 15))
|
||||
except (ValueError, TypeError):
|
||||
cap = 15
|
||||
|
||||
# Size proportional to capacity
|
||||
size = max(16.0, min(65.0, (cap or 15) * 1.8))
|
||||
half = size / 2.0
|
||||
cx, cy = pt_3812.x, pt_3812.y
|
||||
poly = Polygon([
|
||||
(cx - half, cy - half),
|
||||
(cx + half, cy - half),
|
||||
(cx + half, cy + half),
|
||||
(cx - half, cy + half),
|
||||
(cx - half, cy - half),
|
||||
], srid=3812)
|
||||
mpoly = MultiPolygon([poly])
|
||||
|
||||
access = tags.get('access', 'public')
|
||||
fee = tags.get('fee', 'non payant')
|
||||
parking_type = tags.get('parking', 'surface')
|
||||
desc = f"Aire de stationnement ({parking_type}). Accès: {access}. Tarification: {fee}. Capacité estimée: {cap} places."
|
||||
|
||||
max_loc_num += 1
|
||||
code = f"CSE-PKG-{max_loc_num:04d}"
|
||||
|
||||
loc = ParkingLocation(
|
||||
code=code,
|
||||
name_fr=name_fr,
|
||||
geom=mpoly,
|
||||
description=desc,
|
||||
address=f"{closest_street.name_fr or 'Court-Saint-Étienne'}, 1490 Court-Saint-Étienne",
|
||||
|
||||
)
|
||||
# Sync
|
||||
loc.sync_geom_and_geojson()
|
||||
new_locations.append(loc)
|
||||
|
||||
with transaction.atomic():
|
||||
ParkingLocation.objects.bulk_create(new_locations)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created {len(new_locations)} ParkingLocations. Total in DB: {ParkingLocation.objects.count()}"))
|
||||
|
||||
# Re-fetch all locations with spatial index
|
||||
all_locations = list(ParkingLocation.objects.filter(geom__isnull=False))
|
||||
|
||||
# ─── 2. ParkingSpot (Vélos & Emplacements individuels) ────────────
|
||||
self.stdout.write("--- Step 2: ParkingSpot (Vélos, PMR, VE) ---")
|
||||
bike_elements = [e for e in in_cse if e.get('tags', {}).get('amenity') == 'bicycle_parking']
|
||||
space_elements = [e for e in in_cse if e.get('tags', {}).get('amenity') == 'parking_space']
|
||||
|
||||
self.stdout.write(f"Bicycle parkings in CSE: {len(bike_elements)}")
|
||||
self.stdout.write(f"Parking spaces in CSE: {len(space_elements)}")
|
||||
|
||||
max_spot_num = 0
|
||||
for sc in ParkingSpot.objects.values_list('code', flat=True):
|
||||
if sc:
|
||||
try:
|
||||
num = int(sc.split('-')[-1])
|
||||
if num > max_spot_num:
|
||||
max_spot_num = num
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
new_spots = []
|
||||
|
||||
# A. Bicycle parkings
|
||||
for b in bike_elements:
|
||||
tags = b.get('tags', {})
|
||||
pt_4326 = b['_pt_wgs84']
|
||||
pt_3812 = pt_4326.transform(3812, clone=True)
|
||||
|
||||
closest_street = min(streets, key=lambda st: st.geom.distance(pt_3812))
|
||||
closest_loc = min(all_locations, key=lambda l: l.geom.distance(pt_3812)) if all_locations else None
|
||||
|
||||
# Capacity & stands
|
||||
try:
|
||||
cap = int(tags.get('capacity', 6))
|
||||
except (ValueError, TypeError):
|
||||
cap = 6
|
||||
|
||||
btype_osm = (tags.get('bicycle_parking') or '').lower()
|
||||
if 'box' in btype_osm or 'shed' in btype_osm or 'secure' in btype_osm:
|
||||
stand_type = 'box'
|
||||
stands_label = "Box / Abri sécurisé"
|
||||
elif 'rack' in btype_osm or 'wall' in btype_osm:
|
||||
stand_type = 'rack'
|
||||
stands_label = "Râteau / Pince-roue"
|
||||
else:
|
||||
stand_type = 'normal'
|
||||
stands_label = "Arceaux U standard"
|
||||
|
||||
# 3m x 2m polygon in 3812
|
||||
cx, cy = pt_3812.x, pt_3812.y
|
||||
poly = Polygon([
|
||||
(cx - 1.5, cy - 1.0),
|
||||
(cx + 1.5, cy - 1.0),
|
||||
(cx + 1.5, cy + 1.0),
|
||||
(cx - 1.5, cy + 1.0),
|
||||
(cx - 1.5, cy - 1.0),
|
||||
], srid=3812)
|
||||
|
||||
max_spot_num += 1
|
||||
code = f"CSE-PKV-{max_spot_num:04d}"
|
||||
osm_name = tags.get('name')
|
||||
name_fr = osm_name if osm_name else f"Stationnement vélos ({stands_label}) - {closest_street.name_fr or 'Rue'}"
|
||||
|
||||
spot = ParkingSpot(
|
||||
code=code,
|
||||
name_fr=name_fr,
|
||||
spot_type='bicycle',
|
||||
stand_type=stand_type,
|
||||
capacity=cap,
|
||||
standard_stand_count=max(1, cap // 2),
|
||||
geom=poly,
|
||||
location=closest_loc,
|
||||
street=closest_street,
|
||||
|
||||
category=bike_cat,
|
||||
|
||||
is_compliant=True,
|
||||
)
|
||||
spot.sync_geom_and_geojson()
|
||||
new_spots.append(spot)
|
||||
|
||||
# B. Parking Spaces (PMR, EV charging, Car)
|
||||
for s in space_elements:
|
||||
tags = s.get('tags', {})
|
||||
pt_4326 = s['_pt_wgs84']
|
||||
pt_3812 = pt_4326.transform(3812, clone=True)
|
||||
|
||||
closest_street = min(streets, key=lambda st: st.geom.distance(pt_3812))
|
||||
closest_loc = min(all_locations, key=lambda l: l.geom.distance(pt_3812)) if all_locations else None
|
||||
|
||||
# Check PMR vs EV vs standard
|
||||
is_pmr = (
|
||||
tags.get('capacity:disabled') in ('1', 'yes') or
|
||||
tags.get('disabled') in ('yes', 'designated') or
|
||||
tags.get('access') == 'disabled' or
|
||||
'pmr' in str(tags).lower() or
|
||||
'disabled' in str(tags).lower()
|
||||
)
|
||||
is_ev = (
|
||||
'charging' in str(tags).lower() or
|
||||
tags.get('socket:type2') or
|
||||
tags.get('amenity') == 'charging_station'
|
||||
)
|
||||
|
||||
if is_pmr:
|
||||
spot_type = 'pmr'
|
||||
spot_label = "Emplacement PMR"
|
||||
elif is_ev:
|
||||
spot_type = 'ev_charging'
|
||||
spot_label = "Borne recharge VE"
|
||||
else:
|
||||
spot_type = 'car_parking'
|
||||
spot_label = "Emplacement automobile"
|
||||
|
||||
# 2.5m x 5m polygon
|
||||
cx, cy = pt_3812.x, pt_3812.y
|
||||
poly = Polygon([
|
||||
(cx - 1.25, cy - 2.5),
|
||||
(cx + 1.25, cy - 2.5),
|
||||
(cx + 1.25, cy + 2.5),
|
||||
(cx - 1.25, cy + 2.5),
|
||||
(cx - 1.25, cy - 2.5),
|
||||
], srid=3812)
|
||||
|
||||
max_spot_num += 1
|
||||
code = f"CSE-PKS-{max_spot_num:04d}"
|
||||
name_fr = f"{spot_label} - {closest_street.name_fr or 'Rue'}"
|
||||
|
||||
spot = ParkingSpot(
|
||||
code=code,
|
||||
name_fr=name_fr,
|
||||
spot_type=spot_type,
|
||||
capacity=1,
|
||||
geom=poly,
|
||||
location=closest_loc,
|
||||
street=closest_street,
|
||||
|
||||
category=parking_cat,
|
||||
|
||||
is_compliant=True,
|
||||
)
|
||||
spot.sync_geom_and_geojson()
|
||||
new_spots.append(spot)
|
||||
|
||||
with transaction.atomic():
|
||||
ParkingSpot.objects.bulk_create(new_spots)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created {len(new_spots)} ParkingSpots (total: {ParkingSpot.objects.count()})."))
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.contrib.gis.geos import Point, GEOSGeometry
|
||||
from django.db import transaction
|
||||
|
||||
from common.models import Thematic
|
||||
from assets.models import AssetCategory
|
||||
from assets.models.sign import SignStreet, SignPole, SignPanel, SignPanelType
|
||||
|
||||
BELGIAN_PANEL_TYPES = [
|
||||
# Priorité
|
||||
('B1', 'Cédez le passage', 'Voorrang verlenen', 'B/B1.svg'),
|
||||
('B5', 'Arrêt obligatoire (Stop)', 'Stop', 'B/B5.svg'),
|
||||
('B9', 'Route prioritaire', 'Voorrangsweg', 'B/B9.svg'),
|
||||
('B15', 'Fin de route prioritaire', 'Einde voorrangsweg', 'B/B15.svg'),
|
||||
|
||||
# Danger
|
||||
('A1a', 'Virage dangereux à droite', 'Gevaarlijke bocht naar rechts', 'A/A1A.svg'),
|
||||
('A1b', 'Virage dangereux à gauche', 'Gevaarlijke bocht naar links', 'A/A1B.svg'),
|
||||
('A7a', 'Passage pour piétons', 'Voetgangersoversteekplaats', 'A/A7A.svg'),
|
||||
('A14', 'Présence d\'enfants', 'Kinderen', 'A/A14.svg'),
|
||||
('A23', 'Travaux', 'Werken', 'A/A23.svg'),
|
||||
('A25', 'Ralentisseur de trafic', 'Verkeersdrempel', 'A/A25.svg'),
|
||||
|
||||
# Interdiction
|
||||
('C1', 'Sens interdit', 'Verboden richting', 'C/C1.svg'),
|
||||
('C3', 'Accès interdit dans les deux sens', 'Verboden toegang in beide richtingen', 'C/C3.svg'),
|
||||
('C11', 'Accès interdit aux cycles', 'Verboden toegang voor fietsers', 'C/C11.svg'),
|
||||
('C43', 'Vitesse maximale limitée (50/30 km/h)', 'Maximumsnelheid', 'C/C43.svg'),
|
||||
('C45', 'Fin de limitation de vitesse', 'Einde snelheidsbeperking', 'C/C45.svg'),
|
||||
|
||||
# Obligation
|
||||
('D7', 'Piste cyclable obligatoire', 'Verplicht fietspad', 'D/D7.svg'),
|
||||
('D9', 'Partage piétons / cyclistes', 'Gedeeld voetpad en fietspad', 'D/D9.svg'),
|
||||
|
||||
# Stationnement
|
||||
('E1', 'Stationnement interdit', 'Parkeerverbod', 'E/E1.svg'),
|
||||
('E3', 'Arrêt et stationnement interdits', 'Stilstand- en parkeerverbod', 'E/E3.svg'),
|
||||
('E9a', 'Stationnement autorisé', 'Parkeren toegelaten', 'E/E9A.svg'),
|
||||
('E9b', 'Stationnement réservé aux PMR', 'Parkeren voor personen met een handicap', 'E/E9B.svg'),
|
||||
('E9c', 'Stationnement camionnettes / camions', 'Parkeren voor vrachtwagens', 'E/E9C.svg'),
|
||||
('E9h', 'Stationnement véhicules électriques (recharge)', 'Parkeren voor elektrische voertuigen', 'E/E9H.svg'),
|
||||
|
||||
# Indications
|
||||
('F4a', 'Début de zone 30', 'Begin van een zone 30', 'F/F4A.svg'),
|
||||
('F4b', 'Fin de zone 30', 'Einde van een zone 30', 'F/F4B.svg'),
|
||||
('F12a', 'Zone résidentielle ou de rencontre', 'Woonerf of erf', 'F/F12A.svg'),
|
||||
('F19', 'Voie publique à sens unique', 'Eenrichtingsverkeer', 'F/F19.svg'),
|
||||
('F45', 'Impasse', 'Doodlopende weg', 'F/F45.svg'),
|
||||
('F49', 'Passage pour piétons', 'Voetgangersoversteekplaats', 'F/F49.svg'),
|
||||
('F99a', 'Chemin réservé piétons / cyclistes', 'Weg voorbehouden voor voetgangers en fietsers', 'F/F99A.svg'),
|
||||
('F101a', 'Fin de chemin réservé', 'Einde weg voorbehouden', 'F/F101A.svg'),
|
||||
]
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Import signage (PICC poles and road panels) for Court-Saint-Étienne"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write("=== Importing Signage for Court-Saint-Étienne ===")
|
||||
|
||||
# 1. SignPanelType catalog
|
||||
self.stdout.write("--- Step 1: SignPanelType catalog ---")
|
||||
media_target_dir = Path(settings.MEDIA_ROOT) / "sign" / "signpanels_types"
|
||||
media_target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
static_road_signs = Path(settings.BASE_DIR) / "assets" / "static" / "assets" / "road_signs"
|
||||
if not static_road_signs.exists():
|
||||
# Fallback check
|
||||
static_road_signs = Path("/app/static/assets/road_signs")
|
||||
|
||||
created_types = 0
|
||||
for code, name_fr, name_nl, rel_svg in BELGIAN_PANEL_TYPES:
|
||||
pt, created = SignPanelType.objects.get_or_create(
|
||||
code=code,
|
||||
defaults={
|
||||
'name_fr': name_fr,
|
||||
'name_nl': name_nl,
|
||||
'default_width': 50.0,
|
||||
'default_height': 50.0,
|
||||
}
|
||||
)
|
||||
# Copy SVG icon if exists
|
||||
svg_source = static_road_signs / rel_svg
|
||||
if svg_source.exists():
|
||||
dst_name = f"{code.lower()}.svg"
|
||||
dst_path = media_target_dir / dst_name
|
||||
if not dst_path.exists():
|
||||
shutil.copyfile(svg_source, dst_path)
|
||||
pt.sgv_picture = f"sign/signpanels_types/{dst_name}"
|
||||
pt.save(update_fields=['sgv_picture'])
|
||||
if created:
|
||||
created_types += 1
|
||||
self.stdout.write(f"SignPanelTypes ensured: {SignPanelType.objects.count()} (created {created_types})")
|
||||
|
||||
# 2. Import PICC Poles
|
||||
self.stdout.write("--- Step 2: PICC Poles ---")
|
||||
sign_thematic = Thematic.objects.filter(code='sign').first()
|
||||
pole_category = AssetCategory.objects.filter(code='SI_POLE').first()
|
||||
|
||||
streets = list(SignStreet.objects.filter(geom__isnull=False))
|
||||
if not streets:
|
||||
self.stderr.write("No SignStreet found in database!")
|
||||
return
|
||||
|
||||
# Preload existing poles to avoid duplicates (< 1.5m)
|
||||
existing_poles = list(SignPole.objects.all().values('id', 'code', 'geom'))
|
||||
self.stdout.write(f"Existing poles in DB: {len(existing_poles)}")
|
||||
|
||||
# Find highest code number
|
||||
max_num = 0
|
||||
for ep in existing_poles:
|
||||
c = ep.get('code') or ''
|
||||
if c.startswith('CSE-SGP-'):
|
||||
try:
|
||||
num = int(c.split('-')[-1])
|
||||
if num > max_num:
|
||||
max_num = num
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
geojson_path = Path("/tmp/cse_picc_poles.geojson")
|
||||
if not geojson_path.exists():
|
||||
self.stderr.write(f"GeoJSON not found: {geojson_path}")
|
||||
return
|
||||
|
||||
with open(geojson_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
features = data.get('features', [])
|
||||
self.stdout.write(f"PICC poles in GeoJSON: {len(features)}")
|
||||
|
||||
# Street poles counters for names
|
||||
street_counters = {}
|
||||
for s in streets:
|
||||
street_counters[s.id] = SignPole.objects.filter(street=s).count()
|
||||
|
||||
new_poles = []
|
||||
skipped_existing = 0
|
||||
|
||||
for feat in features:
|
||||
coords = feat['geometry']['coordinates']
|
||||
x, y = coords[0], coords[1]
|
||||
pt = Point(x, y, srid=3812)
|
||||
|
||||
# Check if existing pole is close (< 1.5m)
|
||||
is_duplicate = False
|
||||
for ep in existing_poles:
|
||||
eg = ep.get('geom')
|
||||
if eg and eg.distance(pt) < 1.5:
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
if is_duplicate:
|
||||
skipped_existing += 1
|
||||
continue
|
||||
|
||||
# Find closest street
|
||||
closest_street = min(streets, key=lambda st: st.geom.distance(pt))
|
||||
street_counters[closest_street.id] = street_counters.get(closest_street.id, 0) + 1
|
||||
street_num = street_counters[closest_street.id]
|
||||
|
||||
max_num += 1
|
||||
code = f"CSE-SGP-{max_num:04d}"
|
||||
name = f"Poteau signalisation - {closest_street.name_fr or 'Rue'} #{street_num}"
|
||||
|
||||
pole = SignPole(
|
||||
code=code,
|
||||
name_fr=name,
|
||||
geom=pt,
|
||||
street=closest_street,
|
||||
|
||||
category=pole_category,
|
||||
status='active',
|
||||
orientation=90
|
||||
)
|
||||
# Sync lon, lat, geojson
|
||||
pole.sync_geom_and_geojson()
|
||||
new_poles.append(pole)
|
||||
|
||||
# Add to local cache for duplicate checking
|
||||
existing_poles.append({'id': None, 'code': code, 'geom': pt})
|
||||
|
||||
if new_poles:
|
||||
with transaction.atomic():
|
||||
SignPole.objects.bulk_create(new_poles, batch_size=500)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created {len(new_poles)} new SignPoles (skipped {skipped_existing} existing). Total in DB: {SignPole.objects.count()}"))
|
||||
else:
|
||||
self.stdout.write(f"No new poles created (skipped {skipped_existing} duplicates).")
|
||||
|
||||
# 3. Import Panels from OSM
|
||||
self.stdout.write("--- Step 3: SignPanels from OSM ---")
|
||||
panel_category = AssetCategory.objects.filter(code='SI_PANEL').first()
|
||||
osm_json_path = Path("/tmp/osm_cse_sign_parking.json")
|
||||
|
||||
if not osm_json_path.exists():
|
||||
self.stderr.write(f"OSM JSON not found: {osm_json_path}")
|
||||
return
|
||||
|
||||
with open(osm_json_path, 'r', encoding='utf-8') as f:
|
||||
osm_data = json.load(f)
|
||||
|
||||
osm_elements = osm_data.get('elements', [])
|
||||
osm_signs = [
|
||||
e for e in osm_elements
|
||||
if 'traffic_sign' in e.get('tags', {}) or e.get('tags', {}).get('highway') in ('give_way', 'stop')
|
||||
]
|
||||
self.stdout.write(f"OSM signs found in dataset: {len(osm_signs)}")
|
||||
|
||||
all_poles = list(SignPole.objects.select_related('street').all())
|
||||
type_b1 = SignPanelType.objects.filter(code='B1').first()
|
||||
type_b5 = SignPanelType.objects.filter(code='B5').first()
|
||||
type_f99a = SignPanelType.objects.filter(code='F99a').first()
|
||||
|
||||
max_panel_num = 0
|
||||
for sp in SignPanel.objects.all().values_list('code', flat=True):
|
||||
if sp and sp.startswith('CSE-PAN-'):
|
||||
try:
|
||||
num = int(sp.split('-')[-1])
|
||||
if num > max_panel_num:
|
||||
max_panel_num = num
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
panels_created = 0
|
||||
for s in osm_signs:
|
||||
tags = s.get('tags', {})
|
||||
lat = s.get('lat') or (s.get('center') and s['center'].get('lat'))
|
||||
lon = s.get('lon') or (s.get('center') and s['center'].get('lon'))
|
||||
if lat is None or lon is None:
|
||||
continue
|
||||
|
||||
pt_4326 = Point(lon, lat, srid=4326)
|
||||
pt_3812 = pt_4326.transform(3812, clone=True)
|
||||
|
||||
# Determine SignPanelType
|
||||
sig = tags.get('traffic_sign') or ''
|
||||
hw = tags.get('highway') or ''
|
||||
panel_type = None
|
||||
|
||||
if 'B1' in sig or hw == 'give_way':
|
||||
panel_type = type_b1
|
||||
elif 'B5' in sig or hw == 'stop':
|
||||
panel_type = type_b5
|
||||
elif 'F99a' in sig:
|
||||
panel_type = type_f99a
|
||||
else:
|
||||
# Try to extract code from string like "BE:B1", "BE:C43"
|
||||
clean_code = sig.replace('BE:', '').split(';')[0].strip()
|
||||
panel_type = SignPanelType.objects.filter(code__iexact=clean_code).first()
|
||||
|
||||
if not panel_type:
|
||||
panel_type = type_b1 # default fallback
|
||||
|
||||
# Find nearest pole within 30 meters
|
||||
nearest_pole = None
|
||||
min_dist = float('inf')
|
||||
for p in all_poles:
|
||||
d = p.geom.distance(pt_3812)
|
||||
if d < min_dist:
|
||||
min_dist = d
|
||||
nearest_pole = p
|
||||
|
||||
if not nearest_pole or min_dist > 35.0:
|
||||
# If no pole close enough, attach to closest pole anyway or skip
|
||||
if not nearest_pole:
|
||||
continue
|
||||
|
||||
max_panel_num += 1
|
||||
pcode = f"CSE-PAN-{max_panel_num:04d}"
|
||||
pname = f"Panneau {panel_type.code} - {nearest_pole.street.name_fr if nearest_pole.street else 'Rue'}"
|
||||
|
||||
panel = SignPanel(
|
||||
code=pcode,
|
||||
name_fr=pname,
|
||||
pole=nearest_pole,
|
||||
street=nearest_pole.street,
|
||||
signpanel_type=panel_type,
|
||||
category=panel_category,
|
||||
|
||||
status='active',
|
||||
orientation=nearest_pole.orientation or 90,
|
||||
support_type='pole',
|
||||
geom=nearest_pole.geom,
|
||||
)
|
||||
panel.sync_geom_and_geojson()
|
||||
panel.save()
|
||||
panels_created += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Created {panels_created} SignPanels attached to poles. Total panels: {SignPanel.objects.count()}"))
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
import os
|
||||
import csv
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from assets.models import Structure, StructureExceptionalTransport, StructureExceptionalTransportNotice
|
||||
|
||||
|
||||
def parse_float_safe(val):
|
||||
if val is None or val == "":
|
||||
return None
|
||||
try:
|
||||
f = float(str(val).replace(",", ".").strip())
|
||||
return round(f, 2)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_int_safe(val):
|
||||
if val is None or val == "":
|
||||
return None
|
||||
try:
|
||||
return int(float(str(val).replace(",", ".").strip()))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_height(val):
|
||||
if val is None or val == "" or val == 0:
|
||||
return None
|
||||
try:
|
||||
f = float(str(val).replace(",", ".").strip())
|
||||
if f <= 0 or f >= 999:
|
||||
return None
|
||||
if f > 100: # e.g. 443 cm -> 4.43 m
|
||||
return round(f / 100.0, 2)
|
||||
if f > 15: # e.g. 45 dm -> 4.50 m
|
||||
return round(f / 10.0, 2)
|
||||
return round(f, 2)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Importe les données de transport exceptionnel (FOD/SPF) sur les ouvrages d'art correspondants."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--file",
|
||||
type=str,
|
||||
default="/home/kdt/Downloads/matching_structures_transports_exceptionnels.csv",
|
||||
help="Chemin vers le fichier CSV de matching."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--confidence",
|
||||
type=str,
|
||||
default="HAUTE",
|
||||
choices=["HAUTE", "MOYENNE", "TOUS"],
|
||||
help="Niveau de confiance à importer (défaut: HAUTE)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Exécute sans enregistrer les modifications en base de données."
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
csv_file = options["file"]
|
||||
confidence_filter = options["confidence"]
|
||||
dry_run = options["dry_run"]
|
||||
|
||||
if not os.path.exists(csv_file):
|
||||
self.stderr.write(self.style.ERROR(f"Fichier introuvable : {csv_file}"))
|
||||
return
|
||||
|
||||
self.stdout.write(self.style.NOTICE(f"=== Import Transport Exceptionnel FOD ==="))
|
||||
self.stdout.write(f"Source : {csv_file}")
|
||||
self.stdout.write(f"Filtre de confiance : {confidence_filter}")
|
||||
self.stdout.write(f"Mode simulation (dry-run) : {dry_run}\n")
|
||||
|
||||
with open(csv_file, mode="r", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f, delimiter=";")
|
||||
rows = list(reader)
|
||||
|
||||
# Filtrer par niveau de confiance
|
||||
if confidence_filter == "HAUTE":
|
||||
target_rows = [r for r in rows if r.get("confidence_level") == "HAUTE"]
|
||||
elif confidence_filter == "MOYENNE":
|
||||
target_rows = [r for r in rows if r.get("confidence_level") in ("HAUTE", "MOYENNE")]
|
||||
else:
|
||||
target_rows = [r for r in rows if r.get("matched_structure_code")]
|
||||
|
||||
self.stdout.write(f"Total de lignes éligibles : {len(target_rows)}")
|
||||
|
||||
seen_structures = set()
|
||||
to_process = []
|
||||
duplicates_skipped = 0
|
||||
|
||||
for r in target_rows:
|
||||
code = (r.get("user_validation_choice") or r.get("matched_structure_code") or "").strip()
|
||||
if not code:
|
||||
continue
|
||||
if code in seen_structures:
|
||||
duplicates_skipped += 1
|
||||
continue
|
||||
seen_structures.add(code)
|
||||
to_process.append(r)
|
||||
|
||||
self.stdout.write(f"Ouvrages cibles uniques à mettre à jour : {len(to_process)}")
|
||||
self.stdout.write(f"Lignes doublons ignorées (première conservée) : {duplicates_skipped}\n")
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
skipped_count = 0
|
||||
notices_created_count = 0
|
||||
|
||||
with transaction.atomic():
|
||||
for r in to_process:
|
||||
code = (r.get("user_validation_choice") or r.get("matched_structure_code") or "").strip()
|
||||
structure = Structure.objects.filter(code__iexact=code).first()
|
||||
if not structure:
|
||||
self.stdout.write(self.style.WARNING(f" [IGNORE] Structure avec code '{code}' introuvable en DB."))
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
fod_vnr = parse_int_safe(r.get("fod_vnr"))
|
||||
fod_idn = (r.get("fod_idn") or "").strip() or None
|
||||
bridge_class = (r.get("fod_bridge_class") or "").strip() or None
|
||||
maxwa = parse_float_safe(r.get("fod_maxwa_tonnes"))
|
||||
minwr = parse_float_safe(r.get("fod_minwr_tonnes"))
|
||||
mx = parse_float_safe(r.get("fod_mx_tonnes"))
|
||||
usable_width = parse_float_safe(r.get("fod_usable_width_m"))
|
||||
|
||||
# Spoorlijn
|
||||
spoorlijn_raw = str(r.get("fod_spoorlijn") or "").strip().lower()
|
||||
is_railway_metro = spoorlijn_raw in ("true", "1", "t", "oui", "yes")
|
||||
|
||||
# Hauteurs
|
||||
h_str = r.get("fod_clearance_heights") or ""
|
||||
h1_m, h2_m, h3_m = None, None, None
|
||||
if h_str and "/" in h_str:
|
||||
parts = h_str.split("/")
|
||||
if len(parts) >= 3:
|
||||
h1_m = parse_height(parts[0])
|
||||
h2_m = parse_height(parts[1])
|
||||
h3_m = parse_height(parts[2])
|
||||
|
||||
# Prescriptions textuelles
|
||||
p1 = (r.get("fod_prescriptions_1") or "").strip()
|
||||
p2 = (r.get("fod_prescriptions_2") or "").strip()
|
||||
p3 = (r.get("fod_prescriptions_3") or "").strip()
|
||||
|
||||
prescriptions_fr_list = []
|
||||
prescriptions_nl_list = []
|
||||
|
||||
for p in (p1, p2, p3):
|
||||
if not p or len(p) < 3:
|
||||
continue
|
||||
p_lower = p.lower()
|
||||
if any(w in p_lower for w in ("passage", "autorisé", "condition", "vitesse", "limitée", "freinage", "chaussée", "ouvrage", "surplombe")):
|
||||
if p not in prescriptions_fr_list:
|
||||
prescriptions_fr_list.append(p)
|
||||
if any(w in p_lower for w in ("toegestaan", "voorwaarde", "snelheid", "beperkt", "metrotunnel", "overbelastingen", "structuur", "rijbaan")):
|
||||
if p not in prescriptions_nl_list:
|
||||
prescriptions_nl_list.append(p)
|
||||
|
||||
# Si W14 était en FR et non classé, fallback
|
||||
if p1 and not prescriptions_fr_list and not prescriptions_nl_list:
|
||||
prescriptions_fr_list.append(p1)
|
||||
|
||||
presc_fr = "\n\n".join(prescriptions_fr_list) if prescriptions_fr_list else None
|
||||
presc_nl = "\n\n".join(prescriptions_nl_list) if prescriptions_nl_list else None
|
||||
|
||||
# Détection des règles d'exploitation
|
||||
all_text = f"{p1} {p2} {p3}".lower()
|
||||
is_braking_forbidden = any(w in all_text for w in ("frein", "remmen", "sans freinage", "niet remmen"))
|
||||
is_isolated_crossing = any(w in all_text for w in ("convoi seul", "seul", "alleen", "geen andere gelijktijdige", "pas simultanément d'autres surcharges", "pas d'autre surcharge"))
|
||||
has_speed_5 = any(w in all_text for w in ("5km/h", "5 km/h", "5km/u", "5 km/u"))
|
||||
max_speed = 5 if has_speed_5 else None
|
||||
|
||||
# Statut d'admissibilité
|
||||
if presc_fr or presc_nl or is_braking_forbidden or is_isolated_crossing or max_speed:
|
||||
passage_status = "conditional"
|
||||
elif maxwa or mx or minwr:
|
||||
passage_status = "authorized"
|
||||
else:
|
||||
passage_status = "not_evaluated"
|
||||
|
||||
# Détection référence d'étude
|
||||
last_study_ref = None
|
||||
study_match = re.search(r"(avis\s*DITP[_\s\w\d]*|Mission\s*\d+|note\s*de\s*calcul[_\s\w\d]*)", all_text, re.IGNORECASE)
|
||||
if study_match:
|
||||
last_study_ref = study_match.group(0).strip()
|
||||
|
||||
te_obj, created = StructureExceptionalTransport.objects.get_or_create(structure=structure)
|
||||
|
||||
te_obj.fod_vnr = fod_vnr
|
||||
te_obj.national_id = fod_idn
|
||||
if bridge_class:
|
||||
te_obj.bridge_class = bridge_class
|
||||
if maxwa is not None:
|
||||
te_obj.max_authorized_weight_tonnes = maxwa
|
||||
if minwr is not None:
|
||||
te_obj.min_guaranteed_capacity_tonnes = minwr
|
||||
if mx is not None:
|
||||
te_obj.max_calculated_weight_tonnes = mx
|
||||
if usable_width is not None:
|
||||
te_obj.usable_width_m = usable_width
|
||||
if h1_m is not None:
|
||||
te_obj.clearance_height_left_m = h1_m
|
||||
if h2_m is not None:
|
||||
te_obj.clearance_height_center_m = h2_m
|
||||
if h3_m is not None:
|
||||
te_obj.clearance_height_right_m = h3_m
|
||||
if is_railway_metro:
|
||||
te_obj.is_railway_metro_crossing = True
|
||||
if is_braking_forbidden:
|
||||
te_obj.is_braking_forbidden = True
|
||||
if is_isolated_crossing:
|
||||
te_obj.is_isolated_crossing_only = True
|
||||
if max_speed:
|
||||
te_obj.max_speed_kmh = max_speed
|
||||
if passage_status:
|
||||
te_obj.passage_status = passage_status
|
||||
if presc_fr:
|
||||
te_obj.prescriptions_fr = presc_fr
|
||||
if presc_nl:
|
||||
te_obj.prescriptions_nl = presc_nl
|
||||
if last_study_ref:
|
||||
te_obj.last_study_ref = last_study_ref
|
||||
|
||||
if not dry_run:
|
||||
te_obj.save()
|
||||
|
||||
if created:
|
||||
created_count += 1
|
||||
else:
|
||||
updated_count += 1
|
||||
|
||||
self.stdout.write(
|
||||
f" [OK] {structure.code} ({structure.get_name()}) : VNR={fod_vnr}, BKL={bridge_class or '-'}, MAXWA={maxwa or '-'}t, Statut={passage_status}"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
transaction.set_rollback(True)
|
||||
|
||||
self.stdout.write("\n" + self.style.SUCCESS("=== Résultat de l'import ==="))
|
||||
self.stdout.write(f"Ouvrages créés / initialisés : {created_count}")
|
||||
self.stdout.write(f"Ouvrages mis à jour : {updated_count}")
|
||||
self.stdout.write(f"Ouvrages ignorés : {skipped_count}")
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING("Mode simulation : aucune modification n'a été enregistrée en base."))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS("Toutes les données ont été enregistrées avec succès en base de données."))
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from assets.management.commands.import_fulcrum_trees import Command as BaseFulcrumCommand
|
||||
|
||||
|
||||
class Command(BaseFulcrumCommand):
|
||||
help = "Importe l'inventaire Fulcrum des arbres régionaux (arbres, commandes/gestion, interventions)."
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
"""
|
||||
Management command to load baseline asset categories (from loko-bm) and
|
||||
associate existing asset models and assets with their corresponding categories.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
from common.models import Thematic
|
||||
from assets.models import (
|
||||
AssetCategory,
|
||||
CleanLitterBin, CleanLitterBinModel,
|
||||
CleanGlassContainer, CleanGlassContainerModel,
|
||||
RoadDrain, RoadDrainModel,
|
||||
Roadway, RoadwayModel,
|
||||
RoadSidewalk, RoadSidewalkModel,
|
||||
RoadFurniture, RoadFurnitureModel,
|
||||
StructureAssetModel,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Charge les catégories d'assets de base (définies dans loko-bm) et lie les modèles existants."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
type=str,
|
||||
help="Chemin vers le fichier JSON des catégories (défaut: common/fixtures/asset_categories.json)",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# 1. Localiser le fichier JSON
|
||||
json_path = options.get('file')
|
||||
candidates = []
|
||||
if not json_path:
|
||||
base_dir = getattr(settings, 'BASE_DIR', None)
|
||||
candidates = [
|
||||
os.path.join(base_dir, 'common', 'fixtures', 'asset_categories.json') if base_dir else None,
|
||||
os.path.join(base_dir, 'loko', 'common', 'fixtures', 'asset_categories.json') if base_dir else None,
|
||||
'/app/common/fixtures/asset_categories.json',
|
||||
'/app/loko/common/fixtures/asset_categories.json',
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'common', 'fixtures', 'asset_categories.json'),
|
||||
]
|
||||
for c in candidates:
|
||||
if c and os.path.exists(c):
|
||||
json_path = os.path.abspath(c)
|
||||
break
|
||||
|
||||
if not json_path or not os.path.exists(json_path):
|
||||
self.stderr.write(self.style.ERROR(f"Fichier asset_categories.json introuvable. Candidats testés: {candidates}"))
|
||||
return
|
||||
|
||||
self.stdout.write(f"Chargement des catégories depuis : {json_path}")
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
# Cache thématiques
|
||||
thematics_by_code = {t.code: t for t in Thematic.objects.all()}
|
||||
|
||||
# 2. Création / Mise à jour des catégories
|
||||
for item in data:
|
||||
code = item['code']
|
||||
name_fr = item['name_fr']
|
||||
name_nl = item.get('name_nl') or name_fr
|
||||
thematic_code = item.get('thematic_code')
|
||||
thematic = thematics_by_code.get(thematic_code)
|
||||
|
||||
cat, created = AssetCategory.objects.update_or_create(
|
||||
code=code,
|
||||
defaults={
|
||||
'name_fr': name_fr,
|
||||
'name_nl': name_nl,
|
||||
'thematic': thematic,
|
||||
}
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
else:
|
||||
updated_count += 1
|
||||
|
||||
# Modèles Django autorisés (ContentType)
|
||||
allowed_models = item.get('allowed_models', [])
|
||||
if allowed_models:
|
||||
cts = ContentType.objects.filter(model__in=allowed_models)
|
||||
cat.allowed_models.set(cts)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"✅ Catégories d'assets : {created_count} créées, {updated_count} mises à jour (Total: {len(data)})."
|
||||
))
|
||||
|
||||
# 3. Association automatique des modèles d'assets existants aux catégories correspondantes
|
||||
self.link_models_to_categories()
|
||||
|
||||
def link_models_to_categories(self):
|
||||
links = [
|
||||
('CleanLitterBinModel', CleanLitterBinModel, 'CL_BASKET'),
|
||||
('CleanLitterBin', CleanLitterBin, 'CL_BASKET'),
|
||||
('CleanGlassContainerModel', CleanGlassContainerModel, 'CL_BUBBLE'),
|
||||
('CleanGlassContainer', CleanGlassContainer, 'CL_BUBBLE'),
|
||||
('RoadDrainModel', RoadDrainModel, 'RD_DRAIN'),
|
||||
('RoadDrain', RoadDrain, 'RD_DRAIN'),
|
||||
('RoadwayModel', RoadwayModel, 'RD_ROADWAY'),
|
||||
('Roadway', Roadway, 'RD_ROADWAY'),
|
||||
('RoadSidewalkModel', RoadSidewalkModel, 'RD_SIDEWALK'),
|
||||
('RoadSidewalk', RoadSidewalk, 'RD_SIDEWALK'),
|
||||
('RoadFurnitureModel', RoadFurnitureModel, 'RD_STREET_FURNITURE'),
|
||||
('RoadFurniture', RoadFurniture, 'RD_STREET_FURNITURE'),
|
||||
('StructureAssetModel', StructureAssetModel, 'TU_CIVIL_ENGINEERING'),
|
||||
]
|
||||
|
||||
for name, model_cls, cat_code in links:
|
||||
cat = AssetCategory.objects.filter(code=cat_code).first()
|
||||
if not cat:
|
||||
continue
|
||||
try:
|
||||
updated = model_cls.objects.filter(category__isnull=True).update(category=cat)
|
||||
if updated:
|
||||
self.stdout.write(f" • {name} : {updated} enregistrement(s) lié(s) à la catégorie {cat.name_fr} ({cat_code}).")
|
||||
except Exception as e:
|
||||
self.stderr.write(f" ⚠️ Erreur liaison {name} -> {cat_code}: {e}")
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.apps import apps
|
||||
from django.db import connection, transaction
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Synchronizes the geojson field (and lon/lat if missing) from geom for all assets/models using PostGIS SQL."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Perform a dry run without modifying the database.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--only-missing',
|
||||
action='store_true',
|
||||
help='Only update records where geojson is null or empty.',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options['dry_run']
|
||||
only_missing = options['only_missing']
|
||||
|
||||
total_scanned = 0
|
||||
total_updated = 0
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
for model in apps.get_models():
|
||||
fields = [f.name for f in model._meta.get_fields()]
|
||||
if 'geom' not in fields or 'geojson' not in fields:
|
||||
continue
|
||||
|
||||
table = model._meta.db_table
|
||||
is_json_field = model._meta.get_field('geojson').get_internal_type() == 'JSONField'
|
||||
has_lon = 'lon' in fields
|
||||
has_lat = 'lat' in fields
|
||||
|
||||
# Count total candidates with non-null geom
|
||||
cursor.execute(f'SELECT COUNT(*) FROM "{table}" WHERE geom IS NOT NULL;')
|
||||
count = cursor.fetchone()[0]
|
||||
if count == 0:
|
||||
continue
|
||||
|
||||
self.stdout.write(f"Processing {model._meta.app_label}.{model.__name__} ({table}, {count} candidate records)...")
|
||||
|
||||
if only_missing:
|
||||
where_clause = "geom IS NOT NULL AND (geojson IS NULL OR geojson::text = '' OR geojson::text = 'null')"
|
||||
else:
|
||||
where_clause = "geom IS NOT NULL"
|
||||
|
||||
cursor.execute(f'SELECT COUNT(*) FROM "{table}" WHERE {where_clause};')
|
||||
records_to_update = cursor.fetchone()[0]
|
||||
|
||||
if records_to_update == 0:
|
||||
self.stdout.write(self.style.SUCCESS(f" {model.__name__}: 0 records need update."))
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.SUCCESS(f" [DRY-RUN] {model.__name__}: Would update {records_to_update}/{count} records."))
|
||||
total_scanned += count
|
||||
total_updated += records_to_update
|
||||
continue
|
||||
|
||||
cast_geojson = "ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb" if is_json_field else "ST_AsGeoJSON(ST_Transform(geom, 4326))"
|
||||
|
||||
set_clauses = [f"geojson = {cast_geojson}"]
|
||||
if has_lon:
|
||||
set_clauses.append("lon = COALESCE(lon, ST_X(ST_PointOnSurface(ST_Transform(geom, 4326))))")
|
||||
if has_lat:
|
||||
set_clauses.append("lat = COALESCE(lat, ST_Y(ST_PointOnSurface(ST_Transform(geom, 4326))))")
|
||||
|
||||
set_str = ", ".join(set_clauses)
|
||||
update_sql = f'UPDATE "{table}" SET {set_str} WHERE {where_clause};'
|
||||
|
||||
with transaction.atomic():
|
||||
cursor.execute(update_sql)
|
||||
updated_count = cursor.rowcount
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f" {model.__name__}: {updated_count}/{count} records updated in DB."))
|
||||
total_scanned += count
|
||||
total_updated += updated_count
|
||||
|
||||
prefix = "[DRY-RUN] Would update" if dry_run else "Successfully updated"
|
||||
self.stdout.write(self.style.SUCCESS(f"\n{prefix} {total_updated} / {total_scanned} records across all models."))
|
||||
|
|
@ -1,379 +0,0 @@
|
|||
import logging
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from assets.models.trafficlights import (
|
||||
TrafficLightLantern,
|
||||
TrafficLightAccessory,
|
||||
TrafficLightDetector,
|
||||
TrafficLightControllerHardware,
|
||||
TrafficLightNetworkHardware,
|
||||
TrafficLightVoltageHardware,
|
||||
TrafficLightElectricalCabinetContent,
|
||||
)
|
||||
from assets.models.sign import SignPanel
|
||||
from assets.models.publiclighting import PublicLightingLightFixture
|
||||
from assets.models.its import ITSGeoAsset
|
||||
from assets.models.structures import StructureGeoAsset
|
||||
from assets.models.buildings import BuildingAsset
|
||||
from assets.models.outdoor import OutdoorAsset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Synchronizes coordinates (geom, lon, lat, geojson) and locations from parent supports to child assets."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--batch-size',
|
||||
type=int,
|
||||
default=1000,
|
||||
help="Batch size for processing records."
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
batch_size = options['batch_size']
|
||||
self.stdout.write(self.style.NOTICE("Starting parent-child asset data synchronization..."))
|
||||
|
||||
# 1. Traffic Light Lanterns
|
||||
self.stdout.write("1. Syncing Traffic Light Lanterns...")
|
||||
lanterns = TrafficLightLantern.objects.select_related('pole').filter(pole__geom__isnull=False)
|
||||
count = 0
|
||||
to_update = []
|
||||
for lantern in lanterns.iterator(chunk_size=batch_size):
|
||||
if lantern.pole and lantern.pole.geom:
|
||||
if lantern.geom != lantern.pole.geom or lantern.lon != lantern.pole.lon or lantern.lat != lantern.pole.lat:
|
||||
lantern.geom = lantern.pole.geom
|
||||
lantern.lon = lantern.pole.lon
|
||||
lantern.lat = lantern.pole.lat
|
||||
lantern.geojson = lantern.pole.geojson
|
||||
to_update.append(lantern)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
TrafficLightLantern.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
TrafficLightLantern.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Lanterns"))
|
||||
|
||||
# 2. Traffic Light Accessories
|
||||
self.stdout.write("2. Syncing Traffic Light Accessories...")
|
||||
accessories = TrafficLightAccessory.objects.select_related('pole', 'intersection').filter(pole__geom__isnull=False)
|
||||
count = 0
|
||||
to_update = []
|
||||
for acc in accessories.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
if acc.pole and acc.pole.geom:
|
||||
if acc.geom != acc.pole.geom or acc.lon != acc.pole.lon or acc.lat != acc.pole.lat:
|
||||
acc.geom = acc.pole.geom
|
||||
acc.lon = acc.pole.lon
|
||||
acc.lat = acc.pole.lat
|
||||
acc.geojson = acc.pole.geojson
|
||||
changed = True
|
||||
if acc.pole and acc.pole.intersection and not acc.intersection_id:
|
||||
acc.intersection = acc.pole.intersection
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(acc)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
TrafficLightAccessory.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
TrafficLightAccessory.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Accessories"))
|
||||
|
||||
# 3. Traffic Light Detectors
|
||||
self.stdout.write("3. Syncing Traffic Light Detectors...")
|
||||
detectors = TrafficLightDetector.objects.select_related('pole', 'controller')
|
||||
count = 0
|
||||
to_update = []
|
||||
for det in detectors.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
support = det.pole or det.controller
|
||||
if support and getattr(support, 'geom', None):
|
||||
if det.geom != support.geom or det.lon != support.lon or det.lat != support.lat:
|
||||
det.geom = support.geom
|
||||
det.lon = support.lon
|
||||
det.lat = support.lat
|
||||
det.geojson = support.geojson
|
||||
changed = True
|
||||
if det.pole and det.pole.intersection and not det.intersection_id:
|
||||
det.intersection = det.pole.intersection
|
||||
changed = True
|
||||
elif det.controller and det.controller.intersection and not det.intersection_id:
|
||||
det.intersection = det.controller.intersection
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(det)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
TrafficLightDetector.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
TrafficLightDetector.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Detectors"))
|
||||
|
||||
# 4. Traffic Light Controller Hardware
|
||||
self.stdout.write("4. Syncing Traffic Light Controller Hardware...")
|
||||
hws = TrafficLightControllerHardware.objects.select_related('controller').filter(controller__geom__isnull=False)
|
||||
count = 0
|
||||
to_update = []
|
||||
for hw in hws.iterator(chunk_size=batch_size):
|
||||
if hw.controller and hw.controller.geom:
|
||||
if hw.geom != hw.controller.geom or hw.lon != hw.controller.lon or hw.lat != hw.controller.lat:
|
||||
hw.geom = hw.controller.geom
|
||||
hw.lon = hw.controller.lon
|
||||
hw.lat = hw.controller.lat
|
||||
hw.geojson = hw.controller.geojson
|
||||
to_update.append(hw)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
TrafficLightControllerHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
TrafficLightControllerHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Controller Hardwares"))
|
||||
|
||||
# 5. Traffic Light Network Hardware
|
||||
self.stdout.write("5. Syncing Traffic Light Network Hardware...")
|
||||
nets = TrafficLightNetworkHardware.objects.select_related('controller').filter(controller__geom__isnull=False)
|
||||
count = 0
|
||||
to_update = []
|
||||
for net in nets.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
if net.controller and net.controller.geom:
|
||||
if net.geom != net.controller.geom or net.lon != net.controller.lon or net.lat != net.controller.lat:
|
||||
net.geom = net.controller.geom
|
||||
net.lon = net.controller.lon
|
||||
net.lat = net.controller.lat
|
||||
net.geojson = net.controller.geojson
|
||||
changed = True
|
||||
if net.controller and net.controller.intersection and not net.intersection_id:
|
||||
net.intersection = net.controller.intersection
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(net)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
TrafficLightNetworkHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
TrafficLightNetworkHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Network Hardwares"))
|
||||
|
||||
# 6. Traffic Light Voltage Hardware
|
||||
self.stdout.write("6. Syncing Traffic Light Voltage Hardware...")
|
||||
volts = TrafficLightVoltageHardware.objects.select_related('controller').filter(controller__geom__isnull=False)
|
||||
count = 0
|
||||
to_update = []
|
||||
for volt in volts.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
if volt.controller and volt.controller.geom:
|
||||
if volt.geom != volt.controller.geom or volt.lon != volt.controller.lon or volt.lat != volt.controller.lat:
|
||||
volt.geom = volt.controller.geom
|
||||
volt.lon = volt.controller.lon
|
||||
volt.lat = volt.controller.lat
|
||||
volt.geojson = volt.controller.geojson
|
||||
changed = True
|
||||
if volt.controller and volt.controller.intersection and not volt.intersection_id:
|
||||
volt.intersection = volt.controller.intersection
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(volt)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
TrafficLightVoltageHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
TrafficLightVoltageHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Voltage Hardwares"))
|
||||
|
||||
# 7. Traffic Light Electrical Cabinet Contents
|
||||
self.stdout.write("7. Syncing Traffic Light Electrical Cabinet Contents...")
|
||||
contents = TrafficLightElectricalCabinetContent.objects.select_related('cabinet').filter(cabinet__geom__isnull=False)
|
||||
count = 0
|
||||
to_update = []
|
||||
for content in contents.iterator(chunk_size=batch_size):
|
||||
if content.cabinet and content.cabinet.geom:
|
||||
if content.geom != content.cabinet.geom or content.lon != content.cabinet.lon or content.lat != content.cabinet.lat:
|
||||
content.geom = content.cabinet.geom
|
||||
content.lon = content.cabinet.lon
|
||||
content.lat = content.cabinet.lat
|
||||
content.geojson = content.cabinet.geojson
|
||||
to_update.append(content)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
TrafficLightElectricalCabinetContent.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
TrafficLightElectricalCabinetContent.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Electrical Cabinet Contents"))
|
||||
|
||||
# 8. Road Sign Panels
|
||||
self.stdout.write("8. Syncing Road Sign Panels...")
|
||||
panels = SignPanel.objects.select_related('pole', 'trafficlight_pole', 'publiclighting_pole')
|
||||
count = 0
|
||||
to_update = []
|
||||
for panel in panels.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
support = panel.pole or panel.trafficlight_pole or panel.publiclighting_pole
|
||||
if support and getattr(support, 'geom', None):
|
||||
if panel.geom != support.geom or panel.lon != support.lon or panel.lat != support.lat:
|
||||
panel.geom = support.geom
|
||||
panel.lon = support.lon
|
||||
panel.lat = support.lat
|
||||
panel.geojson = support.geojson
|
||||
changed = True
|
||||
if panel.pole and panel.pole.street and not panel.street_id:
|
||||
panel.street = panel.pole.street
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(panel)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
SignPanel.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
SignPanel.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Road Sign Panels"))
|
||||
|
||||
# 9. Public Lighting Light Fixtures (Luminaires)
|
||||
self.stdout.write("9. Syncing Public Lighting Light Fixtures...")
|
||||
fixtures = PublicLightingLightFixture.objects.select_related('pole', 'traffic_light_pole')
|
||||
count = 0
|
||||
to_update = []
|
||||
for fix in fixtures.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
support = fix.pole or fix.traffic_light_pole
|
||||
if support and getattr(support, 'geom', None):
|
||||
if fix.geom != support.geom or fix.lon != support.lon or fix.lat != support.lat:
|
||||
fix.geom = support.geom
|
||||
fix.lon = support.lon
|
||||
fix.lat = support.lat
|
||||
fix.geojson = support.geojson
|
||||
changed = True
|
||||
if fix.pole and fix.pole.street and not fix.street_id:
|
||||
fix.street = fix.pole.street
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(fix)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
PublicLightingLightFixture.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
PublicLightingLightFixture.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Public Lighting Light Fixtures"))
|
||||
|
||||
# 10. ITS Assets
|
||||
self.stdout.write("10. Syncing ITS Assets...")
|
||||
its_assets = ITSGeoAsset.objects.select_related('trafficlights_pole', 'lighting_pole')
|
||||
count = 0
|
||||
to_update = []
|
||||
for its in its_assets.iterator(chunk_size=batch_size):
|
||||
support = its.trafficlights_pole or its.lighting_pole
|
||||
if support and getattr(support, 'geom', None):
|
||||
if its.geom != support.geom or its.lon != support.lon or its.lat != support.lat:
|
||||
its.geom = support.geom
|
||||
its.lon = support.lon
|
||||
its.lat = support.lat
|
||||
its.geojson = support.geojson
|
||||
to_update.append(its)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
ITSGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
ITSGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} ITS Assets"))
|
||||
|
||||
# 11. Structure Assets
|
||||
self.stdout.write("11. Syncing Structure Assets...")
|
||||
struct_assets = StructureGeoAsset.objects.select_related('parent_asset')
|
||||
count = 0
|
||||
to_update = []
|
||||
for sa in struct_assets.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
if sa.parent_asset and getattr(sa.parent_asset, 'geom', None) and not sa.geom:
|
||||
sa.geom = sa.parent_asset.geom
|
||||
sa.lon = sa.parent_asset.lon
|
||||
sa.lat = sa.parent_asset.lat
|
||||
sa.geojson = sa.parent_asset.geojson
|
||||
changed = True
|
||||
if sa.parent_asset and sa.parent_asset.location and not sa.location_id:
|
||||
sa.location = sa.parent_asset.location
|
||||
changed = True
|
||||
if sa.parent_asset and sa.parent_asset.structure and not sa.structure_id:
|
||||
sa.structure = sa.parent_asset.structure
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(sa)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
StructureGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'structure'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
StructureGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'structure'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Structure Assets"))
|
||||
|
||||
# 12. Building Assets
|
||||
self.stdout.write("12. Syncing Building Assets...")
|
||||
bld_assets = BuildingAsset.objects.select_related('parent_asset')
|
||||
count = 0
|
||||
to_update = []
|
||||
for ba in bld_assets.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
if ba.parent_asset and getattr(ba.parent_asset, 'geom', None) and not ba.geom:
|
||||
ba.geom = ba.parent_asset.geom
|
||||
ba.lon = ba.parent_asset.lon
|
||||
ba.lat = ba.parent_asset.lat
|
||||
ba.geojson = ba.parent_asset.geojson
|
||||
changed = True
|
||||
if ba.parent_asset and ba.parent_asset.location and not ba.location_id:
|
||||
ba.location = ba.parent_asset.location
|
||||
changed = True
|
||||
if ba.parent_asset and ba.parent_asset.building and not ba.building_id:
|
||||
ba.building = ba.parent_asset.building
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(ba)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
BuildingAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'building'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
BuildingAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'building'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Building Assets"))
|
||||
|
||||
# 13. Outdoor Assets
|
||||
self.stdout.write("13. Syncing Outdoor Assets...")
|
||||
out_assets = OutdoorAsset.objects.select_related('parent_asset')
|
||||
count = 0
|
||||
to_update = []
|
||||
for oa in out_assets.iterator(chunk_size=batch_size):
|
||||
changed = False
|
||||
if oa.parent_asset and getattr(oa.parent_asset, 'geom', None) and not oa.geom:
|
||||
oa.geom = oa.parent_asset.geom
|
||||
oa.lon = oa.parent_asset.lon
|
||||
oa.lat = oa.parent_asset.lat
|
||||
oa.geojson = oa.parent_asset.geojson
|
||||
changed = True
|
||||
if oa.parent_asset and oa.parent_asset.location and not oa.location_id:
|
||||
oa.location = oa.parent_asset.location
|
||||
changed = True
|
||||
if oa.parent_asset and oa.parent_asset.site and not oa.site_id:
|
||||
oa.site = oa.parent_asset.site
|
||||
changed = True
|
||||
if changed:
|
||||
to_update.append(oa)
|
||||
count += 1
|
||||
if len(to_update) >= batch_size:
|
||||
OutdoorAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'site'])
|
||||
to_update = []
|
||||
if to_update:
|
||||
OutdoorAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'site'])
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Outdoor Assets"))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("All asset parent data synchronized successfully!"))
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
# 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
# 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
# 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",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 07:54
|
||||
|
||||
import assets.models.core
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0007_outdoorlocation_outdoorasset_outdoorlocationdocument'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AssetInspection',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('asset_object_id', models.PositiveIntegerField(verbose_name="ID de l'asset")),
|
||||
('inspection_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date d'inspection")),
|
||||
('result_status', models.CharField(choices=[('compliant', 'Conforme'), ('non_compliant', 'Non conforme / Anomalie'), ('updated', 'Données corrigées'), ('damaged', 'Endommagé'), ('missing', 'Absent / Retiré')], default='compliant', max_length=50, verbose_name='Résultat du contrôle')),
|
||||
('verified_standard_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux classiques constaté")),
|
||||
('verified_cargo_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux cargo constaté")),
|
||||
('verified_stand_type', models.CharField(blank=True, max_length=50, null=True, verbose_name="Type d'arceau constaté")),
|
||||
('verified_has_scooter_drop_zone', models.BooleanField(blank=True, null=True, verbose_name='Drop zone trottinette constatée')),
|
||||
('verified_marking_quality', models.CharField(blank=True, max_length=50, null=True, verbose_name='Qualité du marquage constatée')),
|
||||
('verified_obsolescence', models.CharField(blank=True, max_length=50, null=True, verbose_name="État d'obsolescence constaté")),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Remarques / Constats')),
|
||||
('apply_changes_to_asset', models.BooleanField(default=True, verbose_name="Appliquer directement les modifications à l'asset")),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('asset_content_type', models.ForeignKey(limit_choices_to={'app_label': 'assets'}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset")),
|
||||
('inspector', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_inspections', to=settings.AUTH_USER_MODEL, verbose_name='Inspecteur / Vérificateur')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Inspection / Vérification d'asset",
|
||||
'verbose_name_plural': "Inspections / Vérifications d'assets",
|
||||
'ordering': ['-inspection_date'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParkingLocation',
|
||||
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')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse')),
|
||||
('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': 'Zone de stationnement',
|
||||
'verbose_name_plural': 'Zones de stationnement',
|
||||
'ordering': ['code', 'name_fr'],
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParkingSpot',
|
||||
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.PolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise polygone')),
|
||||
('spot_type', models.CharField(choices=[('bicycle', 'Stationnement vélo'), ('scooter_drop_zone', 'Drop zone trottinettes / EDPM'), ('cargo_bike', 'Stationnement vélo cargo'), ('delivery', 'Zone de livraison / Chargement-déchargement'), ('pmr', 'Emplacement PMR'), ('kiss_and_ride', 'Arrêt minute / Court-durée (CD)'), ('car_parking', 'Stationnement automobile'), ('carsharing', 'Autopartage'), ('ev_charging', 'Recharge véhicule électrique'), ('other', 'Autre')], default='bicycle', max_length=50, verbose_name="Type d'emplacement")),
|
||||
('standard_stand_count', models.IntegerField(default=0, verbose_name="Nombre d'arceaux vélos classiques")),
|
||||
('cargo_stand_count', models.IntegerField(default=0, verbose_name="Nombre d'arceaux vélos cargo")),
|
||||
('other_stand_count', models.IntegerField(default=0, verbose_name='Autres arceaux / agencements')),
|
||||
('stand_type', models.CharField(choices=[('normal', 'Arceau standard (U inversé)'), ('cargo', 'Arceau vélo cargo'), ('mixed', 'Mixte (normal et cargo)'), ('rack', 'Râteau / Pince-roue'), ('box', 'Box / Abri sécurisé'), ('other', 'Autre')], default='normal', max_length=50, verbose_name="Type d'arceau principal")),
|
||||
('has_scooter_drop_zone', models.BooleanField(default=False, verbose_name='Drop zone trottinettes / EDPM')),
|
||||
('scooter_capacity', models.IntegerField(blank=True, null=True, verbose_name='Capacité estimée trottinettes')),
|
||||
('capacity', models.IntegerField(blank=True, null=True, verbose_name='Capacité générale (nbr véhicules)')),
|
||||
('is_paid', models.BooleanField(default=False, verbose_name='Payant')),
|
||||
('time_limit_minutes', models.IntegerField(blank=True, null=True, verbose_name='Durée maximale (minutes)')),
|
||||
('marking_quality', models.CharField(choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('damaged', 'Dégradé'), ('missing', 'Absent / Effacé')], default='good', max_length=50, verbose_name='Qualité du marquage au sol')),
|
||||
('is_temporary', models.BooleanField(default=False, verbose_name='Temporaire (chantier/événement)')),
|
||||
('is_compliant', models.BooleanField(default=True, verbose_name='Conforme aux spécifications')),
|
||||
('non_compliance_reason', models.TextField(blank=True, null=True, verbose_name='Motif de non-conformité')),
|
||||
('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='parking_spots', to='assets.parkinglocation', verbose_name='Zone / Localisation parente')),
|
||||
('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')),
|
||||
('panels', models.ManyToManyField(blank=True, related_name='parking_spots_attached_new', to='assets.signpanel', verbose_name='Panneaux de signalisation associés')),
|
||||
('poles', models.ManyToManyField(blank=True, related_name='parking_spots_attached_new', to='assets.signpole', verbose_name='Poteaux de signalisation associés')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.parkingspot', verbose_name='Remplacé par')),
|
||||
('street', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parking_spots_new', to='assets.signstreet', verbose_name='Rue')),
|
||||
('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': 'Emplacement de stationnement',
|
||||
'verbose_name_plural': 'Emplacements de stationnement',
|
||||
'ordering': ['code', 'name_fr'],
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 08:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0008_assetinspection_parkinglocation_parkingspot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_has_scooter_drop_zone',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_stand_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_marking_quality',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_cargo_stand_count',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_standard_stand_count',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='assetinspection',
|
||||
options={'ordering': ['-inspection_date'], 'verbose_name': "Inspection d'asset (Générique)", 'verbose_name_plural': "Inspections d'assets (Génériques)"},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artworkfountain',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artworkmonument',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanglasscontainer',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanlitterbin',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='controlcentergeoasset',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itsgeoasset',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturebench',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturegreensurface',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturerwiasset',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='natureshrub',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='outdoorasset',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parkingspot',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingelectricalcabinet',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightinglightfixture',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingpole',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadconcreteblockset',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roaddrain',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadfurniture',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadsidewalk',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadway',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpanel',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signparkingspot',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpole',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='structuregeoasset',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightaccessory',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcable',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontroller',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontrollerhardware',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightdetector',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinet',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinetcontent',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightnetworkhardware',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightpole',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightprogramming',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradar',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradarpole',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightvoltagehardware',
|
||||
name='inspection_status',
|
||||
field=models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection"),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParkingSpotInspection',
|
||||
fields=[
|
||||
('assetinspection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.assetinspection')),
|
||||
('verified_standard_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux classiques constaté")),
|
||||
('verified_cargo_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux cargo constaté")),
|
||||
('verified_stand_type', models.CharField(blank=True, max_length=50, null=True, verbose_name="Type d'arceau constaté")),
|
||||
('verified_has_scooter_drop_zone', models.BooleanField(blank=True, null=True, verbose_name='Drop zone trottinette constatée')),
|
||||
('verified_marking_quality', models.CharField(blank=True, max_length=50, null=True, verbose_name='Qualité du marquage constatée')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Inspection d'emplacement de stationnement",
|
||||
'verbose_name_plural': "Inspections d'emplacements de stationnement",
|
||||
},
|
||||
bases=('assets.assetinspection',),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def create_parking_thematic(apps, schema_editor):
|
||||
Thematic = apps.get_model('common', 'Thematic')
|
||||
AssetCategory = apps.get_model('assets', 'AssetCategory')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
thematic, _created = Thematic.objects.get_or_create(
|
||||
code='parking',
|
||||
defaults={
|
||||
'name_fr': 'Stationnement',
|
||||
'name_nl': 'Parkeren & Mobiliteit',
|
||||
'description': 'Thématique dédiée aux emplacements de stationnement et zones de mobilité (vélos, trottinettes, PMR, livraison)',
|
||||
'icon': 'bi-p-square',
|
||||
'color': '#0d6efd',
|
||||
}
|
||||
)
|
||||
|
||||
category, _cat_created = AssetCategory.objects.get_or_create(
|
||||
code='parking_spot',
|
||||
defaults={
|
||||
'thematic': thematic,
|
||||
'name_fr': 'Emplacements de stationnement',
|
||||
'name_nl': 'Parkeerplaatsen',
|
||||
}
|
||||
)
|
||||
if category.thematic_id != thematic.id:
|
||||
category.thematic = thematic
|
||||
category.save()
|
||||
|
||||
# Link ContentTypes
|
||||
try:
|
||||
spot_ct = ContentType.objects.get(app_label='assets', model='parkingspot')
|
||||
category.allowed_models.add(spot_ct)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
loc_ct = ContentType.objects.get(app_label='assets', model='parkinglocation')
|
||||
category.allowed_models.add(loc_ct)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Assign category to existing ParkingSpot records
|
||||
ParkingSpot = apps.get_model('assets', 'ParkingSpot')
|
||||
ParkingSpot.objects.filter(category__isnull=True).update(category=category)
|
||||
|
||||
# Grant thematic permissions only to admin user configurations
|
||||
UserConfig = apps.get_model('common', 'UserConfig')
|
||||
UserThematics = apps.get_model('common', 'UserThematics')
|
||||
for uc in UserConfig.objects.filter(roles__name='admin').distinct():
|
||||
UserThematics.objects.get_or_create(
|
||||
user_config=uc,
|
||||
thematic=thematic,
|
||||
defaults={'can_view_assets': True, 'can_edit_assets': True}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def remove_parking_thematic(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0009_remove_assetinspection_verified_has_scooter_drop_zone_and_more'),
|
||||
('common', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_parking_thematic, remove_parking_thematic),
|
||||
]
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 09:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0010_add_parking_thematic'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='artworkfountain',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artworkmonument',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanglasscontainer',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanlitterbin',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='controlcentergeoasset',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itsgeoasset',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturebench',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturegreensurface',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturerwiasset',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='natureshrub',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='outdoorasset',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parkingspot',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingelectricalcabinet',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightinglightfixture',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingpole',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadconcreteblockset',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roaddrain',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadfurniture',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadsidewalk',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadway',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpanel',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signparkingspot',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpole',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='structuregeoasset',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightaccessory',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcable',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontroller',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontrollerhardware',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightdetector',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinet',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinetcontent',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightnetworkhardware',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightpole',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightprogramming',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradar',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradarpole',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightvoltagehardware',
|
||||
name='funding_program',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 10:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0011_artworkfountain_funding_program_and_more'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetinspection',
|
||||
name='inspector_role',
|
||||
field=models.CharField(choices=[('observer', 'Observer / Agent de terrain'), ('provider', 'Prestataire technique externe'), ('manager', 'Gestionnaire / Contrôleur interne')], default='observer', max_length=50, verbose_name="Rôle de l'inspecteur"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assetinspection',
|
||||
name='nature',
|
||||
field=models.CharField(choices=[('visual', 'Inspection visuelle'), ('technical', 'Inspection technique'), ('post_intervention', 'Suivi / Qualité post-intervention')], default='visual', max_length=50, verbose_name="Nature de l'inspection"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assetinspection',
|
||||
name='asset_content_type',
|
||||
field=models.ForeignKey(limit_choices_to={'app_label__in': ['assets', 'interventions']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset / Cible"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assetinspection',
|
||||
name='asset_object_id',
|
||||
field=models.PositiveIntegerField(verbose_name='ID de la cible'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 15:40
|
||||
|
||||
import inspections.models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0012_assetinspection_inspector_role_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='parkingspotinspection',
|
||||
name='verified_marking_quality',
|
||||
field=models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('damaged', 'Dégradé'), ('missing', 'Absent / Effacé')], max_length=50, null=True, verbose_name='Qualité du marquage constatée'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InspectionDocument',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(upload_to=inspections.models.inspection_document_upload_path, verbose_name='Fichier / Photo / Document')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('inspection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.assetinspection', verbose_name='Inspection')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Document / Photo d'inspection",
|
||||
'verbose_name_plural': "Documents / Photos d'inspection",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 15:46
|
||||
|
||||
import inspections.models
|
||||
import common.storages
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0013_alter_parkingspotinspection_verified_marking_quality_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='inspectiondocument',
|
||||
name='file',
|
||||
field=models.FileField(storage=common.storages.PrivateMediaStorage(), upload_to=inspections.models.inspection_document_upload_path, verbose_name='Fichier / Photo / Document'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-08 07:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0014_alter_inspectiondocument_file'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='parkingspotinspection',
|
||||
name='apply_physical_counts_to_asset',
|
||||
field=models.BooleanField(default=False, verbose_name="Mettre à jour la capacité / comptages de l'asset"),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-10 08:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0015_parkingspotinspection_apply_physical_counts_to_asset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='asset_content_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='inspector',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inspectiondocument',
|
||||
name='inspection',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parkingspot',
|
||||
name='drop_off_stand_count',
|
||||
field=models.IntegerField(default=0, verbose_name="Nombre d'arceaux drop-off"),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ParkingSpotInspection',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AssetInspection',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='InspectionDocument',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def create_bike_parking_category(apps, schema_editor):
|
||||
Thematic = apps.get_model('common', 'Thematic')
|
||||
AssetCategory = apps.get_model('assets', 'AssetCategory')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
try:
|
||||
thematic = Thematic.objects.get(code='parking')
|
||||
except Thematic.DoesNotExist:
|
||||
# Fallback if thematic doesn't exist yet
|
||||
thematic, _ = Thematic.objects.get_or_create(
|
||||
code='parking',
|
||||
defaults={
|
||||
'name_fr': 'Stationnement',
|
||||
'name_nl': 'Parkeren & Mobiliteit',
|
||||
'description': 'Thématique dédiée aux emplacements de stationnement et zones de mobilité (vélos, trottinettes, PMR, livraison)',
|
||||
'icon': 'bi-p-square',
|
||||
'color': '#0d6efd',
|
||||
}
|
||||
)
|
||||
|
||||
category, _cat_created = AssetCategory.objects.get_or_create(
|
||||
code='bike_parking_spot',
|
||||
defaults={
|
||||
'thematic': thematic,
|
||||
'name_fr': 'Emplacement vélos',
|
||||
'name_nl': 'Fietsenstalling',
|
||||
}
|
||||
)
|
||||
|
||||
# Link ContentTypes
|
||||
try:
|
||||
spot_ct = ContentType.objects.get(app_label='assets', model='parkingspot')
|
||||
category.allowed_models.add(spot_ct)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
loc_ct = ContentType.objects.get(app_label='assets', model='parkinglocation')
|
||||
category.allowed_models.add(loc_ct)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def remove_bike_parking_category(apps, schema_editor):
|
||||
AssetCategory = apps.get_model('assets', 'AssetCategory')
|
||||
AssetCategory.objects.filter(code='bike_parking_spot').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0016_remove_assetinspection_asset_content_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_bike_parking_category, remove_bike_parking_category),
|
||||
]
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-10 09:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0017_add_bike_parking_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='parkingspot',
|
||||
name='scooter_capacity',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-11 12:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_locations(apps, schema_editor):
|
||||
NatureRWIZ = apps.get_model('assets', 'NatureRWIZ')
|
||||
NatureRWIAsset = apps.get_model('assets', 'NatureRWIAsset')
|
||||
|
||||
zones = NatureRWIZ.objects.filter(geom__isnull=False)
|
||||
for zone in zones:
|
||||
NatureRWIAsset.objects.filter(geom__intersects=zone.geom, location__isnull=True).update(location=zone)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0018_remove_parkingspot_scooter_capacity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='naturerwiasset',
|
||||
name='location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.naturerwiz', verbose_name='Zone GIEP / ZIEP'),
|
||||
),
|
||||
migrations.RunPython(populate_locations, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-17 07:23
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0019_naturerwiasset_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='geojson',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='geom',
|
||||
field=django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='lat',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='lon',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
from django.db import migrations
|
||||
from django.core.management import call_command
|
||||
|
||||
|
||||
def sync_parent_data(apps, schema_editor):
|
||||
try:
|
||||
call_command('sync_asset_parent_data')
|
||||
except Exception as e:
|
||||
print(f"Warning during sync_asset_parent_data migration: {e}")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('assets', '0020_trafficlightlantern_geojson_trafficlightlantern_geom_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(sync_parent_data, migrations.RunPython.noop),
|
||||
]
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-19 09:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0021_sync_asset_parent_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='trafficlightprogramming',
|
||||
options={'ordering': ['-commissioning_date'], 'verbose_name': 'Programmation', 'verbose_name_plural': 'Programmations'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trafficlightprogramming',
|
||||
name='index',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Index'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def create_programming_category(apps, schema_editor):
|
||||
Thematic = apps.get_model('common', 'Thematic')
|
||||
AssetCategory = apps.get_model('assets', 'AssetCategory')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
try:
|
||||
thematic = Thematic.objects.get(code='trafficlights')
|
||||
except Thematic.DoesNotExist:
|
||||
thematic, _ = Thematic.objects.get_or_create(
|
||||
code='trafficlights',
|
||||
defaults={
|
||||
'name_fr': 'Feux de signalisation',
|
||||
'name_nl': 'Verkeerslichten',
|
||||
'description': 'Gestion des carrefours à feux et équipements associés',
|
||||
'icon': 'bi-stoplights',
|
||||
'color': '#ffc107',
|
||||
}
|
||||
)
|
||||
|
||||
category, _cat_created = AssetCategory.objects.get_or_create(
|
||||
code='TL_PROGRAMMING',
|
||||
defaults={
|
||||
'thematic': thematic,
|
||||
'name_fr': 'Programmation',
|
||||
'name_nl': 'Programmatie',
|
||||
}
|
||||
)
|
||||
|
||||
# Link ContentType for TrafficLightProgramming
|
||||
try:
|
||||
prog_ct = ContentType.objects.get(app_label='assets', model='trafficlightprogramming')
|
||||
category.allowed_models.add(prog_ct)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def remove_programming_category(apps, schema_editor):
|
||||
AssetCategory = apps.get_model('assets', 'AssetCategory')
|
||||
AssetCategory.objects.filter(code='TL_PROGRAMMING').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0022_alter_trafficlightprogramming_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_programming_category, remove_programming_category),
|
||||
]
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-21 08:20
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0023_add_trafficlight_programming_category'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StructureExceptionalTransport',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('fod_vnr', models.IntegerField(blank=True, help_text='Volgnummer pont dans la base fédérale', null=True, verbose_name="Numéro d'ordre FOD/SPF (VNR)")),
|
||||
('national_id', models.CharField(blank=True, max_length=50, null=True, verbose_name='Identifiant national (IDN)')),
|
||||
('bridge_class', models.CharField(blank=True, help_text='Ex: T180152402024030, D18015..., T09015', max_length=100, null=True, verbose_name='Classe de charge du pont (BKL)')),
|
||||
('is_railway_metro_crossing', models.BooleanField(default=False, help_text="Indique si l'ouvrage surplombe ou supporte des voies ferrées/métro/tram", verbose_name='Ouvrage ferroviaire / métro / tram (Spoorlijn)')),
|
||||
('number_of_spans', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Nombre de travées (OVS)')),
|
||||
('max_authorized_weight_tonnes', models.DecimalField(blank=True, decimal_places=2, help_text='Poids maximal standard sans étude particulière (ex: 44 t)', max_digits=6, null=True, verbose_name='Poids max. autorisé standard (MAXWA) [t]')),
|
||||
('min_guaranteed_capacity_tonnes', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True, verbose_name='Capacité minimale garantie (MINWR) [t]')),
|
||||
('max_calculated_weight_tonnes', models.DecimalField(blank=True, decimal_places=2, help_text="Charge maximale absolue évaluée pour l'ouvrage (ex: 140t, 265t, 450t)", max_digits=6, null=True, verbose_name='Tonnage maximal admissible calculé (MX) [t]')),
|
||||
('max_axle_load_tonnes', models.DecimalField(blank=True, decimal_places=2, help_text='Ex: 12.0, 12.8, 13.0 t/essieu', max_digits=5, null=True, verbose_name='Charge max. par essieu [t/essieu]')),
|
||||
('min_axle_spacing_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='Entraxe minimal entre essieux [m]')),
|
||||
('clearance_height_left_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='Hauteur libre gauche / entrée (H1) [m]')),
|
||||
('clearance_height_center_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name="Hauteur libre dans l'axe (H2) [m]")),
|
||||
('clearance_height_right_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='Hauteur libre droite / sortie (H3) [m]')),
|
||||
('usable_width_m', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='Largeur utile roulable (BRE) [m]')),
|
||||
('passage_status', models.CharField(choices=[('authorized', 'Autorisé sans condition particulière'), ('conditional', 'Autorisé sous conditions / prescriptions'), ('prior_study_required', 'Étude / calcul préalable obligatoire'), ('forbidden', 'Strictement interdit'), ('unknown', 'Non évalué')], default='unknown', max_length=30, verbose_name="Statut d'admissibilité TE")),
|
||||
('max_speed_kmh', models.PositiveIntegerField(blank=True, default=5, help_text='Généralement 5 km/h pour supprimer les effets dynamiques', null=True, verbose_name='Vitesse maximale de franchissement (km/h)')),
|
||||
('is_braking_forbidden', models.BooleanField(default=False, help_text='Notamment sur les dalles de toit de métro', verbose_name="Interdiction formelle de freiner sur l'ouvrage")),
|
||||
('is_isolated_crossing_only', models.BooleanField(default=True, help_text="Aucune autre charge mobile simultanée autorisée sur l'ouvrage", verbose_name="Convoi seul sur l'ouvrage")),
|
||||
('safety_distance_m', models.PositiveIntegerField(blank=True, default=5, help_text='Distance minimale avec tout autre véhicule (souvent 5m)', null=True, verbose_name='Distance de sécurité / zone tampon (m)')),
|
||||
('traffic_interruption_required', models.BooleanField(default=False, verbose_name='Interruption de circulation requise pendant le passage')),
|
||||
('lane_positioning', models.CharField(choices=[('standard', 'Voie de circulation normale'), ('center_axis', "Dans l'axe de l'ouvrage / ligne médiane"), ('first_lane', 'Première bande de circulation'), ('tram_tracks', 'Sur les voies de tram'), ('specific_path', 'Trajectoire spécifique (voir prescriptions)')], default='standard', max_length=30, verbose_name='Positionnement sur la chaussée')),
|
||||
('is_escort_required', models.BooleanField(default=False, verbose_name='Escorte policière ou accompagnement requis')),
|
||||
('prescriptions_fr', models.TextField(blank=True, null=True, verbose_name='Prescriptions techniques (FR)')),
|
||||
('prescriptions_nl', models.TextField(blank=True, null=True, verbose_name='Prescriptions techniques (NL)')),
|
||||
('last_study_date', models.DateField(blank=True, null=True, verbose_name='Date de la dernière étude / note de calcul')),
|
||||
('last_study_ref', models.CharField(blank=True, max_length=150, null=True, verbose_name='Référence de la dernière note de calcul / Mission')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('structure', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='exceptional_transport_info', to='assets.structure', verbose_name='Ouvrage associé')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_structure_te_infos', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Transport exceptionnel (Ouvrage)',
|
||||
'verbose_name_plural': 'Transports exceptionnels (Ouvrages)',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StructureExceptionalTransportNotice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mission_reference', models.CharField(help_text='Ex: Mission 64, Avis DITP 468830', max_length=100, verbose_name='Référence mission / Avis DITP')),
|
||||
('notice_date', models.DateField(blank=True, null=True, verbose_name="Date de l'avis")),
|
||||
('result', models.CharField(choices=[('positive', 'Favorable / Positif'), ('conditional', 'Favorable sous conditions'), ('negative', 'Défavorable / Négatif')], default='positive', max_length=20, verbose_name='Résultat')),
|
||||
('evaluated_total_weight_tonnes', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True, verbose_name='Tonnage évalué [t]')),
|
||||
('evaluated_axle_load_tonnes', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='Charge par essieu évaluée [t/essieu]')),
|
||||
('comments', models.TextField(blank=True, verbose_name="Conditions / Remarques de l'avis")),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
('structure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exceptional_transport_notices', to='assets.structure', verbose_name='Ouvrage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Avis / Mission d'étude TE",
|
||||
'verbose_name_plural': "Avis / Missions d'études TE",
|
||||
'ordering': ['-notice_date', '-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-26 13:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0024_structureexceptionaltransport_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='artworkfountain',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artworkmonument',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanglasscontainer',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanlitterbin',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='controlcentergeoasset',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itsgeoasset',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturebench',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturegreensurface',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturerwiasset',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='natureshrub',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='outdoorasset',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parkingspot',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingelectricalcabinet',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightinglightfixture',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingpole',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadconcreteblockset',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roaddrain',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadfurniture',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadsidewalk',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadway',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpanel',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signparkingspot',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpole',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='structuregeoasset',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightaccessory',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcable',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontroller',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontrollerhardware',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightdetector',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinet',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinetcontent',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightnetworkhardware',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightpole',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightprogramming',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradar',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradarpole',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightvoltagehardware',
|
||||
name='external_reference',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-09-12 12:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0025_artworkfountain_external_reference_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='naturetree',
|
||||
options={'verbose_name': 'Arbre', 'verbose_name_plural': 'Arbres'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='natureassetmodel',
|
||||
name='foliage_type',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Type de feuillage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='natureassetmodel',
|
||||
name='scientific_name',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Nom scientifique'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='circumference',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Circonférence à 1m30 (cm)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='crown_clearance',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Hauteur sous couronne (m)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='crown_diameter',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Diamètre du houppier (m)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='development_stage',
|
||||
field=models.CharField(blank=True, choices=[('young', 'Jeune plant'), ('semi_mature', 'Semi-mature / En croissance'), ('mature', 'Adulte / Mature'), ('senescent', 'Sénescent / Dépérissant'), ('remarkable', 'Vénérable / Remarquable')], default='mature', max_length=50, null=True, verbose_name='Stade de développement'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='health_defects',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Défauts constatés & pathologies'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='implantation_context',
|
||||
field=models.CharField(blank=True, choices=[('street', 'Alignement de voirie'), ('park', 'Parc / Espace vert'), ('square', 'Square / Place publique'), ('roundabout', 'Rond-point / Îlot central'), ('cemetery', 'Cimetière'), ('isolated', 'Isolé / Autre')], default='street', max_length=50, null=True, verbose_name="Contexte d'implantation"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='is_remarkable',
|
||||
field=models.BooleanField(default=False, verbose_name='Arbre remarquable / classé'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='last_phytosanitary_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Dernier diagnostic phytosanitaire'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='last_pruning_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Dernière taille / élagage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='mechanical_state',
|
||||
field=models.CharField(blank=True, choices=[('good', 'Bon (aucun défaut majeur)'), ('fair', 'Moyen (défauts mineurs)'), ('cavity', 'Présence de cavité(s)'), ('crack', 'Fissure(s) / Écorce incluse'), ('dead_branches', 'Branches mortes importantes'), ('critical', 'Critique (risque de rupture)')], default='good', max_length=50, null=True, verbose_name='État mécanique'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='next_inspection_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Prochaine inspection préconisée'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='pit_type',
|
||||
field=models.CharField(blank=True, choices=[('open_ground', 'Pleine terre / Engazonné'), ('vegetated', 'Pied végétalisé / Fleuri'), ('metal_grate', "Grille d'arbre métallique"), ('permeable_paving', 'Revêtement drainant / Pavés poreux'), ('compacted_soil', 'Sol compacté / Bitume')], default='open_ground', max_length=50, null=True, verbose_name='Type de pied / Fosse'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='protection_type',
|
||||
field=models.CharField(blank=True, choices=[('none', 'Aucun (affranchi)'), ('staking', 'Tuteur (simple / bipode / tripode)'), ('corset', 'Corset métallique de protection'), ('guying', 'Haubanage'), ('trunk_protector', 'Gaine de protection du tronc')], default='none', max_length=50, null=True, verbose_name='Protection / Tuteurage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='pruning_frequency_years',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='Périodicité de taille (années)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='pruning_type',
|
||||
field=models.CharField(blank=True, choices=[('free_crown', 'Port libre'), ('semi_free', 'Port semi-libre'), ('architectured_head', 'Taille architecturée (tête de chat / marotte)'), ('architectured_curtain', 'Taille en rideau / tonnelle'), ('formation', 'Taille de formation'), ('none', 'Non taillé / Entretien minimal')], default='free_crown', max_length=50, null=True, verbose_name='Type de conduite / Taille'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='risk_level',
|
||||
field=models.CharField(blank=True, choices=[('low', 'Faible'), ('moderate', 'Modéré'), ('high', 'Élevé'), ('imminent_danger', 'Danger immédiat')], default='low', max_length=50, null=True, verbose_name='Niveau de risque'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='tag_number',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name="N° de plaquette / bague d'inventaire"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='urban_constraints',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Contraintes urbaines'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='vitality',
|
||||
field=models.CharField(blank=True, choices=[('strong', 'Forte (très vigoureux)'), ('normal', 'Normale'), ('weak', 'Faible (stress / manque de vigueur)'), ('declining', 'Dépérissant'), ('dead', 'Mort')], default='normal', max_length=50, null=True, verbose_name='Vigueur / Vitalité'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='naturetree',
|
||||
name='depth',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Profondeur de fosse (m)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='naturetree',
|
||||
name='height',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Hauteur totale (m)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='naturetree',
|
||||
name='protection_buffer',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Rayon de protection racinaire (m)'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-09-21 11:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0026_add_naturetree_specific_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='structure',
|
||||
name='structure_type',
|
||||
field=models.CharField(blank=True, choices=[('tunnel', 'Tunnel'), ('bridge', 'Pont'), ('viaduct', 'Viaduc'), ('overpass', 'Passerelle'), ('underpass', 'Passage souterrain'), ('gallery', 'Galerie technique'), ('atypical', 'Ouvrage atypique'), ('metro_station', 'Station de métro'), ('metro_tunnel', 'Tunnel de métro')], max_length=200, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-05 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0027_alter_structure_structure_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='contractor_name',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Prestataire / Mainteneur'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='dimensions',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Dimensions / Format'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='energy_source',
|
||||
field=models.CharField(blank=True, choices=[('natural_gas', 'Gaz naturel'), ('electricity', 'Électricité'), ('fuel_oil', 'Fioul domestique'), ('biomass_wood', 'Biomasse / Bois / Pellets'), ('heat_network', 'Réseau de chaleur urbain'), ('heat_pump_air', 'Aérothermie (PAC air-eau / air-air)'), ('heat_pump_geo', 'Géothermie (PAC eau-eau / sol-eau)'), ('solar_thermal', 'Solaire thermique'), ('other', 'Autre source')], max_length=50, null=True, verbose_name="Source d'énergie"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='fire_rating',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Classement au feu / Coupe-feu'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='glazing_type',
|
||||
field=models.CharField(blank=True, choices=[('single', 'Simple vitrage'), ('double_standard', 'Double vitrage standard'), ('double_low_e', 'Double vitrage ITR / Haute isolation'), ('triple', 'Triple vitrage'), ('laminated_safety', 'Vitrage feuilleté de sécurité'), ('other', 'Autre vitrage')], max_length=50, null=True, verbose_name='Type de vitrage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='maintenance_contract_ref',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Réf. contrat maintenance'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='material',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Matériau principal'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='opening_type',
|
||||
field=models.CharField(blank=True, choices=[('hinged', 'Battant / À la française'), ('sliding', 'Coulissant'), ('tilt_and_turn', 'Oscillo-battant'), ('double_action', 'Va-et-vient'), ('sectional_overhead', 'Sectionnel / Basculant'), ('motorized_automatic', 'Automatique / Motorisé'), ('fixed', 'Châssis fixe / Non ouvrant'), ('other', 'Autre type')], max_length=50, null=True, verbose_name="Type d'ouverture"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='power_kw',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Puissance nominale (kW)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='refrigerant_charge_kg',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Charge fluide frigorigène (kg)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='refrigerant_type',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Fluide frigorigène'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='regulatory_periodicity_months',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='Périodicité contrôle réglementaire (mois)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='replacement_cost_estimate',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='Coût estimé de renouvellement (€)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='surface_m2',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Surface unitaire (m²)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='ceiling_height_m',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Hauteur sous plafond (m)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='fire_compartment',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Secteur / Compartiment coupe-feu'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='floor_type',
|
||||
field=models.CharField(blank=True, choices=[('tiles', 'Carrelage / Faïence'), ('parquet', 'Parquet / Bois'), ('linoleum_pvc', 'Linoléum / Vinyle / PVC'), ('concrete', 'Béton lissé / Chape / Résine'), ('carpet', 'Moquette / Textile'), ('other', 'Autre revêtement')], max_length=50, null=True, verbose_name='Revêtement de sol dominant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='is_heated',
|
||||
field=models.BooleanField(default=True, verbose_name='Local chauffé'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='room_number',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Numéro de local / porte'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='buildingasset',
|
||||
name='asset_type',
|
||||
field=models.CharField(choices=[('roof', 'Toiture / Couverture / Étanchéité'), ('roof_drainage', 'Zinguerie & Évacuation EP (Gouttière, chéneau)'), ('facade', 'Façade / Bardage / Mur extérieur'), ('window', 'Fenêtre / Baie vitrée / Châssis'), ('skylight', 'Lanterneau / Verrière / Puits de lumière'), ('external_door', "Porte extérieure / Porte d'entrée / Portail"), ('shutter_blind', 'Volet / Store / Brise-soleil (BSO)'), ('insulation', 'Isolation thermique (ITE / ITI)'), ('wall', 'Mur intérieur / Cloison'), ('interior_door', 'Porte intérieure / Bloc-porte'), ('fire_door', 'Porte coupe-feu / Bloc pare-flamme'), ('ceiling', 'Faux-plafond / Plafond suspendu'), ('flooring', 'Revêtement de sol'), ('stairs', 'Escalier / Garde-corps / Main courante'), ('boiler', 'Chaudière (Gaz / Fioul / Biomasse / Pellets)'), ('heat_pump', 'Pompe à chaleur (PAC) / Climatisation réversible'), ('heat_substation', "Sous-station d'échangeur thermique / Réseau urbain"), ('radiator_emitter', 'Émetteur de chaleur (Radiateur, convecteur, plancher)'), ('air_handling_unit', "Centrale de traitement d'air (CTA) / VMC"), ('air_conditioning', 'Climatisation / Groupe froid / Chiller / VRV'), ('water_heater', 'Production Eau Chaude Sanitaire (Ballon ECS)'), ('hvac', 'Chauffage / CVC / Régulation générale'), ('electricity', 'Électricité / TGBT / Tableau divisionnaire'), ('inverter_generator', 'Onduleur / Groupe électrogène de secours'), ('lighting', 'Éclairage intérieur / Luminaires'), ('emergency_lighting', 'Éclairage de sécurité / BAES / BAEH'), ('meter', 'Compteur (Eau / Gaz / Électricité / Calories)'), ('solar_panel', 'Panneaux solaires (Photovoltaïque / Thermique)'), ('ev_charging', 'Borne de recharge véhicule électrique'), ('sanitary', 'Plomberie / Sanitaires (WC, lavabo, douche)'), ('plumbing_piping', "Réseau d'eau / Vanne d'arrêt / Disconnecteur"), ('sump_pump', 'Pompe de relevage (Eaux usées / Eaux pluviales)'), ('rainwater_harvesting', "Récupération d'eau de pluie / Cuve de stockage"), ('fire_safety', 'Sécurité incendie / Centrale SSI / CMSI'), ('smoke_detector', 'Détecteur de fumée / Déclencheur manuel (DM)'), ('fire_extinguisher', "Extincteur / Robinet d'Incendie Armé (RIA)"), ('smoke_exhaust', 'Désenfumage / Trappe / Skydome'), ('access_control', "Contrôle d'accès (Badge, interphone, gâche)"), ('alarm', 'Alarme anti-intrusion / Détecteur de présence'), ('cctv_surveillance', 'Vidéosurveillance / Caméra CCTV'), ('elevator', 'Ascenseur / Monte-charge'), ('disabled_lift', 'Élévateur PMR / Plateforme élévatrice'), ('network_rack', 'Baie de brassage / Rack informatique / RJ45'), ('bms_system', 'Gestion Technique du Bâtiment (GTB / GTC)'), ('civil_engineering', 'Génie civil / Bâti / Gros œuvre'), ('other', 'Autre équipement / asset')], default='civil_engineering', max_length=50, verbose_name="Type d'asset / Équipement"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='location_type',
|
||||
field=models.CharField(choices=[('site', 'Site / Complexe'), ('building', 'Bâtiment / Bloc'), ('wing', 'Aile / Corps de bâtiment'), ('level', 'Niveau / Étage'), ('basement', 'Sous-sol / Cave'), ('attic', 'Combles / Grenier / Toiture-terrasse'), ('mezzanine', 'Mezzanine'), ('stairwell', "Cage d'escalier"), ('corridor', 'Couloir / Coursive / Dégagement'), ('lobby', "Hall d'accueil / Entrée / Sas"), ('emergency_exit', "Issue de secours / Sas d'évacuation"), ('elevator_shaft', "Gaine / Palier d'ascenseur"), ('technical_room', 'Local technique général'), ('boiler_room', 'Chaufferie / Sous-station thermique'), ('electrical_room', 'Local TGBT / Électrique'), ('ventilation_room', 'Local ventilation / CTA'), ('elevator_machine_room', 'Local machinerie ascenseur'), ('server_room', 'Salle serveurs / Baie informatique'), ('room', 'Pièce / Local polyvalent'), ('office', 'Bureau / Espace de travail'), ('meeting_room', 'Salle de réunion / Conférence'), ('classroom', 'Salle de classe / Formation'), ('workshop', 'Atelier / Laboratoire'), ('sanitary_room', 'Sanitaires / Vestiaires / Douches'), ('storage_room', 'Réserve / Stockage / Archives'), ('canteen_kitchen', 'Cuisine / Réfectoire / Restauration'), ('cleaning_room', 'Local ménage / Entretien'), ('waste_room', 'Local poubelles / Déchets'), ('indoor_parking', 'Parking couvert / Garage'), ('outdoor_area', 'Abords / Cour / Parvis / Terrasse'), ('other', 'Autre localisation')], default='building', max_length=50, verbose_name='Type de localisation'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-05 08:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0028_buildingasset_contractor_name_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='publicbuildingdocument',
|
||||
name='floor_location',
|
||||
field=models.ForeignKey(blank=True, help_text='Niveau ou local auquel ce document/plan se rapporte.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='floor_documents', to='assets.publicbuilding', verbose_name='Niveau / Étage concerné'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuildingdocument',
|
||||
name='document_type',
|
||||
field=models.CharField(choices=[('floor_plan', "Plan d'étage / Plan architectural"), ('as_built', 'Plan de récolement (As-Built)'), ('erp_notice', 'Notice de sécurité / Dossier ERP'), ('dpe', 'Diagnostic énergétique (DPE / PEB)'), ('technical_notice', "Notice technique / Manuel d'exploitation"), ('other', 'Autre document')], default='other', max_length=50, verbose_name='Type de document'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-05 13:06
|
||||
|
||||
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', '0029_publicbuildingdocument_floor_location_and_more'),
|
||||
('common', '0017_territory'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('documents', '0004_alter_documentfolder_visibility_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ManagementZone',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=100, unique=True, verbose_name='Code')),
|
||||
('name_fr', models.CharField(max_length=255, verbose_name='Nom (FR)')),
|
||||
('name_nl', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom (NL)')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('instructions', models.TextField(blank=True, help_text="Consignes d'exploitation, points de vigilance, règles de sécurité...", null=True, verbose_name="Consignes et points d'attention")),
|
||||
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Périmètre géographique')),
|
||||
('color', models.CharField(blank=True, help_text='Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone.', max_length=20, null=True, verbose_name="Couleur d'affichage")),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('target_frequency_days', models.IntegerField(blank=True, help_text='Intervalle indicatif entre interventions en jours.', null=True, verbose_name='Fréquence cible (jours)')),
|
||||
('seasonality_notes', models.CharField(blank=True, help_text="Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps.", max_length=255, null=True, verbose_name='Saisonnalité / Calendrier')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Date de création')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Date de mise à jour')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_management_zones', to=settings.AUTH_USER_MODEL, verbose_name='Créé par')),
|
||||
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='management_zones', to='common.thematic', verbose_name='Thématique')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Zone de gestion',
|
||||
'verbose_name_plural': 'Zones de gestion',
|
||||
'ordering': ['thematic', 'code'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagementZoneDocument',
|
||||
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_documents/management_zones/', verbose_name='Fichier')),
|
||||
('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/management_zones/')),
|
||||
('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')),
|
||||
('zone', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.managementzone', verbose_name='Zone de gestion')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document de zone de gestion',
|
||||
'verbose_name_plural': 'Documents de zones de gestion',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagementZoneType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=100, unique=True, verbose_name='Code')),
|
||||
('name_fr', models.CharField(max_length=255, verbose_name='Nom (FR)')),
|
||||
('name_nl', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom (NL)')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('default_color', models.CharField(default='#198754', max_length=20, verbose_name='Couleur par défaut')),
|
||||
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='management_zone_types', to='common.thematic', verbose_name='Thématique')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Type de zone de gestion',
|
||||
'verbose_name_plural': 'Types de zones de gestion',
|
||||
'ordering': ['thematic', 'name_fr'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='managementzone',
|
||||
name='zone_type',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='management_zones', to='assets.managementzonetype', verbose_name='Type de zone de gestion'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagementZoneAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField(verbose_name="ID de l'asset")),
|
||||
('is_excluded', models.BooleanField(default=False, help_text='Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre.', verbose_name='Exclu')),
|
||||
('notes', models.CharField(blank=True, max_length=255, null=True, verbose_name='Notes')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset")),
|
||||
('management_zone', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='explicit_assets', to='assets.managementzone', verbose_name='Zone de gestion')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Asset de zone de gestion',
|
||||
'verbose_name_plural': 'Assets de zones de gestion',
|
||||
'unique_together': {('management_zone', 'content_type', 'object_id')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-08 10:19
|
||||
|
||||
import assets.models.core
|
||||
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', '0030_managementzone_managementzonedocument_and_more'),
|
||||
('contracts', '0014_contract_color_contract_geom'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='itsgeoasset',
|
||||
name='extra_data',
|
||||
field=models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardSystem',
|
||||
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)),
|
||||
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
||||
('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)),
|
||||
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
||||
('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')),
|
||||
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
||||
('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.LineStringField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Ligne)')),
|
||||
('width', models.FloatField(blank=True, null=True, verbose_name='Largeur de passage (m)')),
|
||||
('direction', models.CharField(blank=True, choices=[('unidirectional', 'Sens unique'), ('bidirectional', 'Double sens')], default='bidirectional', max_length=50, null=True, verbose_name='Sens de circulation')),
|
||||
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
||||
('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='bollard_systems', to='assets.itslocation', verbose_name='Localisation ITS')),
|
||||
('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')),
|
||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de système')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardsystem', 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': 'Système de bornes',
|
||||
'verbose_name_plural': 'Systèmes de bornes',
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardSystemAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('camera_entry', 'Caméra entrée (LAPI)'), ('camera_exit', 'Caméra sortie (LAPI)'), ('camera_cctv', 'Caméra de contexte / surveillance'), ('cabinet', 'Armoire électrique / automate'), ('totem', 'Totem de commande'), ('traffic_light', 'Feu de franchissement'), ('loop', 'Boucle de détection'), ('other', 'Autre')], default='other', max_length=50, verbose_name="Rôle de l'équipement")),
|
||||
('comment', models.CharField(blank=True, max_length=255, null=True, verbose_name='Commentaire')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name="Date d'association")),
|
||||
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_system_links', to='assets.itsgeoasset', verbose_name='Équipement ITS')),
|
||||
('bollard_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='system_assets', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Équipement associé au système de bornes',
|
||||
'verbose_name_plural': 'Équipements associés au système de bornes',
|
||||
'unique_together': {('bollard_system', 'asset')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itsbollardsystem',
|
||||
name='associated_assets',
|
||||
field=models.ManyToManyField(blank=True, related_name='bollard_systems', through='assets.ITSBollardSystemAsset', to='assets.itsgeoasset', verbose_name='Équipements associés'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardSystemContract',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||
('maintenance_type', models.CharField(choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)),
|
||||
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='contracts.contract')),
|
||||
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='assets.itsbollardsystem')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Contrat de système de bornes',
|
||||
'verbose_name_plural': 'Contrats de systèmes de bornes',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardPlot',
|
||||
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)),
|
||||
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
||||
('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)),
|
||||
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
||||
('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')),
|
||||
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
||||
('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.PointField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Point)')),
|
||||
('position', models.PositiveSmallIntegerField(default=1, help_text='Ordre de gauche à droite (1, 2, 3...)', verbose_name='Position sur la traverse')),
|
||||
('bollard_type', models.CharField(choices=[('automatic', 'Automatique / Escamotable'), ('semi_automatic', 'Semi-automatique'), ('fixed', 'Fixe'), ('removable', 'Amovible')], default='automatic', max_length=50, verbose_name='Type de plot')),
|
||||
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
||||
('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='bollard_plots', to='assets.itslocation', verbose_name='Localisation ITS')),
|
||||
('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')),
|
||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de plot')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardplot', 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')),
|
||||
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plots', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Plot de borne',
|
||||
'verbose_name_plural': 'Plots de bornes',
|
||||
'ordering': ['system', 'position'],
|
||||
'indexes': [models.Index(fields=['code'], name='assets_itsb_code_de6000_idx')],
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='itsbollardsystem',
|
||||
index=models.Index(fields=['code'], name='assets_itsb_code_a4c148_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='itsbollardsystemcontract',
|
||||
unique_together={('system', 'contract')},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-07 15:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0030_managementzone_managementzonedocument_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='amenity_value',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name="Valeur d'aménité (€)"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='biodiversity_index',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Indice de biodiversité'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='carbon_stock',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Stock de carbone (t CO2)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='cooling_energy_indicator',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Énergie de refroidissement (kWh)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='cooling_indicator',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Indice de rafraîchissement'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='cultivar',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Cultivar / Variété'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='death_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Date de dépérissement / mort'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='genus',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Genre botanique'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='is_chronoxyle',
|
||||
field=models.BooleanField(default=False, verbose_name='Arbre sénescent / Chronoxyle (bois mort sur pied)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='is_dead',
|
||||
field=models.BooleanField(default=False, verbose_name='Arbre mort'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='is_stump',
|
||||
field=models.BooleanField(default=False, verbose_name='Souche'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='management_type',
|
||||
field=models.CharField(blank=True, choices=[('municipal', 'Communal (Régie communale)'), ('regional', 'Régional (Bruxelles Mobilité / Région)'), ('community', 'Intercommunal / Autre pouvoir public'), ('private', 'Privé / Tiers')], default='municipal', max_length=50, null=True, verbose_name='Type de gestionnaire'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='managing_authority',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Autorité gestionnaire'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='number_of_trunks',
|
||||
field=models.IntegerField(blank=True, default=1, null=True, verbose_name='Nombre de troncs / Cépée'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='plantation_coefficient',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Coefficient de plantation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='planting_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Date de plantation réelle'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='scientific_name',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Nom scientifique / latin'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='situation_coefficient',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Coefficient de situation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='vernacular_name',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Nom vernaculaire / Essence'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-09 11:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0031_itsgeoasset_extra_data_itsbollardsystem_and_more'),
|
||||
('assets', '0031_naturetree_amenity_value_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
|
|
@ -1,741 +0,0 @@
|
|||
# -----------------------------
|
||||
# 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 = (
|
||||
# Ensembles & Bâtiments
|
||||
('site', _('Site / Complexe')),
|
||||
('building', _('Bâtiment / Bloc')),
|
||||
('wing', _('Aile / Corps de bâtiment')),
|
||||
# Niveaux & Hauteurs
|
||||
('level', _('Niveau / Étage')),
|
||||
('basement', _('Sous-sol / Cave')),
|
||||
('attic', _('Combles / Grenier / Toiture-terrasse')),
|
||||
('mezzanine', _('Mezzanine')),
|
||||
# Circulations & Dégagements
|
||||
('stairwell', _('Cage d\'escalier')),
|
||||
('corridor', _('Couloir / Coursive / Dégagement')),
|
||||
('lobby', _('Hall d\'accueil / Entrée / Sas')),
|
||||
('emergency_exit', _('Issue de secours / Sas d\'évacuation')),
|
||||
('elevator_shaft', _('Gaine / Palier d\'ascenseur')),
|
||||
# Locaux techniques
|
||||
('technical_room', _('Local technique général')),
|
||||
('boiler_room', _('Chaufferie / Sous-station thermique')),
|
||||
('electrical_room', _('Local TGBT / Électrique')),
|
||||
('ventilation_room', _('Local ventilation / CTA')),
|
||||
('elevator_machine_room', _('Local machinerie ascenseur')),
|
||||
('server_room', _('Salle serveurs / Baie informatique')),
|
||||
# Locaux de travail, d'accueil & services
|
||||
('room', _('Pièce / Local polyvalent')),
|
||||
('office', _('Bureau / Espace de travail')),
|
||||
('meeting_room', _('Salle de réunion / Conférence')),
|
||||
('classroom', _('Salle de classe / Formation')),
|
||||
('workshop', _('Atelier / Laboratoire')),
|
||||
('sanitary_room', _('Sanitaires / Vestiaires / Douches')),
|
||||
('storage_room', _('Réserve / Stockage / Archives')),
|
||||
('canteen_kitchen', _('Cuisine / Réfectoire / Restauration')),
|
||||
('cleaning_room', _('Local ménage / Entretien')),
|
||||
('waste_room', _('Local poubelles / Déchets')),
|
||||
('indoor_parking', _('Parking couvert / Garage')),
|
||||
# Espaces extérieurs immédiats
|
||||
('outdoor_area', _('Abords / Cour / Parvis / Terrasse')),
|
||||
# Autre
|
||||
('other', _('Autre localisation')),
|
||||
)
|
||||
|
||||
FLOOR_TYPE_CHOICES = (
|
||||
('tiles', _('Carrelage / Faïence')),
|
||||
('parquet', _('Parquet / Bois')),
|
||||
('linoleum_pvc', _('Linoléum / Vinyle / PVC')),
|
||||
('concrete', _('Béton lissé / Chape / Résine')),
|
||||
('carpet', _('Moquette / Textile')),
|
||||
('other', _('Autre revêtement')),
|
||||
)
|
||||
|
||||
BUILDING_ASSET_TYPE_CHOICES = (
|
||||
# Clos, couvert & enveloppe
|
||||
('roof', _('Toiture / Couverture / Étanchéité')),
|
||||
('roof_drainage', _('Zinguerie & Évacuation EP (Gouttière, chéneau)')),
|
||||
('facade', _('Façade / Bardage / Mur extérieur')),
|
||||
('window', _('Fenêtre / Baie vitrée / Châssis')),
|
||||
('skylight', _('Lanterneau / Verrière / Puits de lumière')),
|
||||
('external_door', _('Porte extérieure / Porte d\'entrée / Portail')),
|
||||
('shutter_blind', _('Volet / Store / Brise-soleil (BSO)')),
|
||||
('insulation', _('Isolation thermique (ITE / ITI)')),
|
||||
# Second œuvre & aménagements intérieurs
|
||||
('wall', _('Mur intérieur / Cloison')),
|
||||
('interior_door', _('Porte intérieure / Bloc-porte')),
|
||||
('fire_door', _('Porte coupe-feu / Bloc pare-flamme')),
|
||||
('ceiling', _('Faux-plafond / Plafond suspendu')),
|
||||
('flooring', _('Revêtement de sol')),
|
||||
('stairs', _('Escalier / Garde-corps / Main courante')),
|
||||
# Génie climatique & thermique (CVC)
|
||||
('boiler', _('Chaudière (Gaz / Fioul / Biomasse / Pellets)')),
|
||||
('heat_pump', _('Pompe à chaleur (PAC) / Climatisation réversible')),
|
||||
('heat_substation', _('Sous-station d\'échangeur thermique / Réseau urbain')),
|
||||
('radiator_emitter', _('Émetteur de chaleur (Radiateur, convecteur, plancher)')),
|
||||
('air_handling_unit', _('Centrale de traitement d\'air (CTA) / VMC')),
|
||||
('air_conditioning', _('Climatisation / Groupe froid / Chiller / VRV')),
|
||||
('water_heater', _('Production Eau Chaude Sanitaire (Ballon ECS)')),
|
||||
('hvac', _('Chauffage / CVC / Régulation générale')),
|
||||
# Électricité, Énergie & Réseaux
|
||||
('electricity', _('Électricité / TGBT / Tableau divisionnaire')),
|
||||
('inverter_generator', _('Onduleur / Groupe électrogène de secours')),
|
||||
('lighting', _('Éclairage intérieur / Luminaires')),
|
||||
('emergency_lighting', _('Éclairage de sécurité / BAES / BAEH')),
|
||||
('meter', _('Compteur (Eau / Gaz / Électricité / Calories)')),
|
||||
('solar_panel', _('Panneaux solaires (Photovoltaïque / Thermique)')),
|
||||
('ev_charging', _('Borne de recharge véhicule électrique')),
|
||||
# Plomberie & Sanitaires
|
||||
('sanitary', _('Plomberie / Sanitaires (WC, lavabo, douche)')),
|
||||
('plumbing_piping', _('Réseau d\'eau / Vanne d\'arrêt / Disconnecteur')),
|
||||
('sump_pump', _('Pompe de relevage (Eaux usées / Eaux pluviales)')),
|
||||
('rainwater_harvesting', _('Récupération d\'eau de pluie / Cuve de stockage')),
|
||||
# Sécurité incendie, sûreté & contrôle d'accès
|
||||
('fire_safety', _('Sécurité incendie / Centrale SSI / CMSI')),
|
||||
('smoke_detector', _('Détecteur de fumée / Déclencheur manuel (DM)')),
|
||||
('fire_extinguisher', _('Extincteur / Robinet d\'Incendie Armé (RIA)')),
|
||||
('smoke_exhaust', _('Désenfumage / Trappe / Skydome')),
|
||||
('access_control', _('Contrôle d\'accès (Badge, interphone, gâche)')),
|
||||
('alarm', _('Alarme anti-intrusion / Détecteur de présence')),
|
||||
('cctv_surveillance', _('Vidéosurveillance / Caméra CCTV')),
|
||||
# Mobilité verticale
|
||||
('elevator', _('Ascenseur / Monte-charge')),
|
||||
('disabled_lift', _('Élévateur PMR / Plateforme élévatrice')),
|
||||
# Réseaux, GTB & Automatismes
|
||||
('network_rack', _('Baie de brassage / Rack informatique / RJ45')),
|
||||
('bms_system', _('Gestion Technique du Bâtiment (GTB / GTC)')),
|
||||
# Gros œuvre & Divers
|
||||
('civil_engineering', _('Génie civil / Bâti / Gros œuvre')),
|
||||
('other', _('Autre équipement / asset')),
|
||||
)
|
||||
|
||||
# For backward compatibility
|
||||
EQUIPMENT_TYPE_CHOICES = BUILDING_ASSET_TYPE_CHOICES
|
||||
|
||||
ENERGY_SOURCE_CHOICES = (
|
||||
('natural_gas', _('Gaz naturel')),
|
||||
('electricity', _('Électricité')),
|
||||
('fuel_oil', _('Fioul domestique')),
|
||||
('biomass_wood', _('Biomasse / Bois / Pellets')),
|
||||
('heat_network', _('Réseau de chaleur urbain')),
|
||||
('heat_pump_air', _('Aérothermie (PAC air-eau / air-air)')),
|
||||
('heat_pump_geo', _('Géothermie (PAC eau-eau / sol-eau)')),
|
||||
('solar_thermal', _('Solaire thermique')),
|
||||
('other', _('Autre source')),
|
||||
)
|
||||
|
||||
GLAZING_TYPE_CHOICES = (
|
||||
('single', _('Simple vitrage')),
|
||||
('double_standard', _('Double vitrage standard')),
|
||||
('double_low_e', _('Double vitrage ITR / Haute isolation')),
|
||||
('triple', _('Triple vitrage')),
|
||||
('laminated_safety', _('Vitrage feuilleté de sécurité')),
|
||||
('other', _('Autre vitrage')),
|
||||
)
|
||||
|
||||
OPENING_TYPE_CHOICES = (
|
||||
('hinged', _('Battant / À la française')),
|
||||
('sliding', _('Coulissant')),
|
||||
('tilt_and_turn', _('Oscillo-battant')),
|
||||
('double_action', _('Va-et-vient')),
|
||||
('sectional_overhead', _('Sectionnel / Basculant')),
|
||||
('motorized_automatic', _('Automatique / Motorisé')),
|
||||
('fixed', _('Châssis fixe / Non ouvrant')),
|
||||
('other', _('Autre type')),
|
||||
)
|
||||
|
||||
|
||||
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)"))
|
||||
room_number = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Numéro de local / porte"))
|
||||
ceiling_height_m = models.FloatField(null=True, blank=True, verbose_name=_("Hauteur sous plafond (m)"))
|
||||
is_heated = models.BooleanField(default=True, verbose_name=_("Local chauffé"))
|
||||
fire_compartment = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Secteur / Compartiment coupe-feu"))
|
||||
floor_type = models.CharField(max_length=50, choices=FLOOR_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Revêtement de sol dominant"))
|
||||
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']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.name_fr or self.name_nl or self.code
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:publicbuilding_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def get_floor_plan_url(self):
|
||||
"""
|
||||
Renvoie l'URL du plan d'étage s'il existe (via floor_plan ou document associé de type floor_plan).
|
||||
"""
|
||||
if self.floor_plan and self.floor_plan.name:
|
||||
return reverse('assets:publicbuilding_plan_file', kwargs={'pk': self.pk})
|
||||
docs = getattr(self, 'floor_documents', None)
|
||||
if docs:
|
||||
first_doc = docs.filter(document_type='floor_plan').first()
|
||||
if first_doc and first_doc.file and first_doc.file.name:
|
||||
return reverse('assets:publicbuilding_plan_file', kwargs={'pk': self.pk})
|
||||
return None
|
||||
|
||||
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_icon_class(self):
|
||||
"""
|
||||
Retourne la classe d'icône Bootstrap adaptée au type de localisation.
|
||||
"""
|
||||
mapping = {
|
||||
'site': 'bi-diagram-3',
|
||||
'building': 'bi-building',
|
||||
'wing': 'bi-building-add',
|
||||
'level': 'bi-layers',
|
||||
'basement': 'bi-layer-backward',
|
||||
'attic': 'bi-triangle',
|
||||
'mezzanine': 'bi-border-inner',
|
||||
'stairwell': 'bi-stairs',
|
||||
'corridor': 'bi-arrow-left-right',
|
||||
'lobby': 'bi-door-open-fill',
|
||||
'emergency_exit': 'bi-box-arrow-right',
|
||||
'elevator_shaft': 'bi-arrow-down-up',
|
||||
'technical_room': 'bi-tools',
|
||||
'boiler_room': 'bi-fire',
|
||||
'electrical_room': 'bi-lightning-charge',
|
||||
'ventilation_room': 'bi-fan',
|
||||
'elevator_machine_room': 'bi-gear-wide-connected',
|
||||
'server_room': 'bi-hdd-network',
|
||||
'room': 'bi-door-open',
|
||||
'office': 'bi-briefcase',
|
||||
'meeting_room': 'bi-people',
|
||||
'classroom': 'bi-mortarboard',
|
||||
'workshop': 'bi-hammer',
|
||||
'sanitary_room': 'bi-badge-wc',
|
||||
'storage_room': 'bi-archive',
|
||||
'canteen_kitchen': 'bi-cup-hot',
|
||||
'cleaning_room': 'bi-bucket',
|
||||
'waste_room': 'bi-trash',
|
||||
'indoor_parking': 'bi-car-front',
|
||||
'outdoor_area': 'bi-tree',
|
||||
}
|
||||
return mapping.get(self.location_type, 'bi-geo-alt')
|
||||
|
||||
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: Bâti -> Façade -> Châssis, ou Chaudière -> Brûleur).
|
||||
"""
|
||||
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"))
|
||||
|
||||
# Caractéristiques physiques & dimensionnelles
|
||||
dimensions = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Dimensions / Format"))
|
||||
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface unitaire (m²)"))
|
||||
material = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Matériau principal"))
|
||||
|
||||
# Caractéristiques thermiques, énergétiques & techniques
|
||||
power_kw = models.FloatField(null=True, blank=True, verbose_name=_("Puissance nominale (kW)"))
|
||||
energy_source = models.CharField(max_length=50, choices=ENERGY_SOURCE_CHOICES, null=True, blank=True, verbose_name=_("Source d'énergie"))
|
||||
refrigerant_type = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Fluide frigorigène"))
|
||||
refrigerant_charge_kg = models.FloatField(null=True, blank=True, verbose_name=_("Charge fluide frigorigène (kg)"))
|
||||
|
||||
# Menuiseries, ouvrants & protection incendie
|
||||
glazing_type = models.CharField(max_length=50, choices=GLAZING_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Type de vitrage"))
|
||||
fire_rating = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Classement au feu / Coupe-feu"))
|
||||
opening_type = models.CharField(max_length=50, choices=OPENING_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Type d'ouverture"))
|
||||
|
||||
# Maintenance, exploitation & suivi réglementaire
|
||||
maintenance_contract_ref = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Réf. contrat maintenance"))
|
||||
contractor_name = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Prestataire / Mainteneur"))
|
||||
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"))
|
||||
regulatory_periodicity_months = models.IntegerField(null=True, blank=True, verbose_name=_("Périodicité contrôle réglementaire (mois)"))
|
||||
replacement_cost_estimate = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True, verbose_name=_("Coût estimé de renouvellement (€)"))
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||
self.geom = self.parent_asset.geom
|
||||
self.lon = self.parent_asset.lon
|
||||
self.lat = self.parent_asset.lat
|
||||
self.geojson = self.parent_asset.geojson
|
||||
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||
self.location = self.parent_asset.location
|
||||
if self.parent_asset and self.parent_asset.building and not self.building_id:
|
||||
self.building = self.parent_asset.building
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||
self.geom = self.parent_asset.geom
|
||||
self.lon = self.parent_asset.lon
|
||||
self.lat = self.parent_asset.lat
|
||||
self.geojson = self.parent_asset.geojson
|
||||
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||
self.location = self.parent_asset.location
|
||||
if self.parent_asset and self.parent_asset.building and not self.building_id:
|
||||
self.building = self.parent_asset.building
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Asset / Équipement de bâtiment")
|
||||
verbose_name_plural = _("Assets / Équipements de bâtiment")
|
||||
ordering = ['building', 'code']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.name_fr or self.name_nl or self.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()
|
||||
|
||||
def get_macro_lot(self):
|
||||
"""
|
||||
Retourne le macro-lot / domaine technique de l'équipement.
|
||||
"""
|
||||
lots = {
|
||||
# Clos & Couvert
|
||||
'roof': _('Clos & Couvert'),
|
||||
'roof_drainage': _('Clos & Couvert'),
|
||||
'facade': _('Clos & Couvert'),
|
||||
'window': _('Clos & Couvert'),
|
||||
'skylight': _('Clos & Couvert'),
|
||||
'external_door': _('Clos & Couvert'),
|
||||
'shutter_blind': _('Clos & Couvert'),
|
||||
'insulation': _('Clos & Couvert'),
|
||||
# Aménagements & Second œuvre
|
||||
'wall': _('Aménagements & Second œuvre'),
|
||||
'interior_door': _('Aménagements & Second œuvre'),
|
||||
'fire_door': _('Aménagements & Second œuvre'),
|
||||
'ceiling': _('Aménagements & Second œuvre'),
|
||||
'flooring': _('Aménagements & Second œuvre'),
|
||||
'stairs': _('Aménagements & Second œuvre'),
|
||||
# Génie climatique & CVC
|
||||
'boiler': _('Génie climatique & CVC'),
|
||||
'heat_pump': _('Génie climatique & CVC'),
|
||||
'heat_substation': _('Génie climatique & CVC'),
|
||||
'radiator_emitter': _('Génie climatique & CVC'),
|
||||
'air_handling_unit': _('Génie climatique & CVC'),
|
||||
'air_conditioning': _('Génie climatique & CVC'),
|
||||
'water_heater': _('Génie climatique & CVC'),
|
||||
'hvac': _('Génie climatique & CVC'),
|
||||
# Électricité & Énergie
|
||||
'electricity': _('Électricité & Énergie'),
|
||||
'inverter_generator': _('Électricité & Énergie'),
|
||||
'lighting': _('Électricité & Éclairage'),
|
||||
'emergency_lighting': _('Électricité & Sécurité'),
|
||||
'meter': _('Fluides & Comptage'),
|
||||
'solar_panel': _('Énergies renouvelables'),
|
||||
'ev_charging': _('Mobilité électrique'),
|
||||
# Plomberie & Sanitaires
|
||||
'sanitary': _('Plomberie & Sanitaires'),
|
||||
'plumbing_piping': _('Plomberie & Réseaux'),
|
||||
'sump_pump': _('Assainissement & Relevage'),
|
||||
'rainwater_harvesting': _('Gestion des eaux pluviales'),
|
||||
# Sécurité incendie & Sûreté
|
||||
'fire_safety': _('Sécurité incendie'),
|
||||
'smoke_detector': _('Sécurité incendie'),
|
||||
'fire_extinguisher': _('Sécurité incendie'),
|
||||
'smoke_exhaust': _('Sécurité incendie'),
|
||||
'access_control': _('Sûreté & Contrôle d\'accès'),
|
||||
'alarm': _('Sûreté & Contrôle d\'accès'),
|
||||
'cctv_surveillance': _('Vidéosurveillance'),
|
||||
# Mobilité verticale
|
||||
'elevator': _('Ascenseurs & Élévateurs'),
|
||||
'disabled_lift': _('Ascenseurs & Élévateurs'),
|
||||
# Réseaux & Numérique
|
||||
'network_rack': _('Réseaux & Télécom'),
|
||||
'bms_system': _('Gestion Technique du Bâtiment (GTB)'),
|
||||
# Gros œuvre & Divers
|
||||
'civil_engineering': _('Génie civil & Structure'),
|
||||
'other': _('Autre équipement'),
|
||||
}
|
||||
return lots.get(self.asset_type, _('Autre'))
|
||||
|
||||
def get_icon_class(self):
|
||||
"""
|
||||
Retourne l'icône Bootstrap adaptée au type d'asset.
|
||||
"""
|
||||
icons = {
|
||||
'roof': 'bi-house-gear',
|
||||
'roof_drainage': 'bi-water',
|
||||
'facade': 'bi-border-style',
|
||||
'window': 'bi-window',
|
||||
'skylight': 'bi-sun',
|
||||
'external_door': 'bi-door-closed',
|
||||
'shutter_blind': 'bi-blinds',
|
||||
'insulation': 'bi-shield',
|
||||
'wall': 'bi-bricks',
|
||||
'interior_door': 'bi-door-open',
|
||||
'fire_door': 'bi-shield-shaded',
|
||||
'ceiling': 'bi-grid-3x3',
|
||||
'flooring': 'bi-square',
|
||||
'stairs': 'bi-stairs',
|
||||
'boiler': 'bi-fire',
|
||||
'heat_pump': 'bi-snow',
|
||||
'heat_substation': 'bi-shuffle',
|
||||
'radiator_emitter': 'bi-thermometer-high',
|
||||
'air_handling_unit': 'bi-fan',
|
||||
'air_conditioning': 'bi-wind',
|
||||
'water_heater': 'bi-droplet-half',
|
||||
'hvac': 'bi-thermometer-half',
|
||||
'electricity': 'bi-lightning-charge',
|
||||
'inverter_generator': 'bi-battery-charging',
|
||||
'lighting': 'bi-lightbulb',
|
||||
'emergency_lighting': 'bi-exclamation-triangle',
|
||||
'meter': 'bi-speedometer2',
|
||||
'solar_panel': 'bi-sun-fill',
|
||||
'ev_charging': 'bi-ev-station',
|
||||
'sanitary': 'bi-badge-wc',
|
||||
'plumbing_piping': 'bi-pipette',
|
||||
'sump_pump': 'bi-arrow-up-circle',
|
||||
'rainwater_harvesting': 'bi-cloud-rain',
|
||||
'fire_safety': 'bi-shield-shaded',
|
||||
'smoke_detector': 'bi-bell',
|
||||
'fire_extinguisher': 'bi-cone-striped',
|
||||
'smoke_exhaust': 'bi-cloud-arrow-up',
|
||||
'access_control': 'bi-key',
|
||||
'alarm': 'bi-shield-exclamation',
|
||||
'cctv_surveillance': 'bi-camera-video',
|
||||
'elevator': 'bi-arrow-down-up',
|
||||
'disabled_lift': 'bi-person-wheelchair',
|
||||
'network_rack': 'bi-hdd-rack',
|
||||
'bms_system': 'bi-cpu',
|
||||
'civil_engineering': 'bi-building-gear',
|
||||
'other': 'bi-gear',
|
||||
}
|
||||
return icons.get(self.asset_type, 'bi-gear')
|
||||
|
||||
def get_asset_theme(self):
|
||||
"""
|
||||
Retourne la configuration graphique (couleurs d'icône et de badge)
|
||||
selon la typologie métier de l'asset.
|
||||
"""
|
||||
# 1. Sécurité incendie (Rouge alerte)
|
||||
if self.asset_type in ('fire_safety', 'smoke_detector', 'fire_extinguisher',
|
||||
'smoke_exhaust', 'fire_door'):
|
||||
return {
|
||||
'color': '#b91c1c', # red-700
|
||||
'bg_color': '#fef2f2', # red-50
|
||||
'border_color': '#fecaca', # red-200
|
||||
'icon_color': '#dc2626', # red-600
|
||||
'badge_class': 'bg-danger-subtle text-danger border',
|
||||
}
|
||||
|
||||
# 2. Génie climatique & CVC (Orange thermique / Chaleur & Froid)
|
||||
if self.asset_type in ('boiler', 'heat_pump', 'heat_substation', 'radiator_emitter',
|
||||
'air_handling_unit', 'air_conditioning', 'water_heater', 'hvac'):
|
||||
return {
|
||||
'color': '#c2410c', # orange-700
|
||||
'bg_color': '#fff7ed', # orange-50
|
||||
'border_color': '#fed7aa', # orange-200
|
||||
'icon_color': '#ea580c', # orange-600
|
||||
'badge_class': 'bg-warning-subtle text-danger-emphasis border',
|
||||
}
|
||||
|
||||
# 3. Électricité, Énergie & Éclairage (Ambre / Jaune or)
|
||||
if self.asset_type in ('electricity', 'inverter_generator', 'lighting', 'emergency_lighting',
|
||||
'meter', 'solar_panel', 'ev_charging'):
|
||||
return {
|
||||
'color': '#a16207', # yellow-700
|
||||
'bg_color': '#fefce8', # yellow-50
|
||||
'border_color': '#fef08a', # yellow-200
|
||||
'icon_color': '#ca8a04', # yellow-600
|
||||
'badge_class': 'bg-warning-subtle text-dark border',
|
||||
}
|
||||
|
||||
# 4. Sûreté, Contrôle d'accès & Vidéosurveillance (Pourpre / Violet)
|
||||
if self.asset_type in ('access_control', 'alarm', 'cctv_surveillance'):
|
||||
return {
|
||||
'color': '#7e22ce', # purple-700
|
||||
'bg_color': '#faf5ff', # purple-50
|
||||
'border_color': '#e9d5ff', # purple-200
|
||||
'icon_color': '#9333ea', # purple-600
|
||||
'badge_class': 'bg-primary-subtle text-primary border',
|
||||
}
|
||||
|
||||
# 5. Clos & Couvert / Enveloppe (Teal / Vert canard)
|
||||
if self.asset_type in ('roof', 'roof_drainage', 'facade', 'window', 'skylight',
|
||||
'external_door', 'shutter_blind', 'insulation'):
|
||||
return {
|
||||
'color': '#0f766e', # teal-700
|
||||
'bg_color': '#f0fdfa', # teal-50
|
||||
'border_color': '#99f6e4', # teal-200
|
||||
'icon_color': '#0d9488', # teal-600
|
||||
'badge_class': 'bg-success-subtle text-success border',
|
||||
}
|
||||
|
||||
# 6. Plomberie, Sanitaires & Gestion de l'eau (Bleu azur / Sky)
|
||||
if self.asset_type in ('sanitary', 'plumbing_piping', 'sump_pump', 'rainwater_harvesting'):
|
||||
return {
|
||||
'color': '#0369a1', # sky-700
|
||||
'bg_color': '#f0f9ff', # sky-50
|
||||
'border_color': '#bae6fd', # sky-200
|
||||
'icon_color': '#0284c7', # sky-600
|
||||
'badge_class': 'bg-info-subtle text-info border',
|
||||
}
|
||||
|
||||
# 7. Mobilité verticale / Ascenseurs (Bleu cobalt)
|
||||
if self.asset_type in ('elevator', 'disabled_lift'):
|
||||
return {
|
||||
'color': '#1d4ed8', # blue-700
|
||||
'bg_color': '#eff6ff', # blue-50
|
||||
'border_color': '#bfdbfe', # blue-200
|
||||
'icon_color': '#2563eb', # blue-600
|
||||
'badge_class': 'bg-primary-subtle text-primary border',
|
||||
}
|
||||
|
||||
# 8. Réseaux & Numérique / GTB (Indigo informatique)
|
||||
if self.asset_type in ('network_rack', 'bms_system'):
|
||||
return {
|
||||
'color': '#4338ca', # indigo-700
|
||||
'bg_color': '#eef2ff', # indigo-50
|
||||
'border_color': '#c7d2fe', # indigo-200
|
||||
'icon_color': '#4f46e5', # indigo-600
|
||||
'badge_class': 'bg-dark-subtle text-dark border',
|
||||
}
|
||||
|
||||
# 9. Second œuvre & Aménagements intérieurs (Ardoise neutre)
|
||||
if self.asset_type in ('wall', 'interior_door', 'ceiling', 'flooring', 'stairs'):
|
||||
return {
|
||||
'color': '#334155', # slate-700
|
||||
'bg_color': '#f8fafc', # slate-50
|
||||
'border_color': '#cbd5e1', # slate-300
|
||||
'icon_color': '#64748b', # slate-500
|
||||
'badge_class': 'bg-secondary-subtle text-secondary border',
|
||||
}
|
||||
|
||||
# Par défaut (Gros œuvre & Autre)
|
||||
return {
|
||||
'color': '#475569',
|
||||
'bg_color': '#f1f5f9',
|
||||
'border_color': '#cbd5e1',
|
||||
'icon_color': '#64748b',
|
||||
'badge_class': 'bg-light text-muted border',
|
||||
}
|
||||
|
||||
@property
|
||||
def theme_color(self):
|
||||
return self.get_asset_theme()['color']
|
||||
|
||||
@property
|
||||
def theme_icon_color(self):
|
||||
return self.get_asset_theme()['icon_color']
|
||||
|
||||
@property
|
||||
def theme_bg_color(self):
|
||||
return self.get_asset_theme()['bg_color']
|
||||
|
||||
@property
|
||||
def theme_border_color(self):
|
||||
return self.get_asset_theme()['border_color']
|
||||
|
||||
@property
|
||||
def theme_badge_style(self):
|
||||
t = self.get_asset_theme()
|
||||
return f"background-color: {t['bg_color']} !important; color: {t['color']} !important; border: 1px solid {t['border_color']} !important;"
|
||||
|
||||
@property
|
||||
def theme_badge_class(self):
|
||||
return self.get_asset_theme()['badge_class']
|
||||
|
||||
|
||||
# Backward compatibility alias
|
||||
BuildingEquipment = BuildingAsset
|
||||
|
||||
|
||||
def upload_to_building(instance, filename):
|
||||
return f"buildings/{instance.building.code}/{filename}"
|
||||
|
||||
|
||||
BUILDING_DOCUMENT_TYPE_CHOICES = (
|
||||
('floor_plan', _("Plan d'étage / Plan architectural")),
|
||||
('as_built', _("Plan de récolement (As-Built)")),
|
||||
('erp_notice', _("Notice de sécurité / Dossier ERP")),
|
||||
('dpe', _("Diagnostic énergétique (DPE / PEB)")),
|
||||
('technical_notice', _("Notice technique / Manuel d'exploitation")),
|
||||
('other', _("Autre document")),
|
||||
)
|
||||
|
||||
|
||||
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"))
|
||||
floor_location = models.ForeignKey(
|
||||
PublicBuilding,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="floor_documents",
|
||||
verbose_name=_("Niveau / Étage concerné"),
|
||||
help_text=_("Niveau ou local auquel ce document/plan se rapporte.")
|
||||
)
|
||||
file = models.FileField(upload_to=upload_to_building, storage=private_storage, verbose_name=_("Fichier"), max_length=255)
|
||||
document_type = models.CharField(max_length=50, choices=BUILDING_DOCUMENT_TYPE_CHOICES, default='other', verbose_name=_("Type de document"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Document de bâtiment")
|
||||
verbose_name_plural = _("Documents de bâtiment")
|
||||
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
|
||||
from common.models import Thematic
|
||||
|
||||
|
||||
class GeoSyncMixin:
|
||||
"""
|
||||
Mixin providing spatial field synchronization (geom, lon, lat, geojson).
|
||||
Automatically updates geojson and lon/lat when geom or lon/lat change.
|
||||
"""
|
||||
|
||||
def sync_geom_and_geojson(self):
|
||||
modified_fields = set()
|
||||
|
||||
old_lon = None
|
||||
old_lat = None
|
||||
old_geom = None
|
||||
|
||||
if self.pk:
|
||||
try:
|
||||
old_instance = type(self).objects.get(pk=self.pk)
|
||||
old_lon = getattr(old_instance, 'lon', None)
|
||||
old_lat = getattr(old_instance, 'lat', None)
|
||||
old_geom = getattr(old_instance, 'geom', None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cur_geom = getattr(self, 'geom', None)
|
||||
cur_lon = getattr(self, 'lon', None)
|
||||
cur_lat = getattr(self, 'lat', None)
|
||||
|
||||
# Check if lon/lat were modified while geom remained unchanged
|
||||
lon_lat_modified = (
|
||||
cur_lon is not None and cur_lat is not None and
|
||||
(cur_lon != old_lon or cur_lat != old_lat) and
|
||||
(cur_geom == old_geom)
|
||||
)
|
||||
|
||||
if lon_lat_modified:
|
||||
# lon/lat were explicitly updated: rebuild geom from lon/lat
|
||||
try:
|
||||
pt_4326 = Point(cur_lon, cur_lat, srid=4326)
|
||||
new_geojson = pt_4326.geojson
|
||||
if getattr(self, 'geojson', None) != new_geojson:
|
||||
self.geojson = new_geojson
|
||||
modified_fields.add('geojson')
|
||||
|
||||
if hasattr(self, 'geom'):
|
||||
field = type(self)._meta.get_field('geom')
|
||||
geom_type = getattr(field, 'geom_type', '').upper()
|
||||
if geom_type in ('POINT', 'GEOMETRY'):
|
||||
srid = getattr(field, 'srid', 3812)
|
||||
pt_target = pt_4326 if srid == 4326 else pt_4326.transform(srid, clone=True)
|
||||
if getattr(self, 'geom', None) != pt_target:
|
||||
self.geom = pt_target
|
||||
modified_fields.add('geom')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif cur_geom is not None:
|
||||
# geom is present and is the source of truth
|
||||
try:
|
||||
geom_4326 = cur_geom.transform(4326, clone=True)
|
||||
new_geojson = geom_4326.geojson
|
||||
if getattr(self, 'geojson', None) != new_geojson:
|
||||
self.geojson = new_geojson
|
||||
modified_fields.add('geojson')
|
||||
|
||||
if geom_4326.geom_type == 'Point':
|
||||
target_point = geom_4326
|
||||
else:
|
||||
centroid = geom_4326.centroid
|
||||
if geom_4326.intersects(centroid) or geom_4326.contains(centroid):
|
||||
target_point = centroid
|
||||
else:
|
||||
target_point = geom_4326.point_on_surface
|
||||
|
||||
new_lon = target_point.x
|
||||
new_lat = target_point.y
|
||||
|
||||
if getattr(self, 'lon', None) != new_lon:
|
||||
self.lon = new_lon
|
||||
modified_fields.add('lon')
|
||||
if getattr(self, 'lat', None) != new_lat:
|
||||
self.lat = new_lat
|
||||
modified_fields.add('lat')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif cur_lon is not None and cur_lat is not None:
|
||||
# geom is None, but lon and lat are provided
|
||||
try:
|
||||
pt_4326 = Point(cur_lon, cur_lat, srid=4326)
|
||||
new_geojson = pt_4326.geojson
|
||||
if getattr(self, 'geojson', None) != new_geojson:
|
||||
self.geojson = new_geojson
|
||||
modified_fields.add('geojson')
|
||||
|
||||
if hasattr(self, 'geom'):
|
||||
field = type(self)._meta.get_field('geom')
|
||||
geom_type = getattr(field, 'geom_type', '').upper()
|
||||
if geom_type in ('POINT', 'GEOMETRY'):
|
||||
srid = getattr(field, 'srid', 3812)
|
||||
pt_target = pt_4326 if srid == 4326 else pt_4326.transform(srid, clone=True)
|
||||
if getattr(self, 'geom', None) != pt_target:
|
||||
self.geom = pt_target
|
||||
modified_fields.add('geom')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif cur_geom is None and cur_lon is None and cur_lat is None:
|
||||
if getattr(self, 'geojson', None) is not None:
|
||||
self.geojson = None
|
||||
modified_fields.add('geojson')
|
||||
|
||||
return modified_fields
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
modified = self.sync_geom_and_geojson()
|
||||
if 'update_fields' in kwargs and kwargs['update_fields'] is not None:
|
||||
update_fields = set(kwargs['update_fields'])
|
||||
update_fields.update(modified)
|
||||
kwargs['update_fields'] = list(update_fields)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
# Abstract base model for locations
|
||||
class AbstractLocation(GeoSyncMixin, models.Model):
|
||||
code = models.CharField(max_length=50)
|
||||
name_fr = models.CharField(max_length=200,null=True,blank=True)
|
||||
name_nl = models.CharField(max_length=200,null=True,blank=True)
|
||||
lon = models.FloatField(null=True,blank=True)
|
||||
lat = models.FloatField(null=True,blank=True)
|
||||
geom = models.PolygonField(srid=3812, null=True,blank=True)
|
||||
geojson = models.TextField(null=True, blank=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_location_updated_by', verbose_name="Mis à jour par")
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr
|
||||
return name
|
||||
|
||||
def __str__(self):
|
||||
return self.code + " - " + self.get_name()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ["code"]
|
||||
|
||||
|
||||
|
||||
# Category of assets
|
||||
class AssetCategory(models.Model):
|
||||
thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True, related_name="asset_categories")
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='children',
|
||||
verbose_name=_("Catégorie parente"),
|
||||
help_text=_("Laisser vide pour une catégorie de premier niveau. Une sous-catégorie ne peut pas avoir d'enfants."),
|
||||
)
|
||||
code = models.CharField(max_length=200)
|
||||
name_fr = models.CharField(max_length=255)
|
||||
name_nl = models.CharField(max_length=255)
|
||||
allowed_models = models.ManyToManyField(
|
||||
ContentType,
|
||||
blank=True,
|
||||
related_name="asset_categories",
|
||||
verbose_name=_("Modèles Django autorisés"),
|
||||
help_text=_("Les types de modèles d'assets (Django) autorisés pour cette catégorie.")
|
||||
)
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
return getattr(self, f"name_{lang}", self.name_fr) # Fallback to name_fr
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_name()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_name()}"
|
||||
|
||||
@property
|
||||
def is_leaf(self):
|
||||
"""Retourne True si cette catégorie n'a pas d'enfants (feuille de l'arbre)."""
|
||||
return not self.children.exists()
|
||||
|
||||
def get_children(self):
|
||||
"""Retourne le queryset des sous-catégories directes."""
|
||||
return self.children.all()
|
||||
|
||||
def get_category_ids_with_children(self):
|
||||
"""Retourne la liste des ids : cette catégorie + toutes ses sous-catégories."""
|
||||
ids = [self.pk]
|
||||
ids.extend(self.children.values_list('pk', flat=True))
|
||||
return ids
|
||||
|
||||
class Meta:
|
||||
ordering = ["thematic", "name_fr"]
|
||||
|
||||
|
||||
# Abstract base model for "models of assets"
|
||||
class AbstractAssetModel(models.Model):
|
||||
code = models.CharField(max_length=50)
|
||||
name_fr = models.CharField(max_length=200,null=True,blank=True)
|
||||
name_nl = models.CharField(max_length=200,null=True,blank=True)
|
||||
description = models.TextField(null=True,blank=True)
|
||||
brand = models.CharField(max_length=500, null=True,blank=True)
|
||||
category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr
|
||||
return name
|
||||
|
||||
def __str__(self):
|
||||
return self.code + " - " + self.get_name()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
ASSET_STATUS_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é'),
|
||||
}
|
||||
|
||||
ASSET_OBSOLESCENCE_CHOICES = {
|
||||
'good': _('Bon état'),
|
||||
'medium': _('Moyen'),
|
||||
'bad': _('Dégradé'),
|
||||
'critical': _('Critique'),
|
||||
'unknown': _('Inconnu'),
|
||||
}
|
||||
|
||||
ASSET_VALIDATION_STATUS_CHOICES = {
|
||||
'to_be_validated': _('À valider'),
|
||||
'validated': _('Validé'),
|
||||
'to_be_completed': _('À compléter'),
|
||||
'rejected': _('Rejeté'),
|
||||
}
|
||||
|
||||
ASSET_INSPECTION_STATUS_CHOICES = {
|
||||
'to_inspect': _('À inspecter'),
|
||||
'compliant': _('Conforme'),
|
||||
'non_compliant': _('Non conforme'),
|
||||
'updated': _('Mis à jour'),
|
||||
'unknown': _('Non inspecté'),
|
||||
}
|
||||
|
||||
# Abstract base model for assets
|
||||
class AbstractAsset(models.Model):
|
||||
code = models.CharField(max_length=50)
|
||||
name_fr = models.CharField(max_length=200,null=True,blank=True)
|
||||
name_nl = models.CharField(max_length=200,null=True,blank=True)
|
||||
external_reference = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Référence externe"))
|
||||
status = models.CharField(choices=ASSET_STATUS_CHOICES, default='active', max_length=50)
|
||||
obsolescence = models.CharField(choices=ASSET_OBSOLESCENCE_CHOICES, default='unknown', null=True, blank=True)
|
||||
installation_date = models.DateTimeField(null=True,blank=True)
|
||||
uninstallation_date = models.DateTimeField(null=True,blank=True)
|
||||
serial_number = models.CharField(max_length=200,null=True,blank=True)
|
||||
warranty_duration = models.IntegerField(null=True,blank=True, verbose_name="Duration of the warranty (months)")
|
||||
last_inspection_date = models.DateTimeField(null=True,blank=True)
|
||||
inspection_status = models.CharField(max_length=50, choices=ASSET_INSPECTION_STATUS_CHOICES, default='unknown', null=True, blank=True, verbose_name="Statut d'inspection")
|
||||
last_preventive_date = models.DateTimeField(null=True,blank=True)
|
||||
last_corrective_date = models.DateTimeField(null=True,blank=True)
|
||||
last_survey_date = models.DateTimeField(null=True,blank=True)
|
||||
category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
brand = models.CharField(max_length=500, null=True,blank=True)
|
||||
replaced_by = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='replaces', verbose_name="Remplacé par")
|
||||
marked_as_removed_date = models.DateTimeField(null=True, blank=True, verbose_name="Date de marquage comme retiré")
|
||||
marked_as_removed_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_marked_as_removed', verbose_name="Marqué comme retiré par")
|
||||
note = models.TextField(null=True, blank=True, verbose_name="Note")
|
||||
validation_status = models.CharField(max_length=50, choices=ASSET_VALIDATION_STATUS_CHOICES, null=True, blank=True, verbose_name="Statut de validation")
|
||||
funding_program = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Programme de financement / Projet"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_updated_by', verbose_name="Mis à jour par")
|
||||
|
||||
|
||||
asset_inspections = GenericRelation(
|
||||
'inspections.Inspection',
|
||||
content_type_field='asset_content_type',
|
||||
object_id_field='asset_object_id',
|
||||
related_query_name='%(app_label)s_%(class)s'
|
||||
)
|
||||
|
||||
@property
|
||||
def is_compliant(self):
|
||||
return self.inspection_status == 'compliant'
|
||||
|
||||
@is_compliant.setter
|
||||
def is_compliant(self, value):
|
||||
if value is True:
|
||||
self.inspection_status = 'compliant'
|
||||
elif value is False:
|
||||
self.inspection_status = 'non_compliant'
|
||||
|
||||
|
||||
|
||||
|
||||
def get_replacement_chain(self):
|
||||
"""Get the complete replacement chain starting from this asset."""
|
||||
chain = [self]
|
||||
current = self
|
||||
seen_ids = {self.id}
|
||||
|
||||
while current.replaced_by:
|
||||
if current.replaced_by.id in seen_ids: # Avoid infinite loops
|
||||
break
|
||||
chain.append(current.replaced_by)
|
||||
seen_ids.add(current.replaced_by.id)
|
||||
current = current.replaced_by
|
||||
|
||||
return chain
|
||||
|
||||
def get_original_asset(self):
|
||||
"""Get the first asset in the replacement chain."""
|
||||
original = self
|
||||
seen_ids = {self.id}
|
||||
|
||||
# Try to find an asset that replaces this one
|
||||
replacer = self.replaces.first()
|
||||
while replacer:
|
||||
if replacer.id in seen_ids: # Avoid infinite loops
|
||||
break
|
||||
original = replacer
|
||||
seen_ids.add(replacer.id)
|
||||
replacer = replacer.replaces.first()
|
||||
|
||||
return original
|
||||
|
||||
def get_parent_support(self):
|
||||
"""
|
||||
Returns the direct parent support object if this asset is mounted or attached to another asset.
|
||||
e.g., pole, controller, cabinet, parent_asset, etc.
|
||||
"""
|
||||
for attr in (
|
||||
'pole', 'trafficlight_pole', 'trafficlights_pole',
|
||||
'publiclighting_pole', 'lighting_pole', 'controller',
|
||||
'cabinet', 'parent_asset'
|
||||
):
|
||||
support = getattr(self, attr, None)
|
||||
if support is not None:
|
||||
return support
|
||||
return None
|
||||
|
||||
def get_location(self):
|
||||
"""
|
||||
Resolves the associated location object (Street, Intersection, StructureLocation, PublicBuilding, etc.)
|
||||
either directly from the asset or indirectly via its parent support.
|
||||
"""
|
||||
# 1. Direct location fields
|
||||
for attr in ('location', 'street', 'intersection', 'building', 'site', 'structure'):
|
||||
loc = getattr(self, attr, None)
|
||||
if loc is not None:
|
||||
if attr == 'structure' and hasattr(loc, 'main_location') and loc.main_location:
|
||||
return loc.main_location
|
||||
return loc
|
||||
|
||||
# 2. Indirectly via parent support
|
||||
parent_support = self.get_parent_support()
|
||||
if parent_support is not None:
|
||||
if hasattr(parent_support, 'get_location'):
|
||||
loc = parent_support.get_location()
|
||||
if loc is not None:
|
||||
return loc
|
||||
for attr in ('location', 'street', 'intersection', 'building', 'site', 'structure'):
|
||||
loc = getattr(parent_support, attr, None)
|
||||
if loc is not None:
|
||||
if attr == 'structure' and hasattr(loc, 'main_location') and loc.main_location:
|
||||
return loc.main_location
|
||||
return loc
|
||||
|
||||
return None
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr
|
||||
return name
|
||||
|
||||
def __str__(self):
|
||||
return self.code + " - " + self.get_name()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
# Abstract base model for geo assets
|
||||
class AbstractGeoAsset(GeoSyncMixin, AbstractAsset):
|
||||
lon = models.FloatField(null=True,blank=True)
|
||||
lat = models.FloatField(null=True,blank=True)
|
||||
geom = models.PointField(srid=3812, null=True,blank=True)
|
||||
geojson = models.TextField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
# ----------------------
|
||||
# ITS Models
|
||||
# ----------------------
|
||||
|
||||
from django.contrib.gis.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
|
||||
import os
|
||||
|
||||
from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel
|
||||
from common.models import Municipality, Document
|
||||
from common.storages import PrivateMediaStorage
|
||||
|
||||
private_storage = PrivateMediaStorage()
|
||||
|
||||
|
||||
|
||||
# ITS locations
|
||||
class ITSLocation(AbstractLocation):
|
||||
geom = models.MultiPolygonField(srid=3812, null=True,blank=True)
|
||||
parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations")
|
||||
is_main_location = models.BooleanField(default=False, verbose_name="Main Location")
|
||||
|
||||
def get_active_assets(self):
|
||||
def collect_assets(location):
|
||||
assets = list(location.assets.filter(status='active'))
|
||||
for child in location.child_locations.all():
|
||||
assets += collect_assets(child)
|
||||
return assets
|
||||
|
||||
return collect_assets(self)
|
||||
|
||||
def get_all_assets(self):
|
||||
def collect_assets(location):
|
||||
assets = list(location.assets.all())
|
||||
for child in location.child_locations.all():
|
||||
assets += collect_assets(child)
|
||||
return assets
|
||||
|
||||
return collect_assets(self)
|
||||
|
||||
def get_direct_child_assets(self):
|
||||
return self.assets.all()
|
||||
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'assets:its_locations_detail',
|
||||
kwargs={
|
||||
'location_id': self.id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "ITS Location"
|
||||
verbose_name_plural = "ITS Locations"
|
||||
ordering = ["code"]
|
||||
|
||||
|
||||
|
||||
def upload_to_its_location(instance, filename):
|
||||
location = instance.location
|
||||
code = slugify(location.code or "unknown") # Fallback si code est None
|
||||
return os.path.join("assets_documents", "its_locations", code, filename)
|
||||
|
||||
ITS_LOC_DOCUMENT_TYPE_CHOICES = (
|
||||
('as_built', 'As Built'),
|
||||
('schema', "Schéma"),
|
||||
('other', 'Autre'),
|
||||
)
|
||||
|
||||
# ITS Location Document
|
||||
class ITSLocationDocument(Document):
|
||||
location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated ITS Location")
|
||||
file = models.FileField(upload_to=upload_to_its_location, storage=private_storage, verbose_name="Fichier", max_length=255)
|
||||
thumbnail = models.ImageField(upload_to=upload_to_its_location, storage=private_storage, null=True, blank=True, max_length=255)
|
||||
document_type = models.CharField(max_length=50, choices=ITS_LOC_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document")
|
||||
|
||||
|
||||
class ITSAssetModel(AbstractAssetModel):
|
||||
class Meta:
|
||||
verbose_name = "ITS Asset Model"
|
||||
verbose_name_plural = "ITS Asset Models"
|
||||
|
||||
|
||||
class ITSGeoAsset(AbstractGeoAsset):
|
||||
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT)
|
||||
location = models.ForeignKey(ITSLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
trafficlights_pole = models.ForeignKey("assets.TrafficLightPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Traffic Light Pole")
|
||||
lighting_pole = models.ForeignKey("assets.PublicLightingPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Lighting Pole")
|
||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
support = self.trafficlights_pole or self.lighting_pole
|
||||
if support and getattr(support, 'geom', None):
|
||||
self.geom = support.geom
|
||||
self.lon = support.lon
|
||||
self.lat = support.lat
|
||||
self.geojson = support.geojson
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
support = self.trafficlights_pole or self.lighting_pole
|
||||
if support and getattr(support, 'geom', None):
|
||||
self.geom = support.geom
|
||||
self.lon = support.lon
|
||||
self.lat = support.lat
|
||||
self.geojson = support.geojson
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["code"]),
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsgeoasset', 'asset_id':self.id})
|
||||
|
||||
|
||||
BOLLARD_DIRECTION_CHOICES = (
|
||||
('unidirectional', _('Sens unique')),
|
||||
('bidirectional', _('Double sens')),
|
||||
)
|
||||
|
||||
class ITSBollardSystem(AbstractGeoAsset):
|
||||
"""
|
||||
Système / Traverse de bornes escamotables (représentation linéaire).
|
||||
Contrôle l'accès à une voie ou zone piétonne.
|
||||
"""
|
||||
geom = models.LineStringField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Ligne)"))
|
||||
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de système"))
|
||||
location = models.ForeignKey(ITSLocation, related_name="bollard_systems", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
||||
width = models.FloatField(null=True, blank=True, verbose_name=_("Largeur de passage (m)"))
|
||||
direction = models.CharField(max_length=50, choices=BOLLARD_DIRECTION_CHOICES, default='bidirectional', null=True, blank=True, verbose_name=_("Sens de circulation"))
|
||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||
associated_assets = models.ManyToManyField(
|
||||
'assets.ITSGeoAsset',
|
||||
through='assets.ITSBollardSystemAsset',
|
||||
related_name="bollard_systems",
|
||||
blank=True,
|
||||
verbose_name=_("Équipements associés")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Système de bornes")
|
||||
verbose_name_plural = _("Systèmes de bornes")
|
||||
indexes = [
|
||||
models.Index(fields=["code"]),
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardsystem', 'asset_id': self.id})
|
||||
|
||||
@property
|
||||
def plots_count(self):
|
||||
return self.plots.count()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_name()
|
||||
|
||||
|
||||
BOLLARD_TYPE_CHOICES = (
|
||||
('automatic', _('Automatique / Escamotable')),
|
||||
('semi_automatic', _('Semi-automatique')),
|
||||
('fixed', _('Fixe')),
|
||||
('removable', _('Amovible')),
|
||||
)
|
||||
|
||||
class ITSBollardPlot(AbstractGeoAsset):
|
||||
"""
|
||||
Plot / borne physique individuel sur un système de bornes.
|
||||
"""
|
||||
geom = models.PointField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Point)"))
|
||||
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="plots", verbose_name=_("Système de bornes"))
|
||||
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de plot"))
|
||||
position = models.PositiveSmallIntegerField(default=1, verbose_name=_("Position sur la traverse"), help_text=_("Ordre de gauche à droite (1, 2, 3...)"))
|
||||
bollard_type = models.CharField(max_length=50, choices=BOLLARD_TYPE_CHOICES, default='automatic', verbose_name=_("Type de plot"))
|
||||
location = models.ForeignKey(ITSLocation, related_name="bollard_plots", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_name()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.location_id and self.system and self.system.location_id:
|
||||
self.location = self.system.location
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.location_id and self.system and self.system.location_id:
|
||||
self.location = self.system.location
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Plot de borne")
|
||||
verbose_name_plural = _("Plots de bornes")
|
||||
ordering = ["system", "position"]
|
||||
indexes = [
|
||||
models.Index(fields=["code"]),
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardplot', 'asset_id': self.id})
|
||||
|
||||
|
||||
class ITSBollardSystemAsset(models.Model):
|
||||
"""
|
||||
Table de liaison ManyToMany entre un ITSBollardSystem et des ITSGeoAsset
|
||||
(caméras LAPI, totems, armoires électriques, feux bicolores, boucles...).
|
||||
"""
|
||||
class AssetRole(models.TextChoices):
|
||||
CAMERA_ENTRY = "camera_entry", _("Caméra entrée (LAPI)")
|
||||
CAMERA_EXIT = "camera_exit", _("Caméra sortie (LAPI)")
|
||||
CAMERA_CCTV = "camera_cctv", _("Caméra de contexte / surveillance")
|
||||
CABINET = "cabinet", _("Armoire électrique / automate")
|
||||
TOTEM = "totem", _("Totem de commande")
|
||||
TRAFFIC_LIGHT = "traffic_light", _("Feu de franchissement")
|
||||
LOOP = "loop", _("Boucle de détection")
|
||||
OTHER = "other", _("Autre")
|
||||
|
||||
bollard_system = models.ForeignKey(
|
||||
ITSBollardSystem,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="system_assets",
|
||||
verbose_name=_("Système de bornes")
|
||||
)
|
||||
asset = models.ForeignKey(
|
||||
ITSGeoAsset,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="bollard_system_links",
|
||||
verbose_name=_("Équipement ITS")
|
||||
)
|
||||
role = models.CharField(
|
||||
max_length=50,
|
||||
choices=AssetRole.choices,
|
||||
default=AssetRole.OTHER,
|
||||
verbose_name=_("Rôle de l'équipement")
|
||||
)
|
||||
comment = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Commentaire"))
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date d'association"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Équipement associé au système de bornes")
|
||||
verbose_name_plural = _("Équipements associés au système de bornes")
|
||||
unique_together = ('bollard_system', 'asset')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.bollard_system.code} - {self.asset.code} ({self.get_role_display()})"
|
||||
|
||||
|
||||
class ITSBollardSystemContract(models.Model):
|
||||
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
||||
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
||||
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
||||
maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.system.code} - {self.contract.contract_number}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Contrat de système de bornes")
|
||||
verbose_name_plural = _("Contrats de systèmes de bornes")
|
||||
unique_together = ('system', 'contract')
|
||||
|
||||
|
||||
|
||||
|
||||
class ITSLocationContract(models.Model):
|
||||
location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="its_locations_contracts")
|
||||
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="its_locations_contracts")
|
||||
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
||||
maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.location.code} - {self.contract.contract_number}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "ITS Location Contract"
|
||||
verbose_name_plural = "ITS Location Contracts"
|
||||
unique_together = ('location', 'contract')
|
||||
|
||||
|
||||
class ITSGeoAssetContract(models.Model):
|
||||
asset = models.ForeignKey(ITSGeoAsset, on_delete=models.CASCADE, related_name="its_assets_contracts")
|
||||
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="its_assets_contracts")
|
||||
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
||||
maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.asset.code} - {self.contract.contract_number}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "ITS Asset Contract"
|
||||
verbose_name_plural = "ITS Asset Contracts"
|
||||
unique_together = ('asset', 'contract')
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _, get_language
|
||||
import json
|
||||
|
||||
from common.models import Thematic, Document
|
||||
from common.storages import PrivateMediaStorage
|
||||
|
||||
private_storage = PrivateMediaStorage()
|
||||
|
||||
|
||||
class ManagementZoneType(models.Model):
|
||||
"""
|
||||
Typologie de zone de gestion spécifique à une thématique
|
||||
(ex: Fauche tardive / refuge biodiversité, Tonte d'agrément, Onde verte coordonnée, etc.).
|
||||
"""
|
||||
code = models.CharField(max_length=100, unique=True, verbose_name=_("Code"))
|
||||
name_fr = models.CharField(max_length=255, verbose_name=_("Nom (FR)"))
|
||||
name_nl = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Nom (NL)"))
|
||||
thematic = models.ForeignKey(
|
||||
Thematic,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="management_zone_types",
|
||||
verbose_name=_("Thématique")
|
||||
)
|
||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
||||
default_color = models.CharField(max_length=20, default="#198754", verbose_name=_("Couleur par défaut"))
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
if lang and lang.startswith("nl") and self.name_nl:
|
||||
return self.name_nl
|
||||
return self.name_fr
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_name()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_name()} ({self.thematic.name})"
|
||||
|
||||
class Meta:
|
||||
ordering = ["thematic", "name_fr"]
|
||||
verbose_name = _("Type de zone de gestion")
|
||||
verbose_name_plural = _("Types de zones de gestion")
|
||||
|
||||
|
||||
class ManagementZone(models.Model):
|
||||
"""
|
||||
Zone de gestion générique avec géométrie polygonale.
|
||||
Regroupe des équipements/assets selon une logique métier, d'exploitation ou de gestion écologique.
|
||||
"""
|
||||
code = models.CharField(max_length=100, unique=True, verbose_name=_("Code"))
|
||||
name_fr = models.CharField(max_length=255, verbose_name=_("Nom (FR)"))
|
||||
name_nl = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Nom (NL)"))
|
||||
thematic = models.ForeignKey(
|
||||
Thematic,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="management_zones",
|
||||
verbose_name=_("Thématique")
|
||||
)
|
||||
zone_type = models.ForeignKey(
|
||||
ManagementZoneType,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="management_zones",
|
||||
verbose_name=_("Type de zone de gestion")
|
||||
)
|
||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
||||
instructions = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Consignes et points d'attention"),
|
||||
help_text=_("Consignes d'exploitation, points de vigilance, règles de sécurité...")
|
||||
)
|
||||
geom = models.MultiPolygonField(
|
||||
srid=3812,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Périmètre géographique")
|
||||
)
|
||||
color = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Couleur d'affichage"),
|
||||
help_text=_("Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone.")
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
target_frequency_days = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Fréquence cible (jours)"),
|
||||
help_text=_("Intervalle indicatif entre interventions en jours.")
|
||||
)
|
||||
seasonality_notes = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Saisonnalité / Calendrier"),
|
||||
help_text=_("Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps.")
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date de création"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Date de mise à jour"))
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="created_management_zones",
|
||||
verbose_name=_("Créé par")
|
||||
)
|
||||
|
||||
def get_color(self):
|
||||
if self.color:
|
||||
return self.color
|
||||
if self.zone_type and self.zone_type.default_color:
|
||||
return self.zone_type.default_color
|
||||
return "#198754"
|
||||
|
||||
@property
|
||||
def geojson_geometry(self):
|
||||
"""
|
||||
Retourne la géométrie GeoJSON transformée en WGS84 (EPSG:4326)
|
||||
pour utilisation directe dans MapLibre / Leaflet.
|
||||
"""
|
||||
if not self.geom:
|
||||
return None
|
||||
try:
|
||||
geom_4326 = self.geom.transform(4326, clone=True)
|
||||
return json.loads(geom_4326.geojson)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
if lang and lang.startswith("nl") and self.name_nl:
|
||||
return self.name_nl
|
||||
return self.name_fr
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_name()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} - {self.get_name()}"
|
||||
|
||||
class Meta:
|
||||
ordering = ["thematic", "code"]
|
||||
verbose_name = _("Zone de gestion")
|
||||
verbose_name_plural = _("Zones de gestion")
|
||||
|
||||
|
||||
class ManagementZoneAsset(models.Model):
|
||||
"""
|
||||
Liaison explicite (inclusion ou exclusion) d'un asset à une zone de gestion.
|
||||
Permet d'affiner le rattachement au-delà de la stricte inclusion spatiale.
|
||||
"""
|
||||
management_zone = models.ForeignKey(
|
||||
ManagementZone,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="explicit_assets",
|
||||
verbose_name=_("Zone de gestion")
|
||||
)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("Type d'asset"))
|
||||
object_id = models.PositiveIntegerField(verbose_name=_("ID de l'asset"))
|
||||
asset = GenericForeignKey('content_type', 'object_id')
|
||||
is_excluded = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Exclu"),
|
||||
help_text=_("Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre.")
|
||||
)
|
||||
notes = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Notes"))
|
||||
|
||||
def __str__(self):
|
||||
status = "Exclu" if self.is_excluded else "Inclus"
|
||||
return f"{self.management_zone.code} -> {self.content_type.model} #{self.object_id} ({status})"
|
||||
|
||||
class Meta:
|
||||
unique_together = ('management_zone', 'content_type', 'object_id')
|
||||
verbose_name = _("Asset de zone de gestion")
|
||||
verbose_name_plural = _("Assets de zones de gestion")
|
||||
|
||||
|
||||
class ManagementZoneDocument(Document):
|
||||
"""
|
||||
Documents, fiches techniques et arrêtés associés à une zone de gestion.
|
||||
"""
|
||||
zone = models.ForeignKey(
|
||||
ManagementZone,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="documents",
|
||||
verbose_name=_("Zone de gestion")
|
||||
)
|
||||
file = models.FileField(
|
||||
upload_to="assets_documents/management_zones/",
|
||||
storage=private_storage,
|
||||
verbose_name=_("Fichier"),
|
||||
max_length=255
|
||||
)
|
||||
thumbnail = models.ImageField(
|
||||
upload_to="assets_documents/management_zones/",
|
||||
storage=private_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=255
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Document de zone de gestion")
|
||||
verbose_name_plural = _("Documents de zones de gestion")
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
# -----------------------------
|
||||
# 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)"))
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||
self.geom = self.parent_asset.geom
|
||||
self.lon = self.parent_asset.lon
|
||||
self.lat = self.parent_asset.lat
|
||||
self.geojson = self.parent_asset.geojson
|
||||
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||
self.location = self.parent_asset.location
|
||||
if self.parent_asset and self.parent_asset.site and not self.site_id:
|
||||
self.site = self.parent_asset.site
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||
self.geom = self.parent_asset.geom
|
||||
self.lon = self.parent_asset.lon
|
||||
self.lat = self.parent_asset.lat
|
||||
self.geojson = self.parent_asset.geojson
|
||||
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||
self.location = self.parent_asset.location
|
||||
if self.parent_asset and self.parent_asset.site and not self.site_id:
|
||||
self.site = self.parent_asset.site
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
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")
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.models import AbstractLocation, AbstractGeoAsset
|
||||
from assets.models.sign import SignStreet, SignPole, SignPanel
|
||||
|
||||
|
||||
SPOT_TYPE_CHOICES = (
|
||||
('bicycle', _('Stationnement vélo')),
|
||||
('scooter_drop_zone', _('Drop zone trottinettes / EDPM')),
|
||||
('cargo_bike', _('Stationnement vélo cargo')),
|
||||
('delivery', _('Zone de livraison / Chargement-déchargement')),
|
||||
('pmr', _('Emplacement PMR')),
|
||||
('kiss_and_ride', _('Arrêt minute / Court-durée (CD)')),
|
||||
('car_parking', _('Stationnement automobile')),
|
||||
('carsharing', _('Autopartage')),
|
||||
('ev_charging', _('Recharge véhicule électrique')),
|
||||
('other', _('Autre')),
|
||||
)
|
||||
|
||||
STAND_TYPE_CHOICES = (
|
||||
('normal', _('Arceau standard (U inversé)')),
|
||||
('cargo', _('Arceau vélo cargo')),
|
||||
('mixed', _('Mixte (normal et cargo)')),
|
||||
('rack', _('Râteau / Pince-roue')),
|
||||
('box', _('Box / Abri sécurisé')),
|
||||
('other', _('Autre')),
|
||||
)
|
||||
|
||||
MARKING_QUALITY_CHOICES = (
|
||||
('good', _('Bon état')),
|
||||
('medium', _('Moyen')),
|
||||
('damaged', _('Dégradé')),
|
||||
('missing', _('Absent / Effacé')),
|
||||
)
|
||||
|
||||
|
||||
class ParkingLocation(AbstractLocation):
|
||||
"""
|
||||
Modèle représentant un secteur ou zone d'ensemble de stationnement.
|
||||
"""
|
||||
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise cartographique"))
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
|
||||
address = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Adresse"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Zone de stationnement")
|
||||
verbose_name_plural = _("Zones de stationnement")
|
||||
ordering = ['code', 'name_fr']
|
||||
|
||||
|
||||
class ParkingSpot(AbstractGeoAsset):
|
||||
"""
|
||||
Modèle représentant un emplacement ou zone de stationnement vélo/trottinette/PMR/livraison.
|
||||
Utilise une géométrie en Polygone.
|
||||
"""
|
||||
geom = models.PolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise polygone"))
|
||||
location = models.ForeignKey(
|
||||
ParkingLocation,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='parking_spots',
|
||||
verbose_name=_("Zone / Localisation parente")
|
||||
)
|
||||
street = models.ForeignKey(
|
||||
SignStreet,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='parking_spots_new',
|
||||
verbose_name=_("Rue")
|
||||
)
|
||||
spot_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=SPOT_TYPE_CHOICES,
|
||||
default='bicycle',
|
||||
verbose_name=_("Type d'emplacement")
|
||||
)
|
||||
|
||||
# Métriques et types d'arceaux vélos
|
||||
standard_stand_count = models.IntegerField(default=0, verbose_name=_("Nombre d'arceaux vélos classiques"))
|
||||
cargo_stand_count = models.IntegerField(default=0, verbose_name=_("Nombre d'arceaux vélos cargo"))
|
||||
drop_off_stand_count = models.IntegerField(default=0, verbose_name=_("Nombre d'arceaux drop-off"))
|
||||
other_stand_count = models.IntegerField(default=0, verbose_name=_("Autres arceaux / agencements"))
|
||||
stand_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=STAND_TYPE_CHOICES,
|
||||
default='normal',
|
||||
verbose_name=_("Type d'arceau principal")
|
||||
)
|
||||
has_scooter_drop_zone = models.BooleanField(default=False, verbose_name=_("Drop zone trottinettes / EDPM"))
|
||||
|
||||
# Métriques générales & stationnement auto/livraison/PMR
|
||||
capacity = models.IntegerField(null=True, blank=True, verbose_name=_("Capacité générale (nbr véhicules)"))
|
||||
is_paid = models.BooleanField(default=False, verbose_name=_("Payant"))
|
||||
time_limit_minutes = models.IntegerField(null=True, blank=True, verbose_name=_("Durée maximale (minutes)"))
|
||||
marking_quality = models.CharField(
|
||||
max_length=50,
|
||||
choices=MARKING_QUALITY_CHOICES,
|
||||
default='good',
|
||||
verbose_name=_("Qualité du marquage au sol")
|
||||
)
|
||||
is_temporary = models.BooleanField(default=False, verbose_name=_("Temporaire (chantier/événement)"))
|
||||
|
||||
# Conformité & résultat d'inspection
|
||||
is_compliant = models.BooleanField(default=True, verbose_name=_("Conforme aux spécifications"))
|
||||
non_compliance_reason = models.TextField(null=True, blank=True, verbose_name=_("Motif de non-conformité"))
|
||||
|
||||
# Signalisation associée
|
||||
poles = models.ManyToManyField(
|
||||
SignPole,
|
||||
related_name='parking_spots_attached_new',
|
||||
blank=True,
|
||||
verbose_name=_("Poteaux de signalisation associés")
|
||||
)
|
||||
panels = models.ManyToManyField(
|
||||
SignPanel,
|
||||
related_name='parking_spots_attached_new',
|
||||
blank=True,
|
||||
verbose_name=_("Panneaux de signalisation associés")
|
||||
)
|
||||
|
||||
@property
|
||||
def total_stand_count(self):
|
||||
"""Retourne le nombre total d'arceaux (classiques + cargo + drop-off + autres)."""
|
||||
return (self.standard_stand_count or 0) + (self.cargo_stand_count or 0) + (self.drop_off_stand_count or 0) + (self.other_stand_count or 0)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Emplacement de stationnement")
|
||||
verbose_name_plural = _("Emplacements de stationnement")
|
||||
ordering = ['code', 'name_fr']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:parking_assets_detail', kwargs={'asset_model': 'parking_spot', 'asset_id': self.id})
|
||||
|
||||
def __str__(self):
|
||||
type_label = self.get_spot_type_display()
|
||||
name = self.get_name()
|
||||
if name:
|
||||
return f"{self.code} - {type_label} ({name})"
|
||||
return f"{self.code} - {type_label}"
|
||||
|
|
@ -1,418 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.gis.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import os
|
||||
|
||||
|
||||
from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel
|
||||
from common.models import Municipality, Document
|
||||
from common.storages import PrivateMediaStorage
|
||||
|
||||
private_storage = PrivateMediaStorage()
|
||||
|
||||
|
||||
|
||||
STRUCTURE_TYPE_CHOICES = (
|
||||
('tunnel', _('Tunnel')),
|
||||
('bridge', _('Pont')),
|
||||
('viaduct', _('Viaduc')),
|
||||
('overpass', _('Passerelle')),
|
||||
('underpass', _('Passage souterrain')),
|
||||
('gallery', _('Galerie technique')),
|
||||
('atypical', _('Ouvrage atypique')),
|
||||
('metro_station', _('Station de métro')),
|
||||
('metro_tunnel', _('Tunnel de métro')),
|
||||
)
|
||||
|
||||
|
||||
# Structure locations
|
||||
class StructureLocation(AbstractLocation):
|
||||
geom = models.MultiPolygonField(srid=3812, null=True,blank=True)
|
||||
parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations")
|
||||
is_main_location = models.BooleanField(default=False, verbose_name="Main Location")
|
||||
structure = models.ForeignKey('Structure', on_delete=models.SET_NULL, null=True, blank=True, related_name="all_locations")
|
||||
|
||||
def get_active_assets(self):
|
||||
def collect_assets(location):
|
||||
assets = list(location.assets.filter(status='active'))
|
||||
for child in location.child_locations.all():
|
||||
assets += collect_assets(child)
|
||||
return assets
|
||||
|
||||
return collect_assets(self)
|
||||
|
||||
def get_all_assets(self):
|
||||
def collect_assets(location):
|
||||
assets = list(location.assets.all())
|
||||
for child in location.child_locations.all():
|
||||
assets += collect_assets(child)
|
||||
return assets
|
||||
|
||||
return collect_assets(self)
|
||||
|
||||
def get_direct_child_assets(self):
|
||||
return self.assets.all()
|
||||
|
||||
def clean(self):
|
||||
if self.is_main_location and StructureLocation.objects.filter(structure=self.structure, is_main_location=True).exclude(pk=self.pk).exists():
|
||||
raise ValidationError("Cette structure a déjà une localisation principale.")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'assets:structures_locations_detail',
|
||||
kwargs={
|
||||
# 'asset_model': content_type.model,
|
||||
'location_id': self.id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Structure Location"
|
||||
verbose_name_plural = "Structure Locations"
|
||||
ordering = ["code"]
|
||||
|
||||
|
||||
class Structure(models.Model):
|
||||
code = models.CharField(max_length=50, unique=True)
|
||||
name_fr = models.CharField(max_length=200,null=True,blank=True)
|
||||
name_nl = models.CharField(max_length=200,null=True,blank=True)
|
||||
structure_type = models.CharField(max_length=200, choices=STRUCTURE_TYPE_CHOICES, null=True, blank=True)
|
||||
main_location = models.OneToOneField(StructureLocation, on_delete=models.SET_NULL, null=True, blank=True, related_name="main_structure")
|
||||
description = models.TextField(null=True, blank=True)
|
||||
linked_folder = models.CharField(max_length=1024, null=True, blank=True)
|
||||
structure_number = models.IntegerField(null=True, blank=True, verbose_name="Numéro de l'ouvrage")
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr
|
||||
return name
|
||||
|
||||
def get_active_assets(self):
|
||||
return self.assets.filter(status='active')
|
||||
|
||||
def get_all_assets(self):
|
||||
return self.assets.all()
|
||||
|
||||
|
||||
def get_all_locations(self):
|
||||
return self.all_locations.all()
|
||||
|
||||
def get_main_location(self):
|
||||
return self.main_location
|
||||
|
||||
def __str__(self):
|
||||
return self.code + " - " + self.get_name()
|
||||
|
||||
class Meta:
|
||||
ordering = ["code"]
|
||||
|
||||
|
||||
def upload_to_structure(instance, filename):
|
||||
location = instance.structure
|
||||
code = slugify(location.code or "unknown") # Fallback si code est None
|
||||
return os.path.join("assets_documents", "structures", code, filename)
|
||||
|
||||
TU_DOCUMENT_TYPE_CHOICES = (
|
||||
('as_built', 'As Built'),
|
||||
('cme', "CME"),
|
||||
('inspection_ce', 'Inspection G.C.'),
|
||||
('inspection_em', 'Inspection E.M.'),
|
||||
('pis', 'PIS'),
|
||||
('other', 'Autre'),
|
||||
)
|
||||
|
||||
# Structure Location Document
|
||||
class StructureDocument(Document):
|
||||
structure = models.ForeignKey(Structure, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Structure")
|
||||
file = models.FileField(upload_to=upload_to_structure, storage=private_storage, verbose_name="Fichier", max_length=255)
|
||||
thumbnail = models.ImageField(upload_to=upload_to_structure, storage=private_storage, null=True, blank=True, max_length=255)
|
||||
document_type = models.CharField(max_length=50, choices=TU_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document")
|
||||
|
||||
|
||||
SC_MAINTENANCE_TYPE_CHOICES = (
|
||||
('electromeca', 'Eléctromécanique'),
|
||||
('civil_engineering', 'Génie civil'),
|
||||
('urgent_cleaning', 'Nettoyage urgent'),
|
||||
('other', 'Autre'),
|
||||
)
|
||||
|
||||
class StructureContract(models.Model):
|
||||
structure = models.ForeignKey(Structure, on_delete=models.CASCADE, related_name="contracts")
|
||||
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="structure_contracts")
|
||||
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
||||
maintenance_type = models.CharField(max_length=50, choices=SC_MAINTENANCE_TYPE_CHOICES, default='other')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.structure.code} - {self.contract.contract_number}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Structure Contract"
|
||||
verbose_name_plural = "Structure Contracts"
|
||||
unique_together = ('structure', 'contract')
|
||||
|
||||
|
||||
class StructureAssetModel(AbstractAssetModel):
|
||||
class Meta:
|
||||
verbose_name = "Structure Asset Model"
|
||||
verbose_name_plural = "Structure Asset Models"
|
||||
|
||||
|
||||
class StructureGeoAsset(AbstractGeoAsset):
|
||||
model = models.ForeignKey(StructureAssetModel, on_delete=models.PROTECT)
|
||||
location = models.ForeignKey(StructureLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
structure = models.ForeignKey(Structure, on_delete=models.SET_NULL, related_name="assets", null=True, blank=True)
|
||||
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets")
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||
self.geom = self.parent_asset.geom
|
||||
self.lon = self.parent_asset.lon
|
||||
self.lat = self.parent_asset.lat
|
||||
self.geojson = self.parent_asset.geojson
|
||||
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||
self.location = self.parent_asset.location
|
||||
if self.parent_asset and self.parent_asset.structure and not self.structure_id:
|
||||
self.structure = self.parent_asset.structure
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||
self.geom = self.parent_asset.geom
|
||||
self.lon = self.parent_asset.lon
|
||||
self.lat = self.parent_asset.lat
|
||||
self.geojson = self.parent_asset.geojson
|
||||
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||
self.location = self.parent_asset.location
|
||||
if self.parent_asset and self.parent_asset.structure and not self.structure_id:
|
||||
self.structure = self.parent_asset.structure
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["code"]),
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:structures_assets_detail', kwargs={'asset_model': 'structuregeoasset', 'asset_id':self.id})
|
||||
|
||||
|
||||
TE_PASSAGE_STATUS_CHOICES = (
|
||||
('authorized', _('Autorisé sans condition particulière')),
|
||||
('conditional', _('Autorisé sous conditions / prescriptions')),
|
||||
('prior_study_required', _('Étude / calcul préalable obligatoire')),
|
||||
('forbidden', _('Strictement interdit')),
|
||||
('unknown', _('Non évalué')),
|
||||
)
|
||||
|
||||
TE_LANE_POSITIONING_CHOICES = (
|
||||
('standard', _('Voie de circulation normale')),
|
||||
('center_axis', _('Dans l\'axe de l\'ouvrage / ligne médiane')),
|
||||
('first_lane', _('Première bande de circulation')),
|
||||
('tram_tracks', _('Sur les voies de tram')),
|
||||
('specific_path', _('Trajectoire spécifique (voir prescriptions)')),
|
||||
)
|
||||
|
||||
|
||||
class StructureExceptionalTransport(models.Model):
|
||||
structure = models.OneToOneField(
|
||||
Structure,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='exceptional_transport_info',
|
||||
verbose_name=_("Ouvrage associé")
|
||||
)
|
||||
|
||||
# --- Références administratives & Classification SPF / Région ---
|
||||
fod_vnr = models.IntegerField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Numéro d'ordre FOD/SPF (VNR)"),
|
||||
help_text=_("Volgnummer pont dans la base fédérale")
|
||||
)
|
||||
national_id = models.CharField(
|
||||
max_length=50, null=True, blank=True,
|
||||
verbose_name=_("Identifiant national (IDN)")
|
||||
)
|
||||
bridge_class = models.CharField(
|
||||
max_length=100, null=True, blank=True,
|
||||
verbose_name=_("Classe de charge du pont (BKL)"),
|
||||
help_text=_("Ex: T180152402024030, D18015..., T09015")
|
||||
)
|
||||
is_railway_metro_crossing = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Ouvrage ferroviaire / métro / tram (Spoorlijn)"),
|
||||
help_text=_("Indique si l'ouvrage surplombe ou supporte des voies ferrées/métro/tram")
|
||||
)
|
||||
number_of_spans = models.PositiveSmallIntegerField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Nombre de travées (OVS)")
|
||||
)
|
||||
|
||||
# --- Capacités portantes & Tonnages ---
|
||||
max_authorized_weight_tonnes = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Poids max. autorisé standard (MAXWA) [t]"),
|
||||
help_text=_("Poids maximal standard sans étude particulière (ex: 44 t)")
|
||||
)
|
||||
min_guaranteed_capacity_tonnes = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Capacité minimale garantie (MINWR) [t]")
|
||||
)
|
||||
max_calculated_weight_tonnes = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Tonnage maximal admissible calculé (MX) [t]"),
|
||||
help_text=_("Charge maximale absolue évaluée pour l'ouvrage (ex: 140t, 265t, 450t)")
|
||||
)
|
||||
max_axle_load_tonnes = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Charge max. par essieu [t/essieu]"),
|
||||
help_text=_("Ex: 12.0, 12.8, 13.0 t/essieu")
|
||||
)
|
||||
min_axle_spacing_m = models.DecimalField(
|
||||
max_digits=4, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Entraxe minimal entre essieux [m]")
|
||||
)
|
||||
|
||||
# --- Gabarits géométriques & Hauteurs libres ---
|
||||
clearance_height_left_m = models.DecimalField(
|
||||
max_digits=4, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Hauteur libre gauche / entrée (H1) [m]")
|
||||
)
|
||||
clearance_height_center_m = models.DecimalField(
|
||||
max_digits=4, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Hauteur libre dans l'axe (H2) [m]")
|
||||
)
|
||||
clearance_height_right_m = models.DecimalField(
|
||||
max_digits=4, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Hauteur libre droite / sortie (H3) [m]")
|
||||
)
|
||||
usable_width_m = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Largeur utile roulable (BRE) [m]")
|
||||
)
|
||||
|
||||
# --- Prescriptions de franchissement & Exploitation ---
|
||||
passage_status = models.CharField(
|
||||
max_length=30,
|
||||
choices=TE_PASSAGE_STATUS_CHOICES,
|
||||
default='unknown',
|
||||
verbose_name=_("Statut d'admissibilité TE")
|
||||
)
|
||||
max_speed_kmh = models.PositiveIntegerField(
|
||||
default=5, null=True, blank=True,
|
||||
verbose_name=_("Vitesse maximale de franchissement (km/h)"),
|
||||
help_text=_("Généralement 5 km/h pour supprimer les effets dynamiques")
|
||||
)
|
||||
is_braking_forbidden = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Interdiction formelle de freiner sur l'ouvrage"),
|
||||
help_text=_("Notamment sur les dalles de toit de métro")
|
||||
)
|
||||
is_isolated_crossing_only = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Convoi seul sur l'ouvrage"),
|
||||
help_text=_("Aucune autre charge mobile simultanée autorisée sur l'ouvrage")
|
||||
)
|
||||
safety_distance_m = models.PositiveIntegerField(
|
||||
default=5, null=True, blank=True,
|
||||
verbose_name=_("Distance de sécurité / zone tampon (m)"),
|
||||
help_text=_("Distance minimale avec tout autre véhicule (souvent 5m)")
|
||||
)
|
||||
traffic_interruption_required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Interruption de circulation requise pendant le passage")
|
||||
)
|
||||
lane_positioning = models.CharField(
|
||||
max_length=30,
|
||||
choices=TE_LANE_POSITIONING_CHOICES,
|
||||
default='standard',
|
||||
verbose_name=_("Positionnement sur la chaussée")
|
||||
)
|
||||
is_escort_required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Escorte policière ou accompagnement requis")
|
||||
)
|
||||
|
||||
# --- Prescriptions textuelles bilingues ---
|
||||
prescriptions_fr = models.TextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Prescriptions techniques (FR)")
|
||||
)
|
||||
prescriptions_nl = models.TextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Prescriptions techniques (NL)")
|
||||
)
|
||||
|
||||
# --- Suivi & Métadonnées ---
|
||||
last_study_date = models.DateField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Date de la dernière étude / note de calcul")
|
||||
)
|
||||
last_study_ref = models.CharField(
|
||||
max_length=150, null=True, blank=True,
|
||||
verbose_name=_("Référence de la dernière note de calcul / Mission")
|
||||
)
|
||||
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
related_name='updated_structure_te_infos'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Transport exceptionnel (Ouvrage)")
|
||||
verbose_name_plural = _("Transports exceptionnels (Ouvrages)")
|
||||
|
||||
def __str__(self):
|
||||
return f"TE - {self.structure.code} ({self.get_passage_status_display()})"
|
||||
|
||||
|
||||
class StructureExceptionalTransportNotice(models.Model):
|
||||
RESULT_CHOICES = (
|
||||
('positive', _('Favorable / Positif')),
|
||||
('conditional', _('Favorable sous conditions')),
|
||||
('negative', _('Défavorable / Négatif')),
|
||||
)
|
||||
|
||||
structure = models.ForeignKey(
|
||||
Structure,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='exceptional_transport_notices',
|
||||
verbose_name=_("Ouvrage")
|
||||
)
|
||||
mission_reference = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_("Référence mission / Avis DITP"),
|
||||
help_text=_("Ex: Mission 64, Avis DITP 468830")
|
||||
)
|
||||
notice_date = models.DateField(null=True, blank=True, verbose_name=_("Date de l'avis"))
|
||||
result = models.CharField(max_length=20, choices=RESULT_CHOICES, default='positive', verbose_name=_("Résultat"))
|
||||
|
||||
evaluated_total_weight_tonnes = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Tonnage évalué [t]")
|
||||
)
|
||||
evaluated_axle_load_tonnes = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True,
|
||||
verbose_name=_("Charge par essieu évaluée [t/essieu]")
|
||||
)
|
||||
comments = models.TextField(blank=True, verbose_name=_("Conditions / Remarques de l'avis"))
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Avis / Mission d'étude TE")
|
||||
verbose_name_plural = _("Avis / Missions d'études TE")
|
||||
ordering = ['-notice_date', '-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.structure.code} - {self.mission_reference} ({self.get_result_display()})"
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="glassDome" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#22c55e"/>
|
||||
<stop offset="40%" stop-color="#16a34a"/>
|
||||
<stop offset="100%" stop-color="#15803d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="glassBase" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#14532d"/>
|
||||
<stop offset="100%" stop-color="#052e16"/>
|
||||
</linearGradient>
|
||||
<filter id="glassShadow" x="-15%" y="-15%" width="130%" height="135%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Top crane lifting ring (anneau de préhension) -->
|
||||
<circle cx="32" cy="11" r="5.5" fill="none" stroke="#475569" stroke-width="3" filter="url(#glassShadow)"/>
|
||||
<rect x="29.5" y="14" width="5" height="4" fill="#334155"/>
|
||||
|
||||
<!-- Base rim / skirt -->
|
||||
<rect x="8" y="51" width="48" height="6" rx="2.5" fill="url(#glassBase)" filter="url(#glassShadow)"/>
|
||||
|
||||
<!-- Main Igloo / Bell Dome Body -->
|
||||
<path d="M 10 51 C 10 27, 19 16, 32 16 C 45 16, 54 27, 54 51 Z"
|
||||
fill="url(#glassDome)" stroke="#166534" stroke-width="1.5" filter="url(#glassShadow)"/>
|
||||
|
||||
<!-- Dome highlight curve -->
|
||||
<path d="M 14 49 C 14 29, 21 19, 32 19" fill="none" stroke="#86efac" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
|
||||
|
||||
<!-- Left bottle drop aperture (orifice bouteilles) -->
|
||||
<circle cx="21" cy="36" r="6" fill="#0f172a" stroke="#f1f5f9" stroke-width="1.8"/>
|
||||
<circle cx="21" cy="36" r="4" fill="#020617"/>
|
||||
|
||||
<!-- Right bottle drop aperture (orifice bouteilles) -->
|
||||
<circle cx="43" cy="36" r="6" fill="#0f172a" stroke="#f1f5f9" stroke-width="1.8"/>
|
||||
<circle cx="43" cy="36" r="4" fill="#020617"/>
|
||||
|
||||
<!-- Center white glass bottle emblem -->
|
||||
<!-- Cap / lip -->
|
||||
<rect x="30" y="27" width="4" height="1.5" rx="0.5" fill="#ffffff"/>
|
||||
<!-- Neck -->
|
||||
<rect x="30.5" y="28.5" width="3" height="5" rx="0.5" fill="#ffffff"/>
|
||||
<!-- Shoulder & Body -->
|
||||
<path d="M 30.5 33.5 C 28.5 35.5, 28 38, 28 40 L 28 46 C 28 47.5, 36 47.5, 36 46 L 36 40 C 36 38, 35.5 35.5, 33.5 33.5 Z" fill="#ffffff"/>
|
||||
<!-- Bottle shine -->
|
||||
<line x1="30" y1="38" x2="30" y2="44" stroke="#86efac" stroke-width="1" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
|
@ -1,36 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<ellipse cx="32" cy="54" rx="22" ry="5" fill="#1e293b" opacity="0.3"/>
|
||||
<ellipse cx="32" cy="52" rx="18" ry="4" fill="#334155"/>
|
||||
<ellipse cx="32" cy="51" rx="16" ry="3.5" fill="#64748b"/>
|
||||
<ellipse cx="32" cy="50.5" rx="13" ry="2.8" fill="#1e293b"/>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="bollardGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#0284c7"/>
|
||||
<stop offset="25%" stop-color="#38bdf8"/>
|
||||
<stop offset="60%" stop-color="#0284c7"/>
|
||||
<stop offset="100%" stop-color="#0369a1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="capGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#7dd3fc"/>
|
||||
<stop offset="100%" stop-color="#0284c7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bandGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#f59e0b"/>
|
||||
<stop offset="35%" stop-color="#fef08a"/>
|
||||
<stop offset="70%" stop-color="#f59e0b"/>
|
||||
<stop offset="100%" stop-color="#d97706"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<path d="M 21,24 L 21,50 C 21,53 43,53 43,50 L 43,24 Z" fill="url(#bollardGrad)"/>
|
||||
<path d="M 21,21 L 21,27 C 21,29.5 43,29.5 43,27 L 43,21 C 43,23.5 21,23.5 21,21 Z" fill="url(#bandGrad)"/>
|
||||
<path d="M 21,15 L 21,21 C 21,23.5 43,23.5 43,21 L 43,15 Z" fill="url(#bollardGrad)"/>
|
||||
|
||||
<ellipse cx="32" cy="15" rx="11" ry="3.5" fill="url(#capGrad)" stroke="#bae6fd" stroke-width="0.8"/>
|
||||
<ellipse cx="32" cy="14.2" rx="7" ry="2" fill="#ffffff" opacity="0.6"/>
|
||||
|
||||
<circle cx="26" cy="24.5" r="1" fill="#ffffff"/>
|
||||
<circle cx="32" cy="25.2" r="1.1" fill="#ffffff"/>
|
||||
<circle cx="38" cy="24.5" r="1" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,405 +0,0 @@
|
|||
/**
|
||||
* Management Zones Map Overlay & Layer Switcher Integration
|
||||
*
|
||||
* Permet d'afficher en surimpression les zones de gestion (gestion écologique,
|
||||
* ondes vertes, secteurs prioritaires, etc.) sur la carte MapLibre
|
||||
* et ajoute une section dédiée "Zones de gestion" dans le LayerSwitcherControl.
|
||||
*/
|
||||
|
||||
(function (window) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Calcule la bounding box [[minX, minY], [maxX, maxY]] d'une géométrie GeoJSON.
|
||||
*/
|
||||
function getGeometryBBox(geometry) {
|
||||
if (!geometry || !geometry.coordinates) return null;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
function traverse(coords) {
|
||||
if (typeof coords[0] === 'number') {
|
||||
const [x, y] = coords;
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
} else if (Array.isArray(coords)) {
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
traverse(coords[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(geometry.coordinates);
|
||||
if (minX === Infinity || minY === Infinity) return null;
|
||||
return [[minX, minY], [maxX, maxY]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise l'overlay des zones de gestion sur la carte MapLibre.
|
||||
*/
|
||||
async function initManagementZonesMapOverlay(options = {}) {
|
||||
const map = options.map;
|
||||
if (!map) {
|
||||
console.warn('[ManagementZonesMapOverlay] Map instance missing.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!map.loaded()) {
|
||||
map.once('load', () => initManagementZonesMapOverlay(options));
|
||||
return;
|
||||
}
|
||||
|
||||
const thematic = options.thematic || 'nature';
|
||||
const geojsonUrl = options.geojsonUrl || '/assets/api/management-zones/geojson/';
|
||||
const layerSwitcher = options.layerSwitcher || window.layerSwitcher;
|
||||
const beforeLayerId = options.beforeLayerId || null;
|
||||
|
||||
const _t = (key, fallback) => (typeof gettext === 'function' ? gettext(key) : (fallback || key));
|
||||
|
||||
try {
|
||||
const url = new URL(geojsonUrl, window.location.origin);
|
||||
url.searchParams.set('thematic', thematic);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ManagementZonesMapOverlay] Failed to fetch geojson:', response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const geojson = await response.json();
|
||||
if (!geojson || geojson.type !== 'FeatureCollection') {
|
||||
console.warn('[ManagementZonesMapOverlay] Invalid GeoJSON received.');
|
||||
return;
|
||||
}
|
||||
|
||||
const features = geojson.features || [];
|
||||
|
||||
// 1. Ajouter la source et les couches à MapLibre
|
||||
const sourceId = 'management-zones-source';
|
||||
const fillLayerId = 'management-zones-fill';
|
||||
const lineLayerId = 'management-zones-line';
|
||||
|
||||
if (map.getSource(sourceId)) {
|
||||
map.getSource(sourceId).setData(geojson);
|
||||
} else {
|
||||
map.addSource(sourceId, {
|
||||
type: 'geojson',
|
||||
data: geojson
|
||||
});
|
||||
}
|
||||
|
||||
// Couche de remplissage translucide
|
||||
if (!map.getLayer(fillLayerId)) {
|
||||
const fillLayerDef = {
|
||||
id: fillLayerId,
|
||||
type: 'fill',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'fill-color': ['coalesce', ['get', 'color'], '#198754'],
|
||||
'fill-opacity': 0.20
|
||||
}
|
||||
};
|
||||
if (beforeLayerId && map.getLayer(beforeLayerId)) {
|
||||
map.addLayer(fillLayerDef, beforeLayerId);
|
||||
} else {
|
||||
map.addLayer(fillLayerDef);
|
||||
}
|
||||
}
|
||||
|
||||
// Couche de contour avec tirets pour marquer une zone de gestion
|
||||
if (!map.getLayer(lineLayerId)) {
|
||||
const lineLayerDef = {
|
||||
id: lineLayerId,
|
||||
type: 'line',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'line-color': ['coalesce', ['get', 'color'], '#198754'],
|
||||
'line-width': 2.5,
|
||||
'line-opacity': 0.85,
|
||||
'line-dasharray': [3, 1.5]
|
||||
}
|
||||
};
|
||||
if (beforeLayerId && map.getLayer(beforeLayerId)) {
|
||||
map.addLayer(lineLayerDef, beforeLayerId);
|
||||
} else {
|
||||
map.addLayer(lineLayerDef);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Gestion des popups et du curseur sur clic
|
||||
const zonePopup = new maplibregl.Popup({
|
||||
closeButton: true,
|
||||
closeOnClick: true,
|
||||
maxWidth: '340px'
|
||||
});
|
||||
|
||||
map.on('click', fillLayerId, (e) => {
|
||||
const assetLayerIds = (window.assets_layers_defs || []).map(l => l.id).filter(id => map.getLayer(id));
|
||||
if (assetLayerIds.length > 0) {
|
||||
const rendered = map.queryRenderedFeatures(e.point, { layers: assetLayerIds });
|
||||
if (rendered && rendered.length > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!e.features || !e.features[0]) return;
|
||||
const props = e.features[0].properties || {};
|
||||
|
||||
let typeHtml = '';
|
||||
if (props.zone_type_name) {
|
||||
typeHtml = `<span class="badge me-1" style="background-color: ${props.color || '#198754'}; color: #fff; font-size: 10px;">
|
||||
${props.zone_type_name}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
let freqHtml = '';
|
||||
if (props.target_frequency_days) {
|
||||
freqHtml = `<div style="color: #64748b; font-size: 11px; margin-top: 3px;">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>${_t('Fréquence cible :', 'Fréquence cible :')} <strong>${props.target_frequency_days} ${_t('jours', 'jours')}</strong>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let seasonHtml = '';
|
||||
if (props.seasonality_notes) {
|
||||
seasonHtml = `<div style="color: #64748b; font-size: 11px; margin-top: 3px;">
|
||||
<i class="bi bi-calendar3 me-1"></i>${props.seasonality_notes}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let instructionsHtml = '';
|
||||
if (props.instructions) {
|
||||
instructionsHtml = `<div style="background-color: #f8fafc; border-left: 3px solid ${props.color || '#198754'}; padding: 6px; margin-top: 6px; font-size: 11px; color: #334155; border-radius: 2px; max-height: 100px; overflow-y: auto;">
|
||||
<div style="font-weight: 600; margin-bottom: 2px;"><i class="bi bi-exclamation-triangle me-1"></i>${_t('Consignes & points d\'attention', 'Consignes & points d\'attention')}</div>
|
||||
${props.instructions}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let descHtml = '';
|
||||
if (props.description) {
|
||||
descHtml = `<div style="color: #475569; font-size: 11px; margin-top: 4px;">
|
||||
${props.description}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const html = `
|
||||
<div style="font-family: inherit; font-size: 12px; padding: 2px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 4px;">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 3px; background-color: ${props.color || '#198754'}; border: 1px solid rgba(0,0,0,0.2); flex-shrink: 0;"></span>
|
||||
<strong style="font-size: 13px; color: #1e293b;">${props.code}</strong>
|
||||
</div>
|
||||
${typeHtml}
|
||||
</div>
|
||||
<div style="font-weight: 600; color: #1e293b; margin-bottom: 4px;">${props.name}</div>
|
||||
${descHtml}
|
||||
${freqHtml}
|
||||
${seasonHtml}
|
||||
${instructionsHtml}
|
||||
<div style="margin-top: 8px; text-align: right;">
|
||||
<a href="/assets/management-zones/${props.id}/" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 11px;">
|
||||
<i class="bi bi-eye me-1"></i>${_t('Détails & gestion', 'Détails & gestion')} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
zonePopup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
||||
});
|
||||
|
||||
map.on('mousemove', fillLayerId, (e) => {
|
||||
const assetLayerIds = (window.assets_layers_defs || []).map(l => l.id).filter(id => map.getLayer(id));
|
||||
if (assetLayerIds.length > 0) {
|
||||
const rendered = map.queryRenderedFeatures(e.point, { layers: assetLayerIds });
|
||||
if (rendered && rendered.length > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
map.on('mouseleave', fillLayerId, () => {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
|
||||
// 3. Intégration dans le LayerSwitcherControl
|
||||
if (layerSwitcher && typeof layerSwitcher.addCustomSection === 'function') {
|
||||
layerSwitcher.addCustomSection({
|
||||
id: 'management-zones',
|
||||
title: `<span class="d-flex align-items-center gap-1"><i class="bi bi-bounding-box-circles me-1"></i>${_t('Zones de gestion', 'Zones de gestion')}</span>`,
|
||||
render: (container, switcher) => {
|
||||
renderManagementZonesSection(container, switcher, features, map, fillLayerId, lineLayerId, zonePopup, _t, thematic);
|
||||
}
|
||||
});
|
||||
|
||||
if (features.length > 0) {
|
||||
layerSwitcher.setOverlayActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('[ManagementZonesMapOverlay] Error initializing overlay:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendu HTML et interactions de la section "Zones de gestion" dans le LayerSwitcherControl.
|
||||
*/
|
||||
function renderManagementZonesSection(container, switcher, features, map, fillLayerId, lineLayerId, zonePopup, _t, thematic) {
|
||||
if (!features || features.length === 0) {
|
||||
const manageUrl = `/assets/management-zones/${thematic ? '?thematic=' + thematic : ''}`;
|
||||
container.innerHTML = `
|
||||
<div class="text-muted small px-2 py-1 fst-italic d-flex justify-content-between align-items-center">
|
||||
<span>${_t('Aucune zone de gestion définie pour cette thématique.', 'Aucune zone de gestion définie pour cette thématique.')}</span>
|
||||
<a href="${manageUrl}" class="btn btn-link btn-sm p-0 text-muted ms-1" title="${_t('Gérer les zones de gestion', 'Gérer les zones de gestion')}">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const countText = `${features.length} ${_t('zones', 'zones')}`;
|
||||
const manageUrl = `/assets/management-zones/${thematic ? '?thematic=' + thematic : ''}`;
|
||||
|
||||
// En-tête avec toggle général
|
||||
const headerEl = document.createElement('div');
|
||||
headerEl.className = 'd-flex justify-content-between align-items-center px-1 mb-2';
|
||||
headerEl.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="text-muted small fw-semibold">${countText}</span>
|
||||
<a href="${manageUrl}" class="btn btn-link btn-sm p-0 text-muted" style="font-size: 11px;" title="${_t('Gérer les zones de gestion', 'Gérer les zones de gestion')}">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input management-zones-master-toggle" type="checkbox" role="switch" checked title="${_t('Afficher / masquer toutes les zones', 'Afficher / masquer toutes les zones')}">
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(headerEl);
|
||||
|
||||
const masterToggle = headerEl.querySelector('.management-zones-master-toggle');
|
||||
|
||||
// Curseur d'opacité
|
||||
const opacityRow = document.createElement('div');
|
||||
opacityRow.className = 'px-1 mb-2';
|
||||
opacityRow.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center small text-muted mb-1">
|
||||
<span>${_t('Opacité', 'Opacité')}</span>
|
||||
<span class="management-zones-opacity-val">20%</span>
|
||||
</div>
|
||||
<input type="range" class="form-range management-zones-opacity-slider" min="5" max="80" value="20" style="height: 4px;">
|
||||
`;
|
||||
container.appendChild(opacityRow);
|
||||
|
||||
const opacitySlider = opacityRow.querySelector('.management-zones-opacity-slider');
|
||||
const opacityValLabel = opacityRow.querySelector('.management-zones-opacity-val');
|
||||
|
||||
opacitySlider.addEventListener('input', (e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
opacityValLabel.textContent = `${val}%`;
|
||||
if (map.getLayer(fillLayerId)) {
|
||||
map.setPaintProperty(fillLayerId, 'fill-opacity', val / 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Liste des zones
|
||||
const listEl = document.createElement('div');
|
||||
listEl.className = 'management-zones-list d-flex flex-column gap-1';
|
||||
listEl.style.maxHeight = '240px';
|
||||
listEl.style.overflowY = 'auto';
|
||||
|
||||
const activeZoneIds = new Set(features.map(f => f.id));
|
||||
|
||||
function updateLayersVisibility() {
|
||||
if (!masterToggle.checked || activeZoneIds.size === 0) {
|
||||
if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, 'visibility', 'none');
|
||||
if (map.getLayer(lineLayerId)) map.setLayoutProperty(lineLayerId, 'visibility', 'none');
|
||||
} else {
|
||||
if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, 'visibility', 'visible');
|
||||
if (map.getLayer(lineLayerId)) map.setLayoutProperty(lineLayerId, 'visibility', 'visible');
|
||||
|
||||
const filter = ['in', ['id'], ['literal', Array.from(activeZoneIds)]];
|
||||
if (map.getLayer(fillLayerId)) map.setFilter(fillLayerId, filter);
|
||||
if (map.getLayer(lineLayerId)) map.setFilter(lineLayerId, filter);
|
||||
}
|
||||
}
|
||||
|
||||
masterToggle.addEventListener('change', () => {
|
||||
const checked = masterToggle.checked;
|
||||
listEl.querySelectorAll('.zone-item-toggle').forEach(chk => {
|
||||
chk.checked = checked;
|
||||
});
|
||||
if (checked) {
|
||||
features.forEach(f => activeZoneIds.add(f.id));
|
||||
} else {
|
||||
activeZoneIds.clear();
|
||||
}
|
||||
updateLayersVisibility();
|
||||
});
|
||||
|
||||
features.forEach(feat => {
|
||||
const props = feat.properties || {};
|
||||
const color = props.color || '#198754';
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.className = 'd-flex align-items-center justify-content-between p-1 rounded hover-bg small';
|
||||
itemEl.style.cursor = 'pointer';
|
||||
|
||||
itemEl.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-2 text-truncate me-2 zone-click-target" style="flex: 1;">
|
||||
<span style="display:inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${color}; border: 1px solid rgba(0,0,0,0.25); flex-shrink: 0;"></span>
|
||||
<div class="text-truncate">
|
||||
<div class="fw-semibold text-truncate text-dark" style="font-size: 11px;">${props.code}</div>
|
||||
<div class="text-muted text-truncate" style="font-size: 10px;">${props.name} ${props.zone_type_name ? `(${props.zone_type_name})` : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1 flex-shrink-0">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted zoom-btn" title="${_t('Centrer sur la zone', 'Centrer sur la zone')}">
|
||||
<i class="bi bi-geo-alt" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0 ms-1">
|
||||
<input class="form-check-input zone-item-toggle" type="checkbox" role="switch" checked data-zone-id="${feat.id}">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const itemToggle = itemEl.querySelector('.zone-item-toggle');
|
||||
itemToggle.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
if (itemToggle.checked) {
|
||||
activeZoneIds.add(feat.id);
|
||||
} else {
|
||||
activeZoneIds.delete(feat.id);
|
||||
}
|
||||
masterToggle.checked = (activeZoneIds.size > 0);
|
||||
updateLayersVisibility();
|
||||
});
|
||||
|
||||
function zoomToZone() {
|
||||
const bbox = getGeometryBBox(feat.geometry);
|
||||
if (bbox) {
|
||||
map.fitBounds(bbox, { padding: 40, maxZoom: 16 });
|
||||
}
|
||||
}
|
||||
|
||||
itemEl.querySelector('.zoom-btn').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
zoomToZone();
|
||||
});
|
||||
|
||||
itemEl.querySelector('.zone-click-target').addEventListener('click', () => {
|
||||
zoomToZone();
|
||||
});
|
||||
|
||||
listEl.appendChild(itemEl);
|
||||
});
|
||||
|
||||
container.appendChild(listEl);
|
||||
}
|
||||
|
||||
window.initManagementZonesMapOverlay = initManagementZonesMapOverlay;
|
||||
|
||||
})(window);
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<defs>
|
||||
<!-- Background grass blades gradient (deep rich green) -->
|
||||
<linearGradient id="grass-dark" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#2e7d32"/>
|
||||
<stop offset="100%" stop-color="#1b5e20"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Mid grass blades gradient (fresh vibrant green) -->
|
||||
<linearGradient id="grass-mid" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#66bb6a"/>
|
||||
<stop offset="100%" stop-color="#2e7d32"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Side arching grass blades gradient -->
|
||||
<linearGradient id="grass-light" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#81c784"/>
|
||||
<stop offset="100%" stop-color="#388e3c"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Ground turf base gradient -->
|
||||
<linearGradient id="turf-base" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#43a047"/>
|
||||
<stop offset="100%" stop-color="#1b5e20"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Front turf lip gradient -->
|
||||
<linearGradient id="turf-front" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#4caf50"/>
|
||||
<stop offset="100%" stop-color="#2e7d32"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Soil ground base gradient -->
|
||||
<linearGradient id="soil-base" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#6d4c41"/>
|
||||
<stop offset="100%" stop-color="#4e342e"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Ground / Soil base slab -->
|
||||
<rect x="9" y="52" width="46" height="6" rx="3" fill="url(#soil-base)"/>
|
||||
|
||||
<!-- Back turf mound -->
|
||||
<ellipse cx="32" cy="49" rx="25" ry="7" fill="url(#turf-base)"/>
|
||||
|
||||
<!-- Background grass blades -->
|
||||
<!-- Mid-left tall blade -->
|
||||
<path d="M 21 48 C 17 32, 16 19, 20 11 C 24 15, 27 29, 28 48 Z" fill="url(#grass-dark)"/>
|
||||
<!-- Mid-right tall blade -->
|
||||
<path d="M 36 48 C 37 29, 40 15, 44 11 C 48 19, 47 32, 43 48 Z" fill="url(#grass-dark)"/>
|
||||
|
||||
<!-- Main foreground grass blades -->
|
||||
<!-- Left arching blade -->
|
||||
<path d="M 14 49 C 10 38, 7 28, 8 19 C 14 21, 21 34, 25 49 Z" fill="url(#grass-light)"/>
|
||||
<!-- Right arching blade -->
|
||||
<path d="M 39 49 C 43 34, 50 21, 56 19 C 57 28, 54 38, 50 49 Z" fill="url(#grass-light)"/>
|
||||
<!-- Center tall proud blade -->
|
||||
<path d="M 26 49 C 27 30, 28 16, 32 6 C 36 16, 37 30, 38 49 Z" fill="url(#grass-mid)"/>
|
||||
|
||||
<!-- Center blade subtle highlight -->
|
||||
<path d="M 31 48 C 31.5 32, 31.8 18, 32 9 C 32.2 18, 32.8 32, 32.5 48 Z" fill="#a5d6a7" opacity="0.6"/>
|
||||
|
||||
<!-- Foreground turf rim -->
|
||||
<ellipse cx="32" cy="51" rx="23" ry="5" fill="url(#turf-front)"/>
|
||||
|
||||
<!-- Small front sprouting blades -->
|
||||
<path d="M 21 51 C 20 44, 18 36, 17 30 C 20 34, 23 42, 24 51 Z" fill="url(#grass-light)"/>
|
||||
<path d="M 40 51 C 41 42, 44 34, 47 30 C 46 36, 44 44, 43 51 Z" fill="url(#grass-light)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,393 +0,0 @@
|
|||
/**
|
||||
* Visualisation cartographique détaillée d'une localisation Nature (StreetUp / Loko)
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const mapContainer = document.getElementById('map');
|
||||
if (!mapContainer) return;
|
||||
|
||||
const lon = typeof feature_lon !== 'undefined' && feature_lon !== null ? feature_lon : 4.3517;
|
||||
const lat = typeof feature_lat !== 'undefined' && feature_lat !== null ? feature_lat : 50.8503;
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: window.MAP_STYLE_LIGHT || 'https://api.maptiler.com/maps/streets-v2-light/style.json',
|
||||
center: [lon, lat],
|
||||
zoom: 16,
|
||||
pitch: 35,
|
||||
bearing: -10,
|
||||
antialias: true
|
||||
});
|
||||
|
||||
// ── Helper to place raster basemaps below nature layers ──
|
||||
function getBasemapBeforeLayer(m) {
|
||||
if (!m || !m.getStyle) return undefined;
|
||||
if (m.getLayer('nature-location-fill')) return 'nature-location-fill';
|
||||
if (m.getLayer('nature-location-outline')) return 'nature-location-outline';
|
||||
if (m.getLayer('layer-surfaces-fill')) return 'layer-surfaces-fill';
|
||||
if (m.getLayer('layer-surfaces-outline')) return 'layer-surfaces-outline';
|
||||
if (m.getLayer('layer-shrubs')) return 'layer-shrubs';
|
||||
if (m.getLayer('layer-benches')) return 'layer-benches';
|
||||
if (m.getLayer('layer-trees')) return 'layer-trees';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── Ensure strict layer stacking order on top of basemaps ──
|
||||
function reorderNatureLayers() {
|
||||
const layerOrder = [
|
||||
'nature-location-fill',
|
||||
'nature-location-outline',
|
||||
'layer-surfaces-fill',
|
||||
'layer-surfaces-outline',
|
||||
'layer-shrubs',
|
||||
'layer-benches',
|
||||
'layer-trees',
|
||||
];
|
||||
layerOrder.forEach(function (layerId) {
|
||||
if (map.getLayer(layerId)) {
|
||||
try {
|
||||
map.moveLayer(layerId);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add layer switcher control for basemaps / orthophotos
|
||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
||||
const layerSwitcher = new LayerSwitcherControl({
|
||||
layers: [{
|
||||
id: 'basemap',
|
||||
name: 'Fonds de plan & Orthophotos',
|
||||
icon: 'satellite',
|
||||
group: 'basemap',
|
||||
beforeLayer: getBasemapBeforeLayer,
|
||||
onLayerAdded: () => reorderNatureLayers(),
|
||||
onVisibilityChange: () => reorderNatureLayers(),
|
||||
layer: {
|
||||
type: 'raster',
|
||||
paint: { 'raster-opacity': 1 }
|
||||
},
|
||||
basemaps: (window.MAP_BACKGROUNDS && window.MAP_BACKGROUNDS.length > 0) ? window.MAP_BACKGROUNDS : undefined,
|
||||
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 || '© Brussels 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');
|
||||
}
|
||||
|
||||
// Add navigation and fullscreen controls
|
||||
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), 'top-right');
|
||||
map.addControl(new maplibregl.FullscreenControl(), 'top-right');
|
||||
|
||||
map.on('load', function () {
|
||||
let combinedBbox = null;
|
||||
|
||||
// 1. Location geometry (polygon / boundary)
|
||||
if (typeof section_geojson !== 'undefined' && section_geojson) {
|
||||
const locGeom = typeof section_geojson === 'string' ? JSON.parse(section_geojson) : section_geojson;
|
||||
const locationFeature = {
|
||||
type: 'Feature',
|
||||
geometry: locGeom,
|
||||
properties: {
|
||||
name: 'Location'
|
||||
}
|
||||
};
|
||||
|
||||
map.addSource('nature-location', {
|
||||
type: 'geojson',
|
||||
data: locationFeature
|
||||
});
|
||||
|
||||
// Fill layer for the section
|
||||
map.addLayer({
|
||||
id: 'nature-location-fill',
|
||||
type: 'fill',
|
||||
source: 'nature-location',
|
||||
paint: {
|
||||
'fill-color': '#16a34a',
|
||||
'fill-opacity': 0.15
|
||||
}
|
||||
});
|
||||
|
||||
// Outline layer for the section
|
||||
map.addLayer({
|
||||
id: 'nature-location-outline',
|
||||
type: 'line',
|
||||
source: 'nature-location',
|
||||
paint: {
|
||||
'line-color': '#15803d',
|
||||
'line-width': 2.5
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof turf !== 'undefined') {
|
||||
try {
|
||||
combinedBbox = turf.bbox(locationFeature);
|
||||
} catch (e) {
|
||||
console.warn('Could not compute location bbox:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Assets (green surfaces, trees, shrubs, benches)
|
||||
if (typeof assets_geojson !== 'undefined' && assets_geojson) {
|
||||
const assetsData = typeof assets_geojson === 'string' ? JSON.parse(assets_geojson) : assets_geojson;
|
||||
|
||||
map.addSource('nature-assets', {
|
||||
type: 'geojson',
|
||||
data: assetsData
|
||||
});
|
||||
|
||||
// Green surfaces fill
|
||||
map.addLayer({
|
||||
id: 'layer-surfaces-fill',
|
||||
type: 'fill',
|
||||
source: 'nature-assets',
|
||||
filter: ['==', ['get', 'asset_type'], 'naturegreensurface'],
|
||||
paint: {
|
||||
'fill-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
||||
'fill-opacity': 0.65
|
||||
}
|
||||
});
|
||||
|
||||
// Green surfaces outline
|
||||
map.addLayer({
|
||||
id: 'layer-surfaces-outline',
|
||||
type: 'line',
|
||||
source: 'nature-assets',
|
||||
filter: ['==', ['get', 'asset_type'], 'naturegreensurface'],
|
||||
paint: {
|
||||
'line-color': '#166534',
|
||||
'line-width': 1.5
|
||||
}
|
||||
});
|
||||
|
||||
// Trees (circles)
|
||||
map.addLayer({
|
||||
id: 'layer-trees',
|
||||
type: 'circle',
|
||||
source: 'nature-assets',
|
||||
filter: ['==', ['get', 'asset_type'], 'naturetree'],
|
||||
paint: {
|
||||
'circle-radius': [
|
||||
'interpolate', ['linear'], ['zoom'],
|
||||
13, 4,
|
||||
16, 7,
|
||||
19, 12
|
||||
],
|
||||
'circle-color': '#15803d',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
});
|
||||
|
||||
// Shrubs (circles)
|
||||
map.addLayer({
|
||||
id: 'layer-shrubs',
|
||||
type: 'circle',
|
||||
source: 'nature-assets',
|
||||
filter: ['==', ['get', 'asset_type'], 'natureshrub'],
|
||||
paint: {
|
||||
'circle-radius': [
|
||||
'interpolate', ['linear'], ['zoom'],
|
||||
13, 3,
|
||||
16, 5,
|
||||
19, 9
|
||||
],
|
||||
'circle-color': '#84cc16',
|
||||
'circle-stroke-width': 1.5,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
});
|
||||
|
||||
// Benches (circles)
|
||||
map.addLayer({
|
||||
id: 'layer-benches',
|
||||
type: 'circle',
|
||||
source: 'nature-assets',
|
||||
filter: ['==', ['get', 'asset_type'], 'naturebench'],
|
||||
paint: {
|
||||
'circle-radius': [
|
||||
'interpolate', ['linear'], ['zoom'],
|
||||
13, 3,
|
||||
16, 5,
|
||||
19, 9
|
||||
],
|
||||
'circle-color': '#b45309',
|
||||
'circle-stroke-width': 1.5,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
});
|
||||
|
||||
// Fit bounds with Turf if features exist
|
||||
if (typeof turf !== 'undefined' && assetsData.features && assetsData.features.length > 0) {
|
||||
try {
|
||||
const assetsBbox = turf.bbox(assetsData);
|
||||
if (!combinedBbox) {
|
||||
combinedBbox = assetsBbox;
|
||||
} else {
|
||||
combinedBbox = [
|
||||
Math.min(combinedBbox[0], assetsBbox[0]),
|
||||
Math.min(combinedBbox[1], assetsBbox[1]),
|
||||
Math.max(combinedBbox[2], assetsBbox[2]),
|
||||
Math.max(combinedBbox[3], assetsBbox[3])
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not compute assets bbox:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply fit bounds
|
||||
if (combinedBbox && combinedBbox[0] !== Infinity) {
|
||||
map.fitBounds(combinedBbox, {
|
||||
padding: { top: 40, bottom: 40, left: 40, right: 40 },
|
||||
maxZoom: 18,
|
||||
duration: 800
|
||||
});
|
||||
}
|
||||
reorderNatureLayers();
|
||||
|
||||
// 3. Interactive Popups
|
||||
const interactiveLayers = [
|
||||
'layer-surfaces-fill',
|
||||
'layer-trees',
|
||||
'layer-shrubs',
|
||||
'layer-benches'
|
||||
];
|
||||
|
||||
interactiveLayers.forEach(function (layerId) {
|
||||
map.on('mouseenter', layerId, function () {
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
map.on('mouseleave', layerId, function () {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
|
||||
map.on('click', layerId, function (e) {
|
||||
if (!e.features || !e.features.length) return;
|
||||
const f = e.features[0];
|
||||
const props = f.properties;
|
||||
|
||||
let iconClass = 'bi-tree-fill text-success';
|
||||
let typeBadge = '<span class="badge bg-success">Surface</span>';
|
||||
|
||||
if (props.asset_type === 'naturetree') {
|
||||
iconClass = 'bi-tree-fill text-success';
|
||||
typeBadge = '<span class="badge bg-success">Arbre</span>';
|
||||
} else if (props.asset_type === 'natureshrub') {
|
||||
iconClass = 'bi-flower3 text-success';
|
||||
typeBadge = '<span class="badge bg-info">Arbuste</span>';
|
||||
} else if (props.asset_type === 'naturebench') {
|
||||
iconClass = 'bi-inbox text-warning';
|
||||
typeBadge = '<span class="badge bg-warning text-dark">Banc / Mobilier</span>';
|
||||
} else if (props.asset_type === 'naturegreensurface') {
|
||||
iconClass = 'bi-grid-fill text-success';
|
||||
typeBadge = '<span class="badge bg-primary">Surface verte</span>';
|
||||
}
|
||||
|
||||
let detailsHtml = '';
|
||||
if (props.area) {
|
||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-rulers me-1"></i>Superficie : <strong>${props.area}</strong></div>`;
|
||||
}
|
||||
if (props.vitality) {
|
||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-heart-pulse me-1"></i>Vitalité : <strong>${props.vitality}</strong></div>`;
|
||||
}
|
||||
if (props.stage) {
|
||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-clock-history me-1"></i>Stade : <strong>${props.stage}</strong></div>`;
|
||||
}
|
||||
if (props.height) {
|
||||
detailsHtml += `<div class="small text-muted"><i class="bi bi-arrows-vertical me-1"></i>Hauteur : <strong>${props.height}</strong></div>`;
|
||||
}
|
||||
if (props.is_remarkable === 'true' || props.is_remarkable === true) {
|
||||
detailsHtml += `<div class="badge bg-warning text-dark my-1"><i class="bi bi-star-fill me-1"></i>Arbre remarquable</div>`;
|
||||
}
|
||||
|
||||
const popupHtml = `
|
||||
<div class="nature-map-popup" style="min-width: 200px; padding: 4px;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1 pb-1 border-bottom">
|
||||
<span class="fw-bold"><i class="bi ${iconClass} me-1"></i>${props.code || ''}</span>
|
||||
${typeBadge}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
${props.model ? `<div class="fw-semibold text-dark">${props.model}</div>` : ''}
|
||||
${props.scientific_name ? `<div class="fst-italic text-muted small">${props.scientific_name}</div>` : ''}
|
||||
${props.status ? `<div class="small"><span class="badge bg-light text-secondary border mt-1">${props.status}</span></div>` : ''}
|
||||
${detailsHtml}
|
||||
</div>
|
||||
${props.detail_url ? `
|
||||
<a href="${props.detail_url}" class="btn btn-xs btn-primary w-100 text-center py-1" style="font-size: 0.8rem;">
|
||||
<i class="bi bi-arrow-right-circle me-1"></i>Consulter la fiche
|
||||
</a>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
new maplibregl.Popup({ closeButton: true, closeOnClick: true, maxWidth: '280px' })
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(popupHtml)
|
||||
.addTo(map);
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Layer Toggle Handlers
|
||||
window.toggleNatureLayer = function (layerGroup, isChecked) {
|
||||
const layerMapping = {
|
||||
'surfaces': ['layer-surfaces-fill', 'layer-surfaces-outline'],
|
||||
'trees': ['layer-trees'],
|
||||
'shrubs': ['layer-shrubs'],
|
||||
'benches': ['layer-benches']
|
||||
};
|
||||
|
||||
const layers = layerMapping[layerGroup];
|
||||
if (layers) {
|
||||
const visibility = isChecked ? 'visible' : 'none';
|
||||
layers.forEach(function (id) {
|
||||
if (map.getLayer(id)) {
|
||||
map.setLayoutProperty(id, 'visibility', visibility);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Global Zoom to Asset helper
|
||||
window.zoomToAsset = function (assetLon, assetLat, assetCode) {
|
||||
if (assetLon && assetLat) {
|
||||
map.flyTo({
|
||||
center: [assetLon, assetLat],
|
||||
zoom: 18.5,
|
||||
pitch: 45,
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="planterWood" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#ea580c"/>
|
||||
<stop offset="100%" stop-color="#9a3412"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="leafGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#4ade80"/>
|
||||
<stop offset="100%" stop-color="#15803d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="flowerPink" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#fb7185"/>
|
||||
<stop offset="100%" stop-color="#e11d48"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Foliage / Leaves -->
|
||||
<!-- Left leaf -->
|
||||
<path d="M 32 34 C 24 32, 14 26, 15 17 C 23 17, 28 24, 32 34 Z" fill="url(#leafGrad)"/>
|
||||
<!-- Right leaf -->
|
||||
<path d="M 32 34 C 40 32, 50 26, 49 17 C 41 17, 36 24, 32 34 Z" fill="url(#leafGrad)"/>
|
||||
<!-- Center tall leaves -->
|
||||
<path d="M 32 34 C 28 26, 26 15, 32 8 C 38 15, 36 26, 32 34 Z" fill="#22c55e"/>
|
||||
<!-- Side drooping leaves -->
|
||||
<path d="M 32 35 C 20 35, 10 33, 9 27 C 16 26, 25 29, 32 35 Z" fill="#16a34a"/>
|
||||
<path d="M 32 35 C 44 35, 54 33, 55 27 C 48 26, 39 29, 32 35 Z" fill="#16a34a"/>
|
||||
|
||||
<!-- Flowers -->
|
||||
<!-- Left flower -->
|
||||
<circle cx="21" cy="21" r="5" fill="url(#flowerPink)"/>
|
||||
<circle cx="21" cy="21" r="2" fill="#fef08a"/>
|
||||
|
||||
<!-- Right flower -->
|
||||
<circle cx="43" cy="21" r="5" fill="#f59e0b"/>
|
||||
<circle cx="43" cy="21" r="2" fill="#fffbeb"/>
|
||||
|
||||
<!-- Center top flower -->
|
||||
<circle cx="32" cy="14" r="6" fill="url(#flowerPink)"/>
|
||||
<circle cx="32" cy="14" r="2.5" fill="#fef08a"/>
|
||||
|
||||
<!-- Planter Box (Bac / Jardinière) -->
|
||||
<!-- Rim -->
|
||||
<rect x="11" y="34" width="42" height="5" rx="2" fill="#78350f"/>
|
||||
<rect x="12" y="35" width="40" height="3" rx="1.5" fill="#ea580c"/>
|
||||
|
||||
<!-- Body -->
|
||||
<polygon points="14,39 50,39 46,55 18,55" fill="url(#planterWood)"/>
|
||||
|
||||
<!-- Wooden slats / horizontal grooves -->
|
||||
<line x1="15" y1="44" x2="49" y2="44" stroke="#78350f" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="16.5" y1="49" x2="47.5" y2="49" stroke="#78350f" stroke-width="1.2" stroke-linecap="round"/>
|
||||
|
||||
<!-- Corner metal brackets / reinforcements -->
|
||||
<line x1="14" y1="39" x2="18" y2="55" stroke="#475569" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="50" y1="39" x2="46" y2="55" stroke="#475569" stroke-width="2" stroke-linecap="round"/>
|
||||
|
||||
<!-- Feet / Stand -->
|
||||
<rect x="19" y="55" width="5" height="4" rx="1" fill="#334155"/>
|
||||
<rect x="40" y="55" width="5" height="4" rx="1" fill="#334155"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
|
@ -1,20 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="foliage-dead" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#9ca3af"/>
|
||||
<stop offset="100%" stop-color="#4b5563"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="trunk-dead" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#78716c"/>
|
||||
<stop offset="100%" stop-color="#57534e"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-dead)"/>
|
||||
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-dead)"/>
|
||||
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-dead)"/>
|
||||
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-dead)"/>
|
||||
|
||||
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk-dead)"/>
|
||||
<rect x="25" y="54" width="14" height="5" rx="2" fill="#44403c"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 928 B |
|
|
@ -1,20 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="foliage-reg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#60a5fa"/>
|
||||
<stop offset="100%" stop-color="#1d4ed8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="trunk" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#8d6e63"/>
|
||||
<stop offset="100%" stop-color="#5d4037"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-reg)"/>
|
||||
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-reg)"/>
|
||||
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-reg)"/>
|
||||
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-reg)"/>
|
||||
|
||||
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk)"/>
|
||||
<rect x="25" y="54" width="14" height="5" rx="2" fill="#4e342e"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 913 B |
|
|
@ -1,20 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="foliage-rem" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#fbbf24"/>
|
||||
<stop offset="100%" stop-color="#d97706"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="trunk" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#8d6e63"/>
|
||||
<stop offset="100%" stop-color="#5d4037"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-rem)"/>
|
||||
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-rem)"/>
|
||||
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-rem)"/>
|
||||
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-rem)"/>
|
||||
|
||||
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk)"/>
|
||||
<rect x="25" y="54" width="14" height="5" rx="2" fill="#4e342e"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 913 B |
|
|
@ -1,38 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="sbBody" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#fb923c"/>
|
||||
<stop offset="30%" stop-color="#f97316"/>
|
||||
<stop offset="100%" stop-color="#c2410c"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sbPlateau" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#fdba74"/>
|
||||
<stop offset="100%" stop-color="#fb923c"/>
|
||||
</linearGradient>
|
||||
<filter id="sbShadow" x="-15%" y="-15%" width="130%" height="135%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.4"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Drop shadow and dark road border -->
|
||||
<path d="M 5 48 L 15 17 C 15.5 15.5 17 14.5 18.5 14.5 L 45.5 14.5 C 47 14.5 48.5 15.5 49 17 L 59 48 C 59.5 49.8 58.2 51.5 56.5 51.5 L 7.5 51.5 C 5.8 51.5 4.5 49.8 5 48 Z"
|
||||
fill="#7c2d12" filter="url(#sbShadow)"/>
|
||||
|
||||
<!-- Main Speed Cushion Body (Coussin berlinois) -->
|
||||
<path d="M 7 47 L 16 18 C 16.5 16.5 17.5 16 19 16 L 45 16 C 46.5 16 47.5 16.5 48 18 L 57 47 C 57.5 48.5 56.5 49.5 55 49.5 L 9 49.5 C 7.5 49.5 6.5 48.5 7 47 Z"
|
||||
fill="url(#sbBody)"/>
|
||||
|
||||
<!-- Lower bevel lip -->
|
||||
<polygon points="7,47 9.5,50 54.5,50 57,47" fill="#9a3412"/>
|
||||
|
||||
<!-- Upper Plateau -->
|
||||
<polygon points="20,27 22,18 42,18 44,27" fill="url(#sbPlateau)" opacity="0.9"/>
|
||||
|
||||
<!-- 3 Reflective White Triangles (Universal Speed Cushion Markings) -->
|
||||
<polygon points="12,45 20,24 28,45" fill="#ffffff" stroke="#cbd5e1" stroke-width="0.75" stroke-linejoin="round"/>
|
||||
<polygon points="24,45 32,24 40,45" fill="#ffffff" stroke="#cbd5e1" stroke-width="0.75" stroke-linejoin="round"/>
|
||||
<polygon points="36,45 44,24 52,45" fill="#ffffff" stroke="#cbd5e1" stroke-width="0.75" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Top ridge highlight -->
|
||||
<line x1="19" y1="16" x2="45" y2="16" stroke="#fed7aa" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,51 +0,0 @@
|
|||
{% load i18n %}
|
||||
<ul class="list-group list-group-flush bg-transparent border-0">
|
||||
{% 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-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 asset_node.children %}
|
||||
<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-asset-{{ asset_node.asset.id }}"
|
||||
aria-expanded="true">
|
||||
<i class="bi bi-chevron-down small"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="d-inline-block text-center text-muted" style="width: 1.35rem;"><i class="bi bi-dash small"></i></span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Icône colorée selon la famille d'asset -->
|
||||
<i class="bi {{ asset_node.asset.get_icon_class|default:'bi-tools' }}" style="font-size: 1rem; color: {{ asset_node.asset.theme_icon_color }};"></i>
|
||||
|
||||
<!-- Code cliquable avec couleur thématique -->
|
||||
<a href="{{ asset_node.asset.get_absolute_url }}" class="fw-bold text-decoration-underline small" style="color: {{ asset_node.asset.theme_color }};">
|
||||
{{ asset_node.asset.code }}
|
||||
</a>
|
||||
<span class="small text-dark">{{ asset_node.asset.get_name }}</span>
|
||||
|
||||
<!-- Badge Macro-lot sobre -->
|
||||
<span class="badge bg-light text-muted border small">{{ asset_node.asset.get_macro_lot }}</span>
|
||||
|
||||
<!-- Badge Typologie avec couleur thématique différenciée -->
|
||||
<span class="badge {{ asset_node.asset.theme_badge_class }} small" style="{{ asset_node.asset.theme_badge_style }}">
|
||||
{{ asset_node.asset.get_asset_type_display }}
|
||||
</span>
|
||||
|
||||
{% if asset_node.asset.status == 'inactive' %}
|
||||
<span class="badge bg-danger-subtle text-danger border small">{% translate "Inactif" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if asset_node.children %}
|
||||
<div id="tree-asset-{{ asset_node.asset.id }}" class="collapse show tree-collapse-node">
|
||||
{% 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>
|
||||
|
|
@ -1,991 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n documents_tags %}
|
||||
|
||||
{% block title %}{{ building.code }} - {{ building.get_name }} | {{ product_name }}{% 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>
|
||||
{% if building.room_number %}
|
||||
<tr>
|
||||
<th class="text-muted">{% translate "Numéro de local / porte" %} :</th>
|
||||
<td><span class="badge bg-secondary-subtle text-secondary border font-monospace"><i class="bi bi-tag me-1"></i>{{ building.room_number }}</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if building.surface_m2 %}
|
||||
<tr>
|
||||
<th class="text-muted">{% translate "Surface spécifique" %} :</th>
|
||||
<td>{{ building.surface_m2 }} m²</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if building.ceiling_height_m %}
|
||||
<tr>
|
||||
<th class="text-muted">{% translate "Hauteur sous plafond" %} :</th>
|
||||
<td>{{ building.ceiling_height_m }} m</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th class="text-muted">{% translate "Chauffage du local" %} :</th>
|
||||
<td>{% if building.is_heated %}<span class="text-success"><i class="bi bi-fire me-1"></i>{% translate "Chauffé" %}</span>{% else %}<span class="text-muted"><i class="bi bi-slash-circle me-1"></i>{% translate "Non chauffé" %}</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% if building.floor_type %}
|
||||
<tr>
|
||||
<th class="text-muted">{% translate "Revêtement de sol" %} :</th>
|
||||
<td>{{ building.get_floor_type_display }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if building.fire_compartment %}
|
||||
<tr>
|
||||
<th class="text-muted">{% translate "Compartiment coupe-feu" %} :</th>
|
||||
<td>{{ building.fire_compartment }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<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" id="card-hierarchy-tree">
|
||||
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence Hiérarchique" %}</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="btn-group btn-group-sm" role="group" id="tree-expand-collapse-group">
|
||||
<button type="button" class="btn btn-outline-secondary py-0 px-2 small" id="btn-tree-collapse-all" title="{% translate 'Tout replier' %}">
|
||||
<i class="bi bi-arrows-collapse me-1"></i>{% translate "Tout replier" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary py-0 px-2 small" id="btn-tree-expand-all" title="{% translate 'Tout déplier' %}">
|
||||
<i class="bi bi-arrows-expand me-1"></i>{% translate "Tout déplier" %}
|
||||
</button>
|
||||
</div>
|
||||
<span class="badge bg-primary-subtle text-primary border">{{ location_tree.children|length }} {% translate "sous-localisation(s)" %}</span>
|
||||
</div>
|
||||
</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 flex-wrap">
|
||||
<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-root-content"
|
||||
aria-expanded="true">
|
||||
<i class="bi bi-chevron-down small"></i>
|
||||
</button>
|
||||
<i class="bi {{ building.get_icon_class }} text-primary fs-5"></i>
|
||||
<span class="fw-bold fs-6 text-primary">{{ building.code }}</span>
|
||||
{% if building.room_number %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border ms-1 font-monospace"><i class="bi bi-tag me-1"></i>{{ building.room_number }}</span>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
<span class="badge bg-info-subtle text-info border small ms-1" title="{% translate 'Assets rattachés directement au bâtiment' %}">
|
||||
<i class="bi bi-tools me-1"></i>{{ location_tree.assets|length }} {% translate "asset(s)" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tree-root-content" class="collapse show mt-2">
|
||||
<!-- Assets rattachés directement au bâtiment racine -->
|
||||
{% if location_tree.assets %}
|
||||
<div class="tree-assets-group">
|
||||
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=location_tree.assets depth=2 only %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Sous-localisations & leurs assets -->
|
||||
<div class="tree-locations-group">
|
||||
{% include "assets/buildings/publicbuilding_location_tree.html" with node=location_tree depth=1 only %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Localisation & Plans d'étage / Carte interactive -->
|
||||
<div class="col-md-5">
|
||||
|
||||
<!-- Card Localisation, Carte interactive & Plans d'étage -->
|
||||
<div class="card shadow-sm mb-4" id="card-location-plans">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center flex-wrap gap-2 py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
{% if level_specific_mode %}
|
||||
<span class="fw-bold text-dark"><i class="bi bi-layers text-primary me-2"></i>{% translate "Plan du niveau" %} : <span class="text-primary">{{ active_floor.name|default:building.name }}</span></span>
|
||||
{% else %}
|
||||
<span class="fw-bold text-dark"><i class="bi bi-geo-alt text-primary me-2"></i>{% translate "Localisation & Plans" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="btn-group btn-group-sm" role="group" id="view-mode-toggle">
|
||||
<button type="button" class="btn btn-outline-primary active" id="btn-toggle-plans" title="{% translate "Afficher les plans d'étage" %}">
|
||||
<i class="bi bi-layers me-1"></i>{% translate "Plans" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btn-toggle-map" title="{% translate "Afficher la carte du site" %}">
|
||||
<i class="bi bi-map me-1"></i>{% translate "Carte" %}
|
||||
</button>
|
||||
</div>
|
||||
{% if center_lat and center_lon %}
|
||||
<a href="https://www.google.com/maps?cbll={{ center_lat_json }},{{ center_lon_json }}&cbp=12,0,0,0,5&layer=c&panoid="
|
||||
target="_blank" class="btn btn-sm btn-outline-secondary py-0 px-2" title="Street View">
|
||||
<img src="{% static 'common/google-street-view-logo.png' %}" alt="Street View" height="14" class="me-1">
|
||||
Street View
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. VUE PLANS D'ÉTAGE -->
|
||||
<div id="section-plans" class="card-body p-0">
|
||||
{% if not level_specific_mode and floors_data %}
|
||||
<!-- Sélecteur d'étage horizontal -->
|
||||
<div class="bg-light border-bottom px-3 py-2 d-flex align-items-center gap-1 overflow-auto" id="floors-nav-bar" style="white-space: nowrap;">
|
||||
<span class="text-muted small fw-semibold me-2"><i class="bi bi-filter me-1"></i>{% translate "Niveaux" %} :</span>
|
||||
{% for f in floors_data %}
|
||||
<button type="button"
|
||||
class="btn btn-sm floor-nav-btn {% if f.id == active_floor.id %}btn-primary text-white shadow-sm{% else %}btn-outline-secondary{% endif %}"
|
||||
data-floor-id="{{ f.id }}"
|
||||
data-plan-url="{{ f.plan_url }}"
|
||||
data-has-plan="{{ f.has_plan|yesno:'true,false' }}"
|
||||
data-floor-name="{{ f.name }}"
|
||||
data-surface="{{ f.surface_m2|default:'' }}"
|
||||
data-rooms-count="{{ f.rooms_count }}"
|
||||
data-assets-count="{{ f.assets_count }}"
|
||||
data-floor-url="{{ f.url }}">
|
||||
{{ f.name }}
|
||||
{% if f.has_plan %}
|
||||
<span class="badge bg-white text-primary ms-1" title="{% translate "Plan architectural disponible" %}">📐</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Conteneur du viewer interactif -->
|
||||
<div id="plan-viewer-wrapper" style="position: relative; height: 380px; width: 100%; background: #0F172A05; overflow: hidden; display: flex; align-items: center; justify-content: center;">
|
||||
|
||||
<!-- Floating Controls (Zoom / Pan / Fullscreen / Download) -->
|
||||
<div class="btn-group-vertical btn-group-sm shadow-sm" style="position: absolute; top: 12px; right: 12px; z-index: 10; background: rgba(255, 255, 255, 0.92); backdrop-filter: blur(4px); border-radius: 6px; border: 1px solid #CBD5E1;">
|
||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-zoom-in" title="{% translate "Zoom avant (+)" %}">
|
||||
<i class="bi bi-zoom-in"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-zoom-out" title="{% translate "Zoom arrière (-)" %}">
|
||||
<i class="bi bi-zoom-out"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-reset" title="{% translate "Recentrer / Ajuster (100%)" %}">
|
||||
<i class="bi bi-arrows-angle-contract"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-light py-1 px-2" id="plan-fullscreen" title="{% translate "Plein écran" %}">
|
||||
<i class="bi bi-arrows-fullscreen"></i>
|
||||
</button>
|
||||
<a href="{{ active_floor.plan_url|default:'#' }}" target="_blank" class="btn btn-light py-1 px-2" id="plan-download-link" title="{% translate "Ouvrir / Télécharger le plan" %}" {% if not active_floor.has_plan %}style="display:none;"{% endif %}>
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Indicateur du niveau affiché -->
|
||||
<div class="badge bg-dark bg-opacity-75 text-white px-2 py-1" style="position: absolute; bottom: 12px; left: 12px; z-index: 10; font-size: 0.8rem; pointer-events: none;" id="plan-level-indicator">
|
||||
<i class="bi bi-layers me-1"></i><span id="plan-active-name">{{ active_floor.name|default:building.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Pan/Zoom Canvas -->
|
||||
<div id="plan-viewport" style="width: 100%; height: 100%; cursor: grab; display: flex; align-items: center; justify-content: center; transform-origin: center center; transition: transform 0.08s ease-out;">
|
||||
<img id="plan-image"
|
||||
src="{{ active_floor.plan_url|default:'' }}"
|
||||
alt="{% translate "Plan d'étage" %}"
|
||||
style="max-width: 95%; max-height: 95%; object-fit: contain; user-select: none; pointer-events: none; {% if not active_floor.has_plan %}display: none;{% endif %}">
|
||||
</div>
|
||||
|
||||
<!-- Empty State when floor has no plan -->
|
||||
<div id="plan-empty-state" class="text-center p-4" style="{% if active_floor.has_plan %}display: none;{% else %}display: block;{% endif %}">
|
||||
<i class="bi bi-file-earmark-image fs-1 text-muted d-block mb-2"></i>
|
||||
<h6 class="text-muted mb-1">{% translate "Aucun plan associé à ce niveau" %}</h6>
|
||||
<p class="text-muted small mb-3">{% translate "Vous pouvez téléverser un plan d'étage (SVG, image ou PDF) pour ce niveau." %}</p>
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'assets:publicbuilding_edit' building.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-upload me-1"></i>{% translate "Associer un plan" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer info du niveau actif (locaux & caractéristiques) -->
|
||||
<div class="card-footer bg-white p-3 border-top" id="plan-floor-info">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
|
||||
<div>
|
||||
<strong class="text-dark" id="info-floor-title">{{ active_floor.name|default:building.name }}</strong>
|
||||
<span class="text-muted small ms-2" id="info-floor-code">({{ active_floor.code|default:building.code }})</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<span class="badge bg-light text-dark border" id="info-floor-surface">
|
||||
{% if active_floor.surface_m2 %}
|
||||
<i class="bi bi-bounding-box me-1 text-primary"></i>{{ active_floor.surface_m2|floatformat:1 }} m²
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="badge bg-light text-dark border" id="info-floor-rooms">
|
||||
<i class="bi bi-door-closed me-1 text-primary"></i><span id="info-rooms-count">{{ active_floor.rooms_count|default:0 }}</span> {% translate "local/locaux" %}
|
||||
</span>
|
||||
<span class="badge bg-light text-dark border" id="info-floor-assets">
|
||||
<i class="bi bi-tools me-1 text-primary"></i><span id="info-assets-count">{{ active_floor.assets_count|default:0 }}</span> {% translate "équipement(s)" %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chips des locaux / pièces situés sur cet étage -->
|
||||
<div id="info-floor-rooms-chips" class="d-flex flex-wrap gap-1 mt-2">
|
||||
{% for r in active_floor.rooms %}
|
||||
<a href="{{ r.url }}" class="badge bg-light text-dark border text-decoration-none py-1 px-2" title="{{ r.location_type_display }}">
|
||||
<i class="bi bi-door-open me-1 text-primary"></i><strong>{{ r.room_number|default:r.code }}</strong> - {{ r.name }}
|
||||
</a>
|
||||
{% empty %}
|
||||
<span class="text-muted small fst-italic">{% translate "Aucun sous-local direct répertorié sur ce niveau." %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. VUE CARTE DU SITE / EMPRISE (MapLibre) -->
|
||||
<div id="section-map" style="display: none;">
|
||||
<div class="card-body p-0">
|
||||
<div id="building-map" style="height: 380px; width: 100%;"></div>
|
||||
</div>
|
||||
<div class="card-footer bg-white p-3 border-top">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Card Documents associés (placée sous les plans et la carte) -->
|
||||
<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 p-2 text-center position-relative">
|
||||
{% if doc.document_type == 'floor_plan' %}
|
||||
<span class="badge bg-primary text-white position-absolute top-0 end-0 m-1" style="font-size: 0.65rem;">{% translate "Plan" %}</span>
|
||||
<i class="bi bi-layers text-primary fs-2 mb-1"></i>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary text-white position-absolute top-0 end-0 m-1" style="font-size: 0.65rem;">{{ doc.get_document_type_display }}</span>
|
||||
<i class="bi bi-file-earmark-pdf text-danger fs-2 mb-1"></i>
|
||||
{% endif %}
|
||||
|
||||
<div class="small text-truncate w-100 fw-bold" title="{{ doc.description|default:doc.file.name }}">
|
||||
{{ doc.description|default:doc.file.name }}
|
||||
</div>
|
||||
|
||||
{% if doc.floor_location %}
|
||||
<div class="small text-muted text-truncate mb-1" style="font-size: 0.75rem;">
|
||||
<i class="bi bi-layers-half me-1"></i>{{ doc.floor_location.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.file %}
|
||||
<a href="{% url 'private_file_download' %}?app=assets&model=PublicBuildingDocument&pk={{ doc.pk }}&field=file" target="_blank" class="btn btn-sm btn-outline-secondary mt-auto py-0 px-2" style="font-size: 0.8rem;">
|
||||
<i class="bi bi-download me-1"></i>{% translate "Télécharger" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<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 "Lot / Type d'asset" %}</th>
|
||||
<th>{% translate "Caractéristiques & Spécifications" %}</th>
|
||||
<th>{% translate "Localisation spécifique" %}</th>
|
||||
<th>{% translate "Asset Parent" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th>{% translate "Prochain contrôle" %}</th>
|
||||
<th>{% translate "Maintenance & Prestataire" %}</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" style="color: {{ eq.theme_color }};">
|
||||
<i class="bi {{ eq.get_icon_class }} me-1" style="color: {{ eq.theme_icon_color }};"></i>{{ eq.code }}
|
||||
</a>
|
||||
<div class="small text-muted">{{ eq.get_name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-muted border me-1">{{ eq.get_macro_lot }}</span>
|
||||
<span class="badge {{ eq.theme_badge_class }} d-block mt-1" style="{{ eq.theme_badge_style }}">
|
||||
{{ eq.get_asset_type_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if eq.dimensions or eq.surface_m2 %}
|
||||
<div class="small fw-semibold">{% if eq.dimensions %}{{ eq.dimensions }}{% endif %}{% if eq.surface_m2 %} ({{ eq.surface_m2 }} m²){% endif %}</div>
|
||||
{% endif %}
|
||||
{% if eq.power_kw or eq.energy_source %}
|
||||
<div class="small text-muted">
|
||||
{% if eq.power_kw %}<i class="bi bi-lightning-charge me-1"></i>{{ eq.power_kw }} kW{% endif %}
|
||||
{% if eq.energy_source %}<span class="badge bg-warning-subtle text-dark border ms-1">{{ eq.get_energy_source_display }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if eq.material or eq.fire_rating or eq.glazing_type %}
|
||||
<div class="small text-muted">
|
||||
{% if eq.material %}{{ eq.material }}{% endif %}
|
||||
{% if eq.glazing_type %} - {{ eq.get_glazing_type_display }}{% endif %}
|
||||
{% if eq.fire_rating %}<span class="badge bg-danger-subtle text-danger border ms-1">{{ eq.fire_rating }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if eq.location %}
|
||||
<a href="{{ eq.location.get_absolute_url }}" class="small text-dark text-decoration-none">
|
||||
<i class="bi {{ eq.location.get_icon_class }} 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 {{ eq.parent_asset.get_icon_class }} 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>
|
||||
{% if eq.contractor_name %}
|
||||
<div class="small fw-semibold"><i class="bi bi-building me-1"></i>{{ eq.contractor_name }}</div>
|
||||
{% endif %}
|
||||
{% if eq.maintenance_contract_ref %}
|
||||
<div class="small text-muted font-monospace">Réf: {{ eq.maintenance_contract_ref }}</div>
|
||||
{% elif not eq.contractor_name %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</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="9" 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_json|safe }} || 4.3572;
|
||||
const centerLat = {{ center_lat_json|safe }} || 50.8477;
|
||||
const floorsData = {{ floors_data_json|safe }} || [];
|
||||
let currentFloor = {{ active_floor_json|safe }};
|
||||
const hasAnyPlan = {{ has_any_plan|yesno:"true,false" }};
|
||||
const levelSpecificMode = {{ level_specific_mode|yesno:"true,false" }};
|
||||
let map = null;
|
||||
|
||||
// 1. Initialisation de la carte MapLibre
|
||||
function initMap() {
|
||||
const container = document.getElementById("building-map");
|
||||
if (!container || map) 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
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Bascule entre Mode Plans d'étage et Mode Carte
|
||||
const btnTogglePlans = document.getElementById("btn-toggle-plans");
|
||||
const btnToggleMap = document.getElementById("btn-toggle-map");
|
||||
const sectionPlans = document.getElementById("section-plans");
|
||||
const sectionMap = document.getElementById("section-map");
|
||||
|
||||
function setViewMode(mode) {
|
||||
if (mode === "map") {
|
||||
btnToggleMap?.classList.add("active");
|
||||
btnTogglePlans?.classList.remove("active");
|
||||
if (sectionPlans) sectionPlans.style.display = "none";
|
||||
if (sectionMap) sectionMap.style.display = "block";
|
||||
if (!map) {
|
||||
initMap();
|
||||
} else {
|
||||
map.resize();
|
||||
}
|
||||
} else {
|
||||
btnTogglePlans?.classList.add("active");
|
||||
btnToggleMap?.classList.remove("active");
|
||||
if (sectionPlans) sectionPlans.style.display = "block";
|
||||
if (sectionMap) sectionMap.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
btnTogglePlans?.addEventListener("click", function() { setViewMode("plans"); });
|
||||
btnToggleMap?.addEventListener("click", function() { setViewMode("map"); });
|
||||
|
||||
// Par défaut, si aucun plan n'existe et qu'on n'est pas en mode spécifique à un niveau, afficher la carte
|
||||
if (!hasAnyPlan && !levelSpecificMode && (buildingGeom || (centerLon && centerLat))) {
|
||||
setViewMode("map");
|
||||
} else {
|
||||
setViewMode("plans");
|
||||
}
|
||||
|
||||
// 3. Pan & Zoom interactif sur le viewer de plan
|
||||
const planViewport = document.getElementById("plan-viewport");
|
||||
const planImage = document.getElementById("plan-image");
|
||||
const planWrapper = document.getElementById("plan-viewer-wrapper");
|
||||
const planEmptyState = document.getElementById("plan-empty-state");
|
||||
const planDownloadLink = document.getElementById("plan-download-link");
|
||||
const planActiveName = document.getElementById("plan-active-name");
|
||||
|
||||
let scale = 1;
|
||||
let translateX = 0;
|
||||
let translateY = 0;
|
||||
let isDragging = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
function updateTransform() {
|
||||
if (planViewport) {
|
||||
planViewport.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
||||
}
|
||||
}
|
||||
|
||||
function resetPlanView() {
|
||||
scale = 1;
|
||||
translateX = 0;
|
||||
translateY = 0;
|
||||
updateTransform();
|
||||
}
|
||||
|
||||
document.getElementById("plan-zoom-in")?.addEventListener("click", function() {
|
||||
scale = Math.min(scale * 1.3, 6);
|
||||
updateTransform();
|
||||
});
|
||||
|
||||
document.getElementById("plan-zoom-out")?.addEventListener("click", function() {
|
||||
scale = Math.max(scale / 1.3, 0.35);
|
||||
updateTransform();
|
||||
});
|
||||
|
||||
document.getElementById("plan-reset")?.addEventListener("click", resetPlanView);
|
||||
|
||||
document.getElementById("plan-fullscreen")?.addEventListener("click", function() {
|
||||
if (!document.fullscreenElement) {
|
||||
planWrapper?.requestFullscreen?.();
|
||||
} else {
|
||||
document.exitFullscreen?.();
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom molette
|
||||
planWrapper?.addEventListener("wheel", function(e) {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.88 : 1.14;
|
||||
scale = Math.min(Math.max(scale * delta, 0.35), 6);
|
||||
updateTransform();
|
||||
}, { passive: false });
|
||||
|
||||
// Déplacement souris (pan)
|
||||
planWrapper?.addEventListener("mousedown", function(e) {
|
||||
if (e.button !== 0) return;
|
||||
isDragging = true;
|
||||
startX = e.clientX - translateX;
|
||||
startY = e.clientY - translateY;
|
||||
if (planViewport) planViewport.style.cursor = "grabbing";
|
||||
});
|
||||
|
||||
window.addEventListener("mousemove", function(e) {
|
||||
if (!isDragging) return;
|
||||
translateX = e.clientX - startX;
|
||||
translateY = e.clientY - startY;
|
||||
updateTransform();
|
||||
});
|
||||
|
||||
window.addEventListener("mouseup", function() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
if (planViewport) planViewport.style.cursor = "grab";
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Navigation entre niveaux / étages
|
||||
function displayFloor(floor) {
|
||||
currentFloor = floor;
|
||||
resetPlanView();
|
||||
|
||||
if (planActiveName) planActiveName.textContent = floor.name;
|
||||
|
||||
// Mise à jour de l'image du plan
|
||||
if (floor.has_plan && floor.plan_url) {
|
||||
if (planImage) {
|
||||
planImage.src = floor.plan_url;
|
||||
planImage.style.display = "block";
|
||||
}
|
||||
if (planEmptyState) planEmptyState.style.display = "none";
|
||||
if (planDownloadLink) {
|
||||
planDownloadLink.href = floor.plan_url;
|
||||
planDownloadLink.style.display = "inline-block";
|
||||
}
|
||||
} else {
|
||||
if (planImage) planImage.style.display = "none";
|
||||
if (planEmptyState) planEmptyState.style.display = "block";
|
||||
if (planDownloadLink) planDownloadLink.style.display = "none";
|
||||
}
|
||||
|
||||
// Mise à jour du bandeau d'information sous le plan
|
||||
const infoTitle = document.getElementById("info-floor-title");
|
||||
const infoCode = document.getElementById("info-floor-code");
|
||||
const infoSurface = document.getElementById("info-floor-surface");
|
||||
const infoRoomsCount = document.getElementById("info-rooms-count");
|
||||
const infoAssetsCount = document.getElementById("info-assets-count");
|
||||
const chipsContainer = document.getElementById("info-floor-rooms-chips");
|
||||
|
||||
if (infoTitle) infoTitle.textContent = floor.name;
|
||||
if (infoCode) infoCode.textContent = `(${floor.code})`;
|
||||
if (infoSurface) {
|
||||
if (floor.surface_m2) {
|
||||
infoSurface.innerHTML = `<i class="bi bi-bounding-box me-1 text-primary"></i>${parseFloat(floor.surface_m2).toFixed(1)} m²`;
|
||||
infoSurface.style.display = "inline-block";
|
||||
} else {
|
||||
infoSurface.style.display = "none";
|
||||
}
|
||||
}
|
||||
if (infoRoomsCount) infoRoomsCount.textContent = floor.rooms_count || 0;
|
||||
if (infoAssetsCount) infoAssetsCount.textContent = floor.assets_count || 0;
|
||||
|
||||
// Locaux de cet étage
|
||||
if (chipsContainer) {
|
||||
chipsContainer.innerHTML = "";
|
||||
if (floor.rooms && floor.rooms.length > 0) {
|
||||
floor.rooms.forEach(function(room) {
|
||||
const chip = document.createElement("a");
|
||||
chip.href = room.url;
|
||||
chip.className = "badge bg-light text-dark border text-decoration-none py-1 px-2";
|
||||
chip.title = room.location_type_display || "";
|
||||
chip.innerHTML = `<i class="bi bi-door-open me-1 text-primary"></i><strong>${room.room_number || room.code}</strong> - ${room.name}`;
|
||||
chipsContainer.appendChild(chip);
|
||||
});
|
||||
} else {
|
||||
chipsContainer.innerHTML = '<span class="text-muted small fst-italic">{% translate "Aucun sous-local direct répertorié sur ce niveau." %}</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Écouteurs sur les boutons d'étages
|
||||
document.querySelectorAll(".floor-nav-btn").forEach(function(btn) {
|
||||
btn.addEventListener("click", function() {
|
||||
const floorId = parseInt(btn.getAttribute("data-floor-id"));
|
||||
const targetFloor = floorsData.find(f => f.id === floorId);
|
||||
if (!targetFloor) return;
|
||||
|
||||
document.querySelectorAll(".floor-nav-btn").forEach(b => {
|
||||
b.classList.remove("btn-primary", "text-white", "shadow-sm");
|
||||
b.classList.add("btn-outline-secondary");
|
||||
});
|
||||
btn.classList.remove("btn-outline-secondary");
|
||||
btn.classList.add("btn-primary", "text-white", "shadow-sm");
|
||||
|
||||
displayFloor(targetFloor);
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Gestion du repliage / dépliage global de l'arborescence
|
||||
const treeCard = document.getElementById("card-hierarchy-tree");
|
||||
if (treeCard) {
|
||||
const collapseAllBtn = document.getElementById("btn-tree-collapse-all");
|
||||
const expandAllBtn = document.getElementById("btn-tree-expand-all");
|
||||
|
||||
if (collapseAllBtn) {
|
||||
collapseAllBtn.addEventListener("click", function() {
|
||||
// Replier tous les nœuds enfants de l'arborescence (localisations et assets)
|
||||
treeCard.querySelectorAll(".tree-collapse-node").forEach(function(el) {
|
||||
if (window.bootstrap && bootstrap.Collapse) {
|
||||
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(el, { toggle: false });
|
||||
bsCollapse.hide();
|
||||
} else {
|
||||
el.classList.remove("show");
|
||||
}
|
||||
});
|
||||
// Mettre à jour l'état visuel de tous les boutons chevrons
|
||||
treeCard.querySelectorAll(".tree-toggle-btn").forEach(function(btn) {
|
||||
if (btn.getAttribute("data-bs-target") !== "#tree-root-content") {
|
||||
btn.classList.add("collapsed");
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
const icon = btn.querySelector("i");
|
||||
if (icon) {
|
||||
icon.classList.remove("bi-chevron-down");
|
||||
icon.classList.add("bi-chevron-right");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (expandAllBtn) {
|
||||
expandAllBtn.addEventListener("click", function() {
|
||||
// S'assurer que le conteneur racine est bien déplié
|
||||
const rootContent = document.getElementById("tree-root-content");
|
||||
if (rootContent) {
|
||||
if (window.bootstrap && bootstrap.Collapse) {
|
||||
bootstrap.Collapse.getOrCreateInstance(rootContent, { toggle: false }).show();
|
||||
} else {
|
||||
rootContent.classList.add("show");
|
||||
}
|
||||
}
|
||||
// Déplier tous les nœuds enfants (localisations et assets)
|
||||
treeCard.querySelectorAll(".tree-collapse-node").forEach(function(el) {
|
||||
if (window.bootstrap && bootstrap.Collapse) {
|
||||
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(el, { toggle: false });
|
||||
bsCollapse.show();
|
||||
} else {
|
||||
el.classList.add("show");
|
||||
}
|
||||
});
|
||||
// Mettre à jour l'état visuel de tous les boutons chevrons
|
||||
treeCard.querySelectorAll(".tree-toggle-btn").forEach(function(btn) {
|
||||
btn.classList.remove("collapsed");
|
||||
btn.setAttribute("aria-expanded", "true");
|
||||
const icon = btn.querySelector("i");
|
||||
if (icon) {
|
||||
icon.classList.remove("bi-chevron-right");
|
||||
icon.classList.add("bi-chevron-down");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock script %}
|
||||
|
|
@ -1,456 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% translate "Modifier le bâtiment" %} {{ building.code }} | {{ product_name }}{% 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 class="col-12 mt-3">
|
||||
<h6 class="border-bottom pb-2 text-primary fw-semibold"><i class="bi bi-door-open me-2"></i>{% translate "Spécificités du local ou du niveau (si applicable)" %}</h6>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="{{ form.room_number.id_for_label }}" class="form-label fw-semibold">{% translate "Numéro de local / porte" %}</label>
|
||||
{{ form.room_number }}
|
||||
{% if form.room_number.errors %}<div class="text-danger small mt-1">{{ form.room_number.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="{{ form.level_number.id_for_label }}" class="form-label fw-semibold">{% translate "Numéro d'étage" %}</label>
|
||||
{{ form.level_number }}
|
||||
{% if form.level_number.errors %}<div class="text-danger small mt-1">{{ form.level_number.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="{{ form.surface_m2.id_for_label }}" class="form-label fw-semibold">{% translate "Surface spécifique (m²)" %}</label>
|
||||
{{ form.surface_m2 }}
|
||||
{% if form.surface_m2.errors %}<div class="text-danger small mt-1">{{ form.surface_m2.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="{{ form.ceiling_height_m.id_for_label }}" class="form-label fw-semibold">{% translate "Hauteur sous plafond (m)" %}</label>
|
||||
{{ form.ceiling_height_m }}
|
||||
{% if form.ceiling_height_m.errors %}<div class="text-danger small mt-1">{{ form.ceiling_height_m.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.floor_type.id_for_label }}" class="form-label fw-semibold">{% translate "Revêtement de sol" %}</label>
|
||||
{{ form.floor_type }}
|
||||
{% if form.floor_type.errors %}<div class="text-danger small mt-1">{{ form.floor_type.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.fire_compartment.id_for_label }}" class="form-label fw-semibold">{% translate "Compartiment coupe-feu" %}</label>
|
||||
{{ form.fire_compartment }}
|
||||
{% if form.fire_compartment.errors %}<div class="text-danger small mt-1">{{ form.fire_compartment.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-center mt-4">
|
||||
<div class="form-check">
|
||||
{{ form.is_heated }}
|
||||
<label for="{{ form.is_heated.id_for_label }}" class="form-check-label fw-semibold">
|
||||
{% translate "Local chauffé" %}
|
||||
</label>
|
||||
</div>
|
||||
</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 %}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% translate "Bâtiments publics" %} | {{ product_name }}{% 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 "Type de localisation" %}</label>
|
||||
<select name="location_type" class="form-select form-select-sm">
|
||||
<option value="">-- {% translate "Tous les types" %} --</option>
|
||||
{% for key, label in location_type_choices %}
|
||||
<option value="{{ key }}" {% if selected_location_type == 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-1">
|
||||
<label class="form-label small text-muted">{% translate "DPE" %}</label>
|
||||
<select name="dpe_rating" class="form-select form-select-sm">
|
||||
<option value="">-- {% translate "Tous" %} --</option>
|
||||
{% for key, label in dpe_rating_choices %}
|
||||
<option value="{{ key }}" {% if selected_dpe == key %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 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">
|
||||
<i class="bi {{ b.get_icon_class }} me-1 text-primary"></i>{{ b.code }}
|
||||
</a>
|
||||
{% if b.room_number %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border small font-monospace"><i class="bi bi-tag me-1"></i>{{ b.room_number }}</span>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
{% 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="d-inline-block" style="width: 1.35rem;"></span>
|
||||
{% endif %}
|
||||
|
||||
<i class="bi {{ child_node.loc.get_icon_class }} text-primary"></i>
|
||||
|
||||
<!-- 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>
|
||||
{% if child_node.loc.room_number %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border small font-monospace"><i class="bi bi-tag me-1"></i>{{ child_node.loc.room_number }}</span>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
<span class="badge bg-info-subtle text-info border small" title="{% translate 'Assets physiques rattachés' %}">
|
||||
<i class="bi bi-tools me-1"></i>{{ child_node.assets|length }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible Content (Direct Assets & Child Locations) -->
|
||||
{% if child_node.children or child_node.assets %}
|
||||
<div id="tree-loc-{{ child_node.loc.id }}" class="collapse show tree-collapse-node">
|
||||
<!-- 1. Assets directement rattachés à cette localisation -->
|
||||
{% if child_node.assets %}
|
||||
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=child_node.assets depth=depth|default:1|add:1 only %}
|
||||
{% endif %}
|
||||
|
||||
<!-- 2. Sous-localisations -->
|
||||
{% if child_node.children %}
|
||||
{% include "assets/buildings/publicbuilding_location_tree.html" with node=child_node depth=depth|default:1|add:1 only %}
|
||||
{% endif %}
|
||||
</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>
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
{% load i18n static inspections_tags short_name %}
|
||||
|
||||
{# Inspections et contrôles liés à l'asset #}
|
||||
<div class="card mb-3" id="asset-inspections-card">
|
||||
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0 fs-6 fw-bold">
|
||||
<i class="bi bi-patch-check me-2 text-primary"></i>{% translate "Inspections et contrôles terrain" %}
|
||||
{% if inspections %}
|
||||
<span class="badge bg-secondary rounded-pill ms-1">{{ total_count }}</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if can_inspect %}
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-sm btn-trigger-asset-inspection"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#quickInspectionModal"
|
||||
data-content-type-id="{{ asset_ct_id }}"
|
||||
data-model-name="{{ asset_ct_model }}"
|
||||
data-object-id="{{ asset_pk }}"
|
||||
data-target-code="{{ asset.code|default:asset.pk }}"
|
||||
data-target-name="{{ asset.name_fr|default:asset.name_nl|default:asset }}"
|
||||
data-target-type="{% if asset.category %}{{ asset.category.name_fr }}{% else %}{% translate 'Zone / Équipement' %}{% endif %}"
|
||||
data-target-status="{% if asset.get_status_display %}{{ asset.get_status_display }}{% else %}-{% endif %}">
|
||||
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouvelle inspection" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if inspections %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 15%;" class="ps-3">{% translate "Date & Heure" %}</th>
|
||||
<th style="width: 18%;">{% translate "Statut du contrôle" %}</th>
|
||||
<th style="width: 15%;">{% translate "Nature" %}</th>
|
||||
<th style="width: 20%;">{% translate "Inspecteur" %}</th>
|
||||
<th style="width: 22%;">{% translate "Remarques" %}</th>
|
||||
<th style="width: 10%;" class="pe-3 text-end">{% translate "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for insp in inspections %}
|
||||
<tr class="{% if insp.is_cancelled %}table-light text-muted opacity-75 fst-italic{% endif %}">
|
||||
<td class="ps-3 fw-semibold">
|
||||
{{ insp.inspection_date|date:"d/m/Y H:i" }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ insp.status_badge_class }}">
|
||||
<i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<i class="bi bi-person me-1 text-muted"></i>{{ insp.inspector|short_name|default:"-" }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="small text-muted text-truncate d-inline-block" style="max-width: 220px;" title="{{ insp.details_summary }}">
|
||||
{{ insp.details_summary }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="pe-3 text-end">
|
||||
<button type="button" class="btn btn-xs btn-outline-primary px-2 py-1 rounded" data-bs-toggle="modal" data-bs-target="#assetInspectionDetailModal{{ insp.id }}">
|
||||
<i class="bi bi-eye me-1"></i>{% translate "Détail" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3 text-center text-muted small">
|
||||
<i class="bi bi-info-circle fs-5 d-block mb-1 text-secondary"></i>
|
||||
{% translate "Aucune inspection ni vérification enregistrée pour cet équipement." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for insp in inspections %}
|
||||
<!-- Modale de détail de l'inspection pour l'asset -->
|
||||
<div class="modal fade" id="assetInspectionDetailModal{{ insp.id }}" tabindex="-1" aria-labelledby="assetInspectionDetailModalLabel{{ insp.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-light py-2.5">
|
||||
<h5 class="modal-title fs-6 fw-bold" id="assetInspectionDetailModalLabel{{ insp.id }}">
|
||||
<i class="bi bi-patch-check me-2 text-primary"></i>{% translate "Détail de l'inspection" %} - {{ insp.inspection_date|date:"d/m/Y H:i" }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-3">
|
||||
{% if insp.is_cancelled %}
|
||||
<div class="alert alert-secondary py-2 px-3 small d-flex align-items-center mb-3">
|
||||
<i class="bi bi-slash-circle me-2 fs-6"></i>
|
||||
<div>
|
||||
<strong>{% translate "Inspection annulée" %}</strong> — {% translate "Cette inspection a été annulée et n'est plus prise en compte dans la conformité active de la cible." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if insp.related_intervention %}
|
||||
<div class="mb-3">
|
||||
<a href="{% url 'interventions:intervention_detail' insp.related_intervention.id %}" class="btn btn-outline-primary w-100 fw-semibold" target="_blank">
|
||||
<i class="bi bi-tools me-2"></i>{% translate "Consulter l'intervention" %} {{ insp.related_intervention.code }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul class="list-group list-group-flush mb-3">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-geo-alt me-1"></i>{% translate "Équipement cible" %}</span>
|
||||
<span class="fw-bold text-end ms-2">{{ insp.target_display }}</span>
|
||||
</li>
|
||||
{% if insp.related_intervention %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-tools me-1"></i>{% translate "Intervention liée" %}</span>
|
||||
<span class="fw-semibold text-end ms-2">
|
||||
<a href="{% url 'interventions:intervention_detail' insp.related_intervention.id %}" class="text-decoration-none fw-bold" target="_blank">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>{{ insp.related_intervention.code }}
|
||||
</a>
|
||||
{% if insp.related_intervention.sub_type %}
|
||||
<span class="badge bg-light text-dark border ms-1">{{ insp.related_intervention.sub_type.get_name }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-shield-check me-1"></i>{% translate "Résultat du contrôle" %}</span>
|
||||
<span class="badge {{ insp.status_badge_class }}"><i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-eye me-1"></i>{% translate "Nature" %}</span>
|
||||
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-person me-1"></i>{% translate "Inspecteur" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:"-" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-clock-history me-1"></i>{% translate "Date de modification" %}</span>
|
||||
<span class="fw-semibold">{{ insp.updated_at|date:"d/m/Y H:i" }}</span>
|
||||
</li>
|
||||
|
||||
|
||||
{% get_inspection_detail_template insp as detail_template %}
|
||||
{% if detail_template %}
|
||||
{% include detail_template with insp=insp %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
{% if insp.notes and not insp.is_giep %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small fw-semibold"><i class="bi bi-journal-text me-1"></i>{% translate "Remarques / Observations" %}:</label>
|
||||
<div class="p-2 bg-light rounded border text-secondary small">
|
||||
{{ insp.notes|linebreaksbr }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if insp.documents.all and not insp.is_giep %}
|
||||
<div>
|
||||
<label class="form-label text-muted small fw-semibold mb-2"><i class="bi bi-camera me-1"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }}):</label>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
{% for doc in insp.documents.all %}
|
||||
{% if doc.is_image %}
|
||||
<a href="{{ doc.file_url }}" target="_blank" class="d-inline-block border rounded p-1 bg-light" title="{{ doc.filename }}">
|
||||
<img data-src="{{ doc.file_url }}" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3C/svg%3E" loading="lazy" alt="{{ doc.filename }}" class="rounded" style="width: 75px; height: 75px; object-fit: cover;">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ doc.file_url }}" target="_blank" class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center gap-1" title="{{ doc.filename }}">
|
||||
<i class="bi bi-file-earmark-arrow-down text-primary"></i>
|
||||
<span class="text-truncate" style="max-width: 160px;">{{ doc.filename }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer py-2 bg-light">
|
||||
{% can_edit_inspection insp request.user as user_can_edit %}
|
||||
{% if user_can_edit %}
|
||||
{% if insp.is_cancelled %}
|
||||
<button type="button" class="btn btn-outline-success btn-sm fw-semibold" onclick="handleToggleCancelAssetInspection({{ insp.id }}, 'restore')">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Rétablir l'inspection" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm fw-semibold" onclick="handleToggleCancelAssetInspection({{ insp.id }}, 'cancel')">
|
||||
<i class="bi bi-x-circle me-1"></i>{% translate "Annuler l'inspection" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm fw-semibold" onclick="openEditInspectionModal({{ insp.id }})" data-bs-dismiss="modal">
|
||||
<i class="bi bi-pencil me-1"></i>{% translate "Modifier l'inspection" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if can_inspect %}
|
||||
{% include "inspections/components/inspection_form_modal.html" %}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const triggerBtns = document.querySelectorAll('.btn-trigger-asset-inspection');
|
||||
triggerBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const ctId = this.getAttribute('data-content-type-id');
|
||||
const objId = this.getAttribute('data-object-id');
|
||||
const code = this.getAttribute('data-target-code');
|
||||
const name = this.getAttribute('data-target-name');
|
||||
const type = this.getAttribute('data-target-type');
|
||||
const status = this.getAttribute('data-target-status');
|
||||
|
||||
const modalCtInput = document.getElementById('insp_content_type_id');
|
||||
const modalObjInput = document.getElementById('insp_object_id');
|
||||
const modalTypeCode = document.getElementById('insp_target_code');
|
||||
const modalTypeName = document.getElementById('insp_target_name');
|
||||
const modalTypeBadge = document.getElementById('insp_target_type');
|
||||
const modalStatusBadge = document.getElementById('insp_target_status');
|
||||
|
||||
if (modalCtInput) modalCtInput.value = ctId;
|
||||
if (modalObjInput) modalObjInput.value = objId;
|
||||
if (modalTypeCode) modalTypeCode.textContent = code;
|
||||
if (modalTypeName) modalTypeName.textContent = name;
|
||||
if (modalTypeBadge) modalTypeBadge.textContent = type;
|
||||
if (modalStatusBadge) modalStatusBadge.textContent = status;
|
||||
|
||||
const modelName = (this.getAttribute('data-model-name') || '').toLowerCase();
|
||||
const codeUpper = (code || '').toUpperCase();
|
||||
const isRwiz = (modelName === 'naturerwiz' || codeUpper.startsWith('RWIZ'));
|
||||
if (typeof window.toggleMissingResultChoice === 'function') {
|
||||
window.toggleMissingResultChoice(isRwiz);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const form = document.getElementById('quickInspectionForm');
|
||||
if (form && !form.dataset.ajaxHandlerAttached) {
|
||||
form.dataset.ajaxHandlerAttached = 'true';
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('submitQuickInspectionBtn');
|
||||
const spinner = document.getElementById('submitQuickInspectionSpinner');
|
||||
const icon = document.getElementById('submitQuickInspectionIcon');
|
||||
const alertBox = document.getElementById('quickInspectionAlert');
|
||||
|
||||
if (alertBox) {
|
||||
alertBox.classList.add('d-none');
|
||||
alertBox.textContent = '';
|
||||
}
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
if (spinner) spinner.classList.remove('d-none');
|
||||
if (icon) icon.classList.add('d-none');
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (res.redirected) {
|
||||
window.location.href = res.url;
|
||||
return null;
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data) return;
|
||||
if (data.success) {
|
||||
const modalEl = document.getElementById('quickInspectionModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) modal.hide();
|
||||
window.location.reload();
|
||||
} else {
|
||||
if (alertBox) {
|
||||
{% translate "Erreur lors de l'enregistrement." as err_save_msg %}
|
||||
alertBox.textContent = data.error || "{{ err_save_msg|escapejs }}";
|
||||
alertBox.classList.remove('d-none');
|
||||
}
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
if (spinner) spinner.classList.add('d-none');
|
||||
if (icon) icon.classList.remove('d-none');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Erreur lors de l'enregistrement de l'inspection:", err);
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
function loadLazyModalImages(modal) {
|
||||
if (!modal || typeof modal.querySelectorAll !== 'function') return;
|
||||
modal.querySelectorAll('img[data-src]').forEach(function(img) {
|
||||
const dataSrc = img.getAttribute('data-src');
|
||||
if (dataSrc) {
|
||||
img.src = dataSrc;
|
||||
img.removeAttribute('data-src');
|
||||
}
|
||||
});
|
||||
}
|
||||
document.addEventListener('show.bs.modal', function(e) {
|
||||
loadLazyModalImages(e.target);
|
||||
});
|
||||
document.addEventListener('shown.bs.modal', function(e) {
|
||||
loadLazyModalImages(e.target);
|
||||
});
|
||||
})();
|
||||
|
||||
window.handleToggleCancelAssetInspection = function(inspectionId, action) {
|
||||
const confirmMsg = action === 'cancel'
|
||||
? "{% filter escapejs %}{% translate 'Êtes-vous sûr de vouloir annuler cette inspection ?' %}{% endfilter %}"
|
||||
: "{% filter escapejs %}{% translate 'Êtes-vous sûr de vouloir rétablir cette inspection ?' %}{% endfilter %}";
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, 10) === 'csrftoken=') {
|
||||
cookieValue = decodeURIComponent(cookie.substring(10));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', action);
|
||||
|
||||
fetch(`/inspections/api/${inspectionId}/toggle-cancel/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': cookieValue
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.error || "{% filter escapejs %}{% translate 'Une erreur est survenue.' %}{% endfilter %}");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert("{% filter escapejs %}{% translate 'Erreur de communication avec le serveur.' %}{% endfilter %}");
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{% include "inspections/components/edit_inspection_modal.html" %}
|
||||
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
{# Interventions liées à l'asset #}
|
||||
<div class="card mb-3" id="asset-interventions-card">
|
||||
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-wrench-adjustable me-2"></i>{% translate "Interventions liées" %}
|
||||
{% if interventions %}
|
||||
<span class="badge bg-secondary rounded-pill ms-1">{{ interventions|length }}</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if can_add_intervention %}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#createInterventionForAssetModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>{% translate "Créer une intervention" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if interventions %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 12%;" class="ps-3">{% translate "Code" %}</th>
|
||||
<th style="width: 25%;">{% translate "Titre" %}</th>
|
||||
<th style="width: 18%;">{% translate "Symptôme" %}</th>
|
||||
<th style="width: 15%;">{% translate "Statut" %}</th>
|
||||
<th style="width: 10%;">{% translate "Priorité" %}</th>
|
||||
<th style="width: 12%;">{% translate "Attribué à" %}</th>
|
||||
<th style="width: 8%;" class="pe-3">{% translate "Date" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for intervention in interventions %}
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<a href="{% url 'interventions:intervention_detail' intervention.id %}" class="fw-bold text-decoration-none">
|
||||
{{ intervention.code }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'interventions:intervention_detail' intervention.id %}" class="text-reset text-decoration-none">
|
||||
{{ intervention.title }}
|
||||
</a>
|
||||
{% if intervention.maintain_type == 'corrective' %}
|
||||
<span class="badge bg-danger ms-1" title="{% translate 'Maintenance corrective' %}">{% translate "Corrective" %}</span>
|
||||
{% elif intervention.maintain_type == 'preventive' %}
|
||||
<span class="badge bg-warning text-dark ms-1" title="{% translate 'Maintenance préventive' %}">{% translate "Préventive" %}</span>
|
||||
{% elif intervention.maintain_type == 'ameliorative' %}
|
||||
<span class="badge bg-success ms-1" title="{% translate 'Maintenance améliorative' %}">{% translate "Améliorative" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if intervention.symptom %}
|
||||
{{ intervention.symptom.get_name }}
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: {{ intervention.status_color_hex }}; color: {{ intervention.status_text_color_hex }};">
|
||||
{{ intervention.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if intervention.priority == '1' %}
|
||||
<span class="badge bg-danger">{{ intervention.get_priority_display }}</span>
|
||||
{% elif intervention.priority == '2' %}
|
||||
<span class="badge bg-warning text-dark">{{ intervention.get_priority_display }}</span>
|
||||
{% elif intervention.priority == '3' %}
|
||||
<span class="badge bg-secondary">{{ intervention.get_priority_display }}</span>
|
||||
{% elif intervention.priority == '4' %}
|
||||
<span class="badge bg-light text-dark border">{{ intervention.get_priority_display }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ intervention.get_priority_display|default:"-" }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if intervention.assigned_team %}
|
||||
<span class="small">{{ intervention.assigned_team.name }}</span>
|
||||
{% elif intervention.assigned_provider %}
|
||||
<span class="small">{{ intervention.assigned_provider.name }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="pe-3">
|
||||
<small class="text-muted" title="{% if intervention.end_time %}{% translate 'Date de réalisation' %}{% elif intervention.expected_end_time %}{% translate 'Échéance prévisionnelle' %}{% elif intervention.expected_begin_time %}{% translate 'Date de début prévisionnelle' %}{% else %}{% translate 'Date de création' %}{% endif %}">
|
||||
{{ intervention.end_time|default:intervention.expected_end_time|default:intervention.expected_begin_time|default:intervention.creation_time|date:"d/m/Y" }}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3 text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>{% translate "Aucune intervention liée à cet asset." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if can_add_intervention %}
|
||||
<!-- Modal de création d'une intervention depuis l'asset -->
|
||||
<div class="modal fade" id="createInterventionForAssetModal" tabindex="-1" aria-labelledby="createInterventionForAssetModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title" id="createInterventionForAssetModalLabel">
|
||||
<i class="bi bi-wrench-adjustable me-2"></i>{% translate "Nouvelle intervention" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||
</div>
|
||||
<form id="createInterventionForAssetForm" action="{% url 'assets:create_intervention_for_asset' asset_model asset.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div id="createInterventionAlert" class="alert alert-danger d-none mb-3"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold required">{% translate "Titre" %}</label>
|
||||
<input type="text" name="title" class="form-control" placeholder="{% translate "Titre de l'intervention" %}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{% translate "Symptôme" %}</label>
|
||||
<select name="symptom_id" id="asset_intervention_symptom_select" class="form-select">
|
||||
<option value="">{% translate "-- Sélectionner un symptôme --" %}</option>
|
||||
{% for s in symptoms %}
|
||||
<option value="{{ s.id }}" data-name="{{ s.get_name }}" data-priority="{{ s.priority|default:'3' }}">{{ s.get_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold d-block">{% translate "Type de maintenance" %}</label>
|
||||
<div class="btn-group w-100" role="group" aria-label="{% translate 'Type de maintenance' %}">
|
||||
<input type="radio" class="btn-check" name="maintain_type" id="maintain_type_corrective" value="corrective" checked>
|
||||
<label class="btn btn-outline-danger d-flex align-items-center justify-content-center gap-1 py-2" for="maintain_type_corrective">
|
||||
<i class="bi bi-wrench"></i>
|
||||
<span>{% translate "Corrective" %}</span>
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="maintain_type" id="maintain_type_preventive" value="preventive">
|
||||
<label class="btn btn-outline-warning text-dark d-flex align-items-center justify-content-center gap-1 py-2" for="maintain_type_preventive">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
<span>{% translate "Préventive" %}</span>
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="maintain_type" id="maintain_type_ameliorative" value="ameliorative">
|
||||
<label class="btn btn-outline-success d-flex align-items-center justify-content-center gap-1 py-2" for="maintain_type_ameliorative">
|
||||
<i class="bi bi-arrow-up-circle"></i>
|
||||
<span>{% translate "Améliorative" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{% translate "Priorité" %}</label>
|
||||
<select name="priority" id="asset_intervention_priority_select" class="form-select">
|
||||
<option value="1">{% translate "Urgent" %}</option>
|
||||
<option value="2">{% translate "Élevé" %}</option>
|
||||
<option value="3" selected>{% translate "Normal" %}</option>
|
||||
<option value="4">{% translate "Bas" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{% translate "Description / Remarques" %}</label>
|
||||
<textarea name="init_description" class="form-control" rows="3" placeholder="{% translate 'Détails ou remarques globales...' %}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitCreateInterventionBtn">
|
||||
<span class="spinner-border spinner-border-sm me-1 d-none" id="submitCreateInterventionSpinner" role="status"></span>
|
||||
<i class="bi bi-check-circle me-1" id="submitCreateInterventionIcon"></i>{% translate "Créer" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const form = document.getElementById('createInterventionForAssetForm');
|
||||
if (!form) return;
|
||||
|
||||
const symptomSelect = document.getElementById('asset_intervention_symptom_select');
|
||||
const titleInput = form.querySelector('input[name="title"]');
|
||||
const prioritySelect = document.getElementById('asset_intervention_priority_select');
|
||||
let lastAutoTitle = '';
|
||||
|
||||
if (symptomSelect && titleInput) {
|
||||
symptomSelect.addEventListener('change', function() {
|
||||
const selectedOpt = symptomSelect.options[symptomSelect.selectedIndex];
|
||||
if (selectedOpt && selectedOpt.value) {
|
||||
const symName = selectedOpt.getAttribute('data-name') || selectedOpt.text.trim();
|
||||
const symPriority = selectedOpt.getAttribute('data-priority');
|
||||
|
||||
if (!titleInput.value || titleInput.value === lastAutoTitle) {
|
||||
titleInput.value = symName;
|
||||
lastAutoTitle = symName;
|
||||
}
|
||||
|
||||
if (symPriority && prioritySelect) {
|
||||
prioritySelect.value = symPriority;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
titleInput.addEventListener('input', function() {
|
||||
if (titleInput.value !== lastAutoTitle) {
|
||||
lastAutoTitle = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const alertDiv = document.getElementById('createInterventionAlert');
|
||||
const submitBtn = document.getElementById('submitCreateInterventionBtn');
|
||||
const spinner = document.getElementById('submitCreateInterventionSpinner');
|
||||
const icon = document.getElementById('submitCreateInterventionIcon');
|
||||
|
||||
alertDiv.classList.add('d-none');
|
||||
alertDiv.textContent = '';
|
||||
submitBtn.disabled = true;
|
||||
if (spinner) spinner.classList.remove('d-none');
|
||||
if (icon) icon.classList.add('d-none');
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
} else {
|
||||
alertDiv.textContent = data.error || '{% translate "Une erreur est survenue." %}';
|
||||
alertDiv.classList.remove('d-none');
|
||||
submitBtn.disabled = false;
|
||||
if (spinner) spinner.classList.add('d-none');
|
||||
if (icon) icon.classList.remove('d-none');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alertDiv.textContent = '{% translate "Erreur réseau lors de la création." %}';
|
||||
alertDiv.classList.remove('d-none');
|
||||
submitBtn.disabled = false;
|
||||
if (spinner) spinner.classList.add('d-none');
|
||||
if (icon) icon.classList.remove('d-none');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
<!-- Modale de Saisie Rapide d'Inspection -->
|
||||
<div class="modal fade" id="quickInspectionModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="quickInspectionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content shadow">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold" id="quickInspectionModalLabel">
|
||||
<i class="bi bi-search text-primary me-2"></i>{% translate "Nouvelle inspection" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||
</div>
|
||||
|
||||
<form id="quickInspectionForm" action="{% url 'assets:create_inspection_api' %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="content_type_id" id="insp_content_type_id">
|
||||
<input type="hidden" name="object_id" id="insp_object_id">
|
||||
<input type="hidden" name="nature" value="visual">
|
||||
<input type="hidden" name="inspector_role" value="observer">
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Target Card Preview -->
|
||||
<div class="card mb-3 bg-light border-0">
|
||||
<div class="card-body p-2 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<span class="badge bg-secondary mb-1" id="insp_target_type">Asset</span>
|
||||
<h6 class="mb-0 fw-bold" id="insp_target_code">#12345</h6>
|
||||
<small class="text-muted" id="insp_target_name">-</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="small text-muted d-block">{% translate "Statut actuel" %}</span>
|
||||
<span class="badge bg-info text-dark" id="insp_target_status">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="quickInspectionAlert" class="alert alert-danger d-none mb-3"></div>
|
||||
|
||||
<!-- Résultat de contrôle avec boutons colorés -->
|
||||
<div class="mb-3" id="quickInspResultSection">
|
||||
<label class="form-label fw-semibold d-block">{% translate "Résultat du contrôle" %} <span class="text-danger">*</span></label>
|
||||
<div class="d-flex flex-column gap-2" role="group" aria-label="{% translate 'Résultat' %}">
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<input type="radio" class="btn-check" name="result_status" id="res_compliant" value="compliant">
|
||||
<label class="btn btn-outline-success w-100 py-2 d-flex align-items-center justify-content-center gap-1" for="res_compliant">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<span>{% translate "Conforme" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="radio" class="btn-check" name="result_status" id="res_non_compliant" value="non_compliant">
|
||||
<label class="btn btn-outline-warning text-dark w-100 py-2 d-flex align-items-center justify-content-center gap-1" for="res_non_compliant">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
<span>{% translate "Non conforme" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-4">
|
||||
<input type="radio" class="btn-check" name="result_status" id="res_damaged" value="damaged">
|
||||
<label class="btn btn-outline-danger w-100 py-2 d-flex align-items-center justify-content-center gap-1 small" for="res_damaged">
|
||||
<i class="bi bi-x-circle-fill"></i>
|
||||
<span>{% translate "Endommagé" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-4" id="res_container_missing">
|
||||
<input type="radio" class="btn-check" name="result_status" id="res_missing" value="missing">
|
||||
<label class="btn btn-outline-dark w-100 py-2 d-flex align-items-center justify-content-center gap-1 small" for="res_missing" id="res_label_missing">
|
||||
<i class="bi bi-dash-circle-fill"></i>
|
||||
<span>{% translate "Absent" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<input type="radio" class="btn-check" name="result_status" id="res_updated" value="updated">
|
||||
<label class="btn btn-outline-info text-dark w-100 py-2 d-flex align-items-center justify-content-center gap-1 small" for="res_updated">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
<span>{% translate "Corrigé" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Photos (Mode continu & Fichiers) -->
|
||||
<div class="card border border-success-subtle mb-3" style="background-color: #e8f6f3;" id="quickInspPhotosSection">
|
||||
<div class="card-header bg-transparent border-0 pb-0 pt-2 px-3">
|
||||
<span class="fw-bold text-dark small"><i class="bi bi-camera me-1 text-teal"></i>{% translate "Photos & Fichiers" %}</span>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" id="insp_btn_continuous_camera" class="btn btn-outline-secondary bg-white text-dark py-2 d-flex align-items-center justify-content-center gap-2 shadow-sm" style="border-color: #ced4da;">
|
||||
<i class="bi bi-camera me-1"></i>
|
||||
<span>{% translate "Prendre des photos (mode continu)" %}</span>
|
||||
</button>
|
||||
<button type="button" id="insp_btn_gallery_files" class="btn btn-outline-secondary bg-white text-dark py-2 d-flex align-items-center justify-content-center gap-2 shadow-sm" style="border-color: #ced4da;">
|
||||
<i class="bi bi-folder2-open me-1"></i>
|
||||
<span>{% translate "Photos, vidéos, fichiers" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Inputs masqués -->
|
||||
<input type="file" id="insp_camera_input" accept="image/*" capture="environment" class="d-none">
|
||||
<input type="file" id="insp_gallery_input" accept="image/*,.pdf,.doc,.docx,.xls,.xlsx" multiple class="d-none">
|
||||
|
||||
<!-- Grille de prévisualisation des photos/fichiers accumulés -->
|
||||
<div id="insp_photos_preview_container" class="row g-2 mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champs spécifiques au type d'asset -->
|
||||
<div id="assetSpecificFieldsContainer">
|
||||
{% if inspection_template %}
|
||||
{% include inspection_template %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Option Création automatique d'une intervention -->
|
||||
<div class="form-check form-switch mb-3 bg-light p-2 rounded ps-4" id="createInterventionSwitchContainer">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="create_intervention" value="true" id="createInterventionSwitch">
|
||||
<label class="form-check-label small fw-semibold" for="createInterventionSwitch">
|
||||
{% translate "Créer automatiquement une intervention en cas d'anomalie / dégât" %}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Remarques -->
|
||||
<div class="mb-3" id="quickInspNotesSection">
|
||||
<label class="form-label fw-semibold mb-1">{% translate "Remarques / Remarques du constat" %}</label>
|
||||
<textarea name="notes" id="insp_notes" class="form-control" rows="2" placeholder="{% translate 'Saisir vos remarques ou détails...' %}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitQuickInspectionBtn">
|
||||
<span class="spinner-border spinner-border-sm me-1 d-none" id="submitQuickInspectionSpinner"></span>
|
||||
<i class="bi bi-check-lg me-1" id="submitQuickInspectionIcon"></i>
|
||||
{% translate "Valider l'inspection" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal caméra Canvas (mode continu pour inspection) -->
|
||||
<div class="modal fade" id="inspCameraModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="inspCameraModalLabel" aria-hidden="true" style="z-index: 1060;">
|
||||
<div class="modal-dialog modal-fullscreen-sm-down d-flex m-0 h-100 mx-auto">
|
||||
<div class="modal-content bg-dark text-white d-flex flex-column">
|
||||
<div class="modal-header border-0 flex-shrink-0 py-2">
|
||||
<h5 class="modal-title fw-bold" id="inspCameraModalLabel">
|
||||
<i class="bi bi-camera-video me-1"></i>{% translate "Caméra (Mode continu)" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}" id="inspCameraModalCloseBtn"></button>
|
||||
</div>
|
||||
<div class="modal-body d-flex flex-column p-2 flex-grow-1" style="overflow: hidden; min-height: 0;">
|
||||
<div class="flex-grow-1 d-flex align-items-center justify-content-center" style="min-height: 0; overflow: hidden;">
|
||||
<div class="position-relative w-100 h-100 d-flex align-items-center justify-content-center" style="border-radius: 0.5rem; overflow: hidden;">
|
||||
<video id="inspCameraPreview" playsinline autoplay style="max-width: 100%; max-height: 100%; object-fit: contain;"></video>
|
||||
<div id="inspCameraFlashOverlay" style="position: absolute; inset: 0; background: rgba(255, 255, 255, 0.9); opacity: 0; pointer-events: none; transition: opacity 120ms ease-out; z-index: 2;"></div>
|
||||
<canvas id="inspCameraCanvas" class="d-none"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div id="inspCameraStatus" class="small text-center text-muted my-2 flex-shrink-0"></div>
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 mb-2 flex-shrink-0">
|
||||
<button type="button" class="btn btn-outline-light flex-fill py-2" id="inspCameraStopBtn">
|
||||
<i class="bi bi-x-circle me-1"></i>{% translate "Terminer" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary flex-fill py-2 fw-semibold" id="inspCameraCaptureBtn">
|
||||
<i class="bi bi-camera-fill me-1"></i>{% translate "Capturer" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modale de Choix d'Asset (Zone dense / Sélection multiple) -->
|
||||
<div class="modal fade" id="targetChooserModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="targetChooserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content shadow">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold" id="targetChooserModalLabel">
|
||||
<i class="bi bi-geo-fill text-primary me-2"></i>{% translate "Choisir un équipement à inspecter" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body p-2">
|
||||
<p class="text-muted small px-2 mb-2">{% translate "Plusieurs équipements ont été détectés à cet emplacement. Veuillez sélectionner celui à inspecter :" %}</p>
|
||||
<div class="list-group" id="targetChooserList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
{% extends "assets/detail_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load asset_tags %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
/* Adaptations mobiles avancées pour le template générique d'asset */
|
||||
@media (max-width: 767.98px) {
|
||||
#asset-detail-container {
|
||||
padding-left: 0.5rem !important;
|
||||
padding-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* En-tête mobile compact */
|
||||
.generic-asset-mobile-header {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 0.85rem;
|
||||
padding: 0.85rem;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.05);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.generic-asset-mobile-header .asset-thematic-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 0.75rem;
|
||||
background-color: rgba(47, 62, 139, 0.1);
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Barre d'onglets de navigation segmentée */
|
||||
.mobile-asset-tabs-nav {
|
||||
position: sticky;
|
||||
top: 3.5rem;
|
||||
z-index: 1015;
|
||||
background: #ffffff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.3rem;
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.06);
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
.mobile-asset-tabs-nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.mobile-tab-btn {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 0.55rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
transition: all 0.15s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.mobile-tab-btn.active {
|
||||
background-color: #0f172a;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(47, 62, 139, 0.25);
|
||||
}
|
||||
|
||||
/* Cartes et sections sur mobile */
|
||||
#asset-detail-container .card {
|
||||
border-radius: 0.85rem !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.05);
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
#asset-detail-container .card-header {
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-top-left-radius: 0.85rem !important;
|
||||
border-top-right-radius: 0.85rem !important;
|
||||
}
|
||||
#asset-detail-container .card-header h5 {
|
||||
font-size: 0.95rem !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
#asset-detail-container .card-body {
|
||||
padding: 0.75rem 0.85rem;
|
||||
}
|
||||
|
||||
/* Tableaux tactiles */
|
||||
#asset-detail-container .table th {
|
||||
font-size: 0.84rem;
|
||||
padding: 0.5rem 0.4rem;
|
||||
width: 38% !important;
|
||||
}
|
||||
#asset-detail-container .table td {
|
||||
font-size: 0.86rem;
|
||||
padding: 0.5rem 0.4rem;
|
||||
}
|
||||
|
||||
/* Barre d'édition CMS mobile */
|
||||
#asset-cms-edit-bar.mobile-edit-bar {
|
||||
top: 3.5rem;
|
||||
z-index: 1020;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #b8c5d3;
|
||||
}
|
||||
|
||||
/* Carte MapLibre sur mobile */
|
||||
#asset-map {
|
||||
height: 320px !important;
|
||||
border-radius: 0 0 0.85rem 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock head %}
|
||||
|
||||
{# ── En-tête mobile readonly avec icône, thématique et badges ── #}
|
||||
{% block asset_header_title_readonly %}
|
||||
<div class="generic-asset-mobile-header">
|
||||
<div class="d-flex align-items-center gap-2.5">
|
||||
<div class="asset-thematic-avatar" {% if thematic.color %}style="background-color: {{ thematic.color }}18; color: {{ thematic.color }};"{% endif %}>
|
||||
{% if thematic.icon %}
|
||||
<i class="bi {{ thematic.icon }}"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-pin-map"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-grow-1 text-truncate">
|
||||
<div class="d-flex align-items-center gap-1.5 flex-wrap">
|
||||
<span class="fw-bold text-dark fs-6" style="color: #0f172a;">{{ asset.code }}</span>
|
||||
{% if asset.status == 'active' %}
|
||||
<span class="badge bg-success">{{ asset.get_status_display }}</span>
|
||||
{% elif asset.status == 'damaged' or asset.status == 'to_replace' %}
|
||||
<span class="badge bg-warning text-dark">{{ asset.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ asset.get_status_display|default:asset.status }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="small text-secondary text-truncate">
|
||||
{% if thematic.name_fr %}
|
||||
<span>{{ thematic.name_fr }}</span> •
|
||||
{% endif %}
|
||||
<span>{{ asset_verbose_name }}</span>
|
||||
{% if asset.name_fr %}
|
||||
— <span class="fw-semibold text-dark">{{ asset.name_fr }}</span>
|
||||
{% elif asset.model %}
|
||||
— <span class="fw-semibold text-dark">{{ asset.model.get_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Barre de navigation par onglets segmentés (Infos, Carte, Documents, Liens) #}
|
||||
<div class="mobile-asset-tabs-nav" id="mobileAssetTabsNav">
|
||||
<button type="button" class="mobile-tab-btn active" data-target-tab="tab-infos">
|
||||
<i class="bi bi-card-checklist"></i>{% translate "Informations" %}
|
||||
</button>
|
||||
<button type="button" class="mobile-tab-btn" data-target-tab="tab-geo">
|
||||
<i class="bi bi-geo-alt"></i>{% translate "Carte & Localisation" %}
|
||||
</button>
|
||||
<button type="button" class="mobile-tab-btn" data-target-tab="tab-docs">
|
||||
<i class="bi bi-paperclip"></i>{% translate "Documents & Photos" %}
|
||||
</button>
|
||||
<button type="button" class="mobile-tab-btn" data-target-tab="tab-links">
|
||||
<i class="bi bi-link-45deg"></i>{% translate "Assets Liés" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endblock asset_header_title_readonly %}
|
||||
|
||||
{# ── Script pour la gestion des onglets mobiles ── #}
|
||||
{% block extra_scripts %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
(function() {
|
||||
function initMobileAssetTabs() {
|
||||
const tabButtons = document.querySelectorAll('.mobile-tab-btn');
|
||||
const container = document.getElementById('asset-detail-container');
|
||||
if (!tabButtons.length || !container) return;
|
||||
|
||||
// Identification des cartes par catégorie
|
||||
const mainCol = container.querySelector('.col-12, .col-md-8');
|
||||
const sideCol = container.querySelector('.col-md-4, .col-12:last-child');
|
||||
|
||||
// Assigner les classes de section aux cartes
|
||||
// 1. Informations
|
||||
const infoCards = [
|
||||
container.querySelector('.card:has(table)'),
|
||||
document.getElementById('model-card'),
|
||||
container.querySelector('.card:has(#observations-card)'),
|
||||
].filter(Boolean);
|
||||
|
||||
// 2. Géolocalisation & Carte
|
||||
const geoCards = [
|
||||
container.querySelector('.card:has(#asset-map)'),
|
||||
container.querySelector('.card:has(.asset-location-link)'),
|
||||
].filter(Boolean);
|
||||
|
||||
// 3. Documents
|
||||
const docCards = [
|
||||
document.getElementById('documents-card') || container.querySelector('.card:has(#documents-list)') || container.querySelector('.card:has(.document-item)')
|
||||
].filter(Boolean);
|
||||
|
||||
// 4. Assets Liés & Remplacements
|
||||
const linkCards = [
|
||||
container.querySelector('.card:has(.replacement-chain)'),
|
||||
container.querySelector('.card:has(#associated-assets-list)'),
|
||||
container.querySelector('.card:has(#linked-interventions-list)'),
|
||||
].filter(Boolean);
|
||||
|
||||
// Initialiser les groupes
|
||||
infoCards.forEach(c => c && c.classList.add('mobile-section-infos'));
|
||||
geoCards.forEach(c => c && c.classList.add('mobile-section-geo'));
|
||||
docCards.forEach(c => c && c.classList.add('mobile-section-docs'));
|
||||
linkCards.forEach(c => c && c.classList.add('mobile-section-links'));
|
||||
|
||||
function showSection(targetTab) {
|
||||
tabButtons.forEach(btn => {
|
||||
if (btn.dataset.targetTab === targetTab) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
const allCustomSections = container.querySelectorAll(
|
||||
'.mobile-section-infos, .mobile-section-geo, .mobile-section-docs, .mobile-section-links'
|
||||
);
|
||||
|
||||
if (targetTab === 'tab-infos') {
|
||||
allCustomSections.forEach(c => c.style.display = 'none');
|
||||
infoCards.forEach(c => c.style.display = '');
|
||||
// Si aucune carte taguée infos, tout afficher
|
||||
if (!infoCards.length) allCustomSections.forEach(c => c.style.display = '');
|
||||
} else if (targetTab === 'tab-geo') {
|
||||
allCustomSections.forEach(c => c.style.display = 'none');
|
||||
geoCards.forEach(c => c.style.display = '');
|
||||
// Redimensionner la carte MapLibre pour s'adapter au conteneur
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 100);
|
||||
} else if (targetTab === 'tab-docs') {
|
||||
allCustomSections.forEach(c => c.style.display = 'none');
|
||||
docCards.forEach(c => c.style.display = '');
|
||||
} else if (targetTab === 'tab-links') {
|
||||
allCustomSections.forEach(c => c.style.display = 'none');
|
||||
linkCards.forEach(c => c.style.display = '');
|
||||
}
|
||||
}
|
||||
|
||||
tabButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
showSection(btn.dataset.targetTab);
|
||||
});
|
||||
});
|
||||
|
||||
// Par défaut, afficher les infos
|
||||
showSection('tab-infos');
|
||||
|
||||
// Si on active le mode édition, s'assurer que les champs éditables restent interactifs
|
||||
document.addEventListener('assetEditModeChanged', (e) => {
|
||||
if (e.detail && e.detail.editing) {
|
||||
// En mode édition, toutes les cartes peuvent être consultées via les onglets
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initMobileAssetTabs);
|
||||
} else {
|
||||
initMobileAssetTabs();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock extra_scripts %}
|
||||
|
||||
{% block script %}
|
||||
{{ block.super }}
|
||||
{% endblock script %}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% translate "Supprimer la zone de gestion" %} {{ zone.code }} | {{ product_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5" style="max-width: 600px;">
|
||||
<div class="card shadow border-danger">
|
||||
<div class="card-header bg-danger text-white py-3">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{% translate "Confirmer la suppression" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<p>{% translate "Êtes-vous certain de vouloir supprimer définitivement la zone de gestion suivante ?" %}</p>
|
||||
<div class="alert alert-light border">
|
||||
<h6 class="fw-bold mb-1">{{ zone.code }} - {{ zone.get_name }}</h6>
|
||||
<div class="small text-muted">{{ zone.thematic.name }} {% if zone.zone_type %}| {{ zone.zone_type.get_name }}{% endif %}</div>
|
||||
</div>
|
||||
<p class="text-danger small">
|
||||
<i class="bi bi-info-circle me-1"></i>{% translate "Cette action est irréversible. Les documents et associations liés à cette zone seront supprimés." %}
|
||||
</p>
|
||||
|
||||
<form method="post" action="{% url 'assets:management_zone_delete' zone.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-end gap-2 mt-4">
|
||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-secondary">
|
||||
{% translate "Annuler" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash me-1"></i>{% translate "Supprimer définitivement" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,434 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{{ zone.code }} - {{ zone.get_name }} | {{ product_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Breadcrumb & Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-1">
|
||||
<li class="breadcrumb-item"><a href="/">{% translate "Accueil" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'assets:index' %}">{% translate "Assets" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'assets:management_zones_index' %}?thematic={{ zone.thematic.code }}">{% translate "Zones de gestion" %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ zone.code }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'assets:management_zones_index' %}?thematic={{ zone.thematic.code }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Retour à la liste" %}
|
||||
</a>
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'assets:management_zone_edit' zone.pk %}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-pencil me-1"></i>{% translate "Modifier" %}
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteZoneModal">
|
||||
<i class="bi bi-trash me-1"></i>{% translate "Supprimer" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zone Banner Card -->
|
||||
<div class="card shadow-sm mb-4 border-start border-4" style="border-left-color: {{ zone.get_color }} !important;">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="display:inline-block; width: 16px; height: 16px; border-radius: 4px; background-color: {{ zone.get_color }}; border: 1px solid rgba(0,0,0,0.25);"></span>
|
||||
<h1 class="h4 mb-0 fw-bold">{{ zone.code }} - {{ zone.get_name }}</h1>
|
||||
</div>
|
||||
{% if zone.name_nl and zone.name_nl != zone.name_fr %}
|
||||
<div class="text-muted small mb-2 fst-italic">{{ zone.name_nl }}</div>
|
||||
{% endif %}
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<span class="badge bg-secondary-subtle text-secondary border">
|
||||
<i class="bi bi-tag me-1"></i>{{ zone.thematic.name }}
|
||||
</span>
|
||||
{% if zone.zone_type %}
|
||||
<span class="badge" style="background-color: {{ zone.zone_type.default_color }}; color: #fff;">
|
||||
{{ zone.zone_type.get_name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if zone.is_active %}
|
||||
<span class="badge bg-success-subtle text-success border">
|
||||
<i class="bi bi-check-circle me-1"></i>{% translate "Active" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger-subtle text-danger border">
|
||||
<i class="bi bi-dash-circle me-1"></i>{% translate "Inactive" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if area_ha %}
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi bi-rulers me-1"></i>{{ area_ha }} ha ({{ area_m2|floatformat:0 }} m²)
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end mt-3 mt-md-0">
|
||||
{% if zone.target_frequency_days %}
|
||||
<div class="d-inline-block text-center me-3 p-2 bg-light rounded border">
|
||||
<span class="d-block text-muted small">{% translate "Fréquence cible" %}</span>
|
||||
<span class="fs-5 fw-bold text-dark">{{ zone.target_frequency_days }} {% translate "j" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if zone.seasonality_notes %}
|
||||
<div class="d-inline-block text-start p-2 bg-light rounded border">
|
||||
<span class="d-block text-muted small">{% translate "Période" %}</span>
|
||||
<span class="small fw-semibold text-dark">{{ zone.seasonality_notes }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consignes et Points d'attention (Callout Banner) -->
|
||||
{% if zone.instructions %}
|
||||
<div class="card shadow-sm mb-4 border-warning border-start border-4">
|
||||
<div class="card-body bg-warning bg-opacity-10">
|
||||
<div class="d-flex align-items-start gap-2">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning fs-4 mt-1"></i>
|
||||
<div>
|
||||
<h5 class="h6 fw-bold text-dark mb-1">{% translate "Consignes d'exploitation et points de vigilance" %}</h5>
|
||||
<div class="text-dark" style="white-space: pre-line;">{{ zone.instructions }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Main Grid: Informations & Carte -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Informations & Modalités -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light fw-bold">
|
||||
<i class="bi bi-info-circle me-2 text-success"></i>{% translate "Détails & Caractéristiques" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4 text-muted">{% translate "Description" %}</dt>
|
||||
<dd class="col-sm-8">{{ zone.description|default:_("Aucune description renseignée")|linebreaks }}</dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted">{% translate "Thématique" %}</dt>
|
||||
<dd class="col-sm-8">{{ zone.thematic.name }}</dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted">{% translate "Type de zone" %}</dt>
|
||||
<dd class="col-sm-8">{{ zone.zone_type.get_name|default:"-" }}</dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted">{% translate "Couleur de repérage" %}</dt>
|
||||
<dd class="col-sm-8">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span style="display:inline-block; width:18px; height:18px; border-radius:3px; background-color: {{ zone.get_color }}; border: 1px solid rgba(0,0,0,0.25);"></span>
|
||||
<code>{{ zone.get_color }}</code>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted">{% translate "Superficie estimée" %}</dt>
|
||||
<dd class="col-sm-8">
|
||||
{% if area_m2 %}
|
||||
<strong>{{ area_ha }} hectares</strong> ({{ area_m2|floatformat:0 }} m²)
|
||||
{% else %}
|
||||
<span class="text-muted">{% translate "Périmètre non défini" %}</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted">{% translate "Créé par" %}</dt>
|
||||
<dd class="col-sm-8">{% if zone.created_by %}{{ zone.created_by.get_full_name|default:zone.created_by.username }}{% else %}-{% endif %}</dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted">{% translate "Date de création" %}</dt>
|
||||
<dd class="col-sm-8">{{ zone.created_at|date:"d/m/Y H:i" }}</dd>
|
||||
|
||||
<dt class="col-sm-4 text-muted">{% translate "Dernière mise à jour" %}</dt>
|
||||
<dd class="col-sm-8">{{ zone.updated_at|date:"d/m/Y H:i" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Carte du périmètre géographique -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">
|
||||
<i class="bi bi-map me-2 text-success"></i>{% translate "Périmètre géographique" %}
|
||||
</span>
|
||||
{% if zone.geom %}
|
||||
<span class="badge bg-success">{% translate "Géométrie définie" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{% translate "Aucun périmètre" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0 position-relative">
|
||||
<div id="detailZoneMap" style="height: 380px; width: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documents & Arrêté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-files me-2 text-success"></i>{% translate "Documents & Fiches techniques" %} ({{ documents.count }})
|
||||
</span>
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-sm btn-outline-success" data-bs-toggle="modal" data-bs-target="#uploadDocModal">
|
||||
<i class="bi bi-upload me-1"></i>{% translate "Ajouter des documents" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if documents %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light small text-uppercase text-muted">
|
||||
<tr>
|
||||
<th>{% translate "Fichier" %}</th>
|
||||
<th>{% translate "Date d'ajout" %}</th>
|
||||
<th>{% translate "Ajouté par" %}</th>
|
||||
<th class="text-end" style="width: 140px;">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doc in documents %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-file-earmark-text text-primary fs-5"></i>
|
||||
<div>
|
||||
<a href="{{ doc.file.url }}" target="_blank" class="fw-semibold text-decoration-none">
|
||||
{{ doc.file.name|slice:"25:"|default:doc.file.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ doc.upload_date|date:"d/m/Y H:i" }}</td>
|
||||
<td>{% if doc.uploaded_by %}{{ doc.uploaded_by.get_full_name|default:doc.uploaded_by.username }}{% else %}-{% endif %}</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ doc.file.url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="{% translate 'Télécharger' %}">
|
||||
<i class="bi bi-download"></i>
|
||||
</a>
|
||||
{% if can_edit %}
|
||||
<form method="post" action="{% url 'assets:management_zone_delete_document' zone.pk doc.pk %}" class="d-inline" onsubmit="return confirm('{% translate 'Supprimer ce document ?' %}');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="{% translate 'Supprimer' %}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-muted small">
|
||||
<i class="bi bi-folder2-open fs-2 d-block mb-2 text-muted"></i>
|
||||
{% translate "Aucun document n'a été rattaché à cette zone de gestion." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Équipements rattachés (Assets explicites) -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<span class="fw-bold">
|
||||
<i class="bi bi-layers me-2 text-success"></i>{% translate "Équipements rattachés explicitement" %} ({{ explicit_assets.count }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if explicit_assets %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light small text-uppercase text-muted">
|
||||
<tr>
|
||||
<th>{% translate "Équipement" %}</th>
|
||||
<th>{% translate "Type" %}</th>
|
||||
<th>{% translate "Statut de rattachement" %}</th>
|
||||
<th>{% translate "Notes" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ea in explicit_assets %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ ea.asset|default:ea.object_id }}</strong>
|
||||
</td>
|
||||
<td>{{ ea.content_type.name }}</td>
|
||||
<td>
|
||||
{% if ea.is_excluded %}
|
||||
<span class="badge bg-danger-subtle text-danger border">
|
||||
<i class="bi bi-slash-circle me-1"></i>{% translate "Exclu de la zone" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success-subtle text-success border">
|
||||
<i class="bi bi-check-circle me-1"></i>{% translate "Inclus dans la zone" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">{{ ea.notes|default:"-" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-muted small">
|
||||
<p class="mb-0">{% translate "Tous les équipements situés dans le périmètre géographique de la zone y sont automatiquement associés par intersection spatiale." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Upload Documents -->
|
||||
{% if can_edit and doc_form %}
|
||||
<div class="modal fade" id="uploadDocModal" tabindex="-1" aria-labelledby="uploadDocModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{% url 'assets:management_zone_add_document' zone.pk %}" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="uploadDocModalLabel">
|
||||
<i class="bi bi-upload me-2 text-success"></i>{% translate "Ajouter des documents" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="{{ doc_form.documents.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Sélectionner des fichiers" %}
|
||||
</label>
|
||||
{{ doc_form.documents }}
|
||||
<div class="form-text small">
|
||||
{% translate "Formats acceptés : PDF, Word, Excel, images, etc. Vous pouvez sélectionner plusieurs fichiers simultanément." %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-check-lg me-1"></i>{% translate "Téléverser" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Modal: Delete Zone Confirmation -->
|
||||
{% if can_edit %}
|
||||
<div class="modal fade" id="deleteZoneModal" tabindex="-1" aria-labelledby="deleteZoneModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{% url 'assets:management_zone_delete' zone.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="deleteZoneModalLabel">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{% translate "Confirmer la suppression" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{% translate "Êtes-vous certain de vouloir supprimer la zone de gestion suivante ?" %}</p>
|
||||
<div class="alert alert-light border">
|
||||
<strong>{{ zone.code }}</strong> - {{ zone.get_name }}<br>
|
||||
<small class="text-muted">{{ zone.thematic.name }} | {{ zone.zone_type.get_name|default:"-" }}</small>
|
||||
</div>
|
||||
<p class="text-danger small mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i>{% translate "Cette action est irréversible. Les documents et historiques liés seront supprimés." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash me-1"></i>{% translate "Supprimer définitivement" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const mapDiv = document.getElementById("detailZoneMap");
|
||||
if (!mapDiv || typeof maplibregl === "undefined") return;
|
||||
|
||||
const zoneColor = "{{ zone.get_color }}";
|
||||
const geojsonGeometry = {% if zone.geojson_geometry %}{{ zone.geojson_geometry|safe }}{% else %}null{% endif %};
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: mapDiv,
|
||||
style: window.MAP_STYLE_LIGHT,
|
||||
center: window.MAP_DEFAULT_CENTER || [4.3572, 50.8477],
|
||||
zoom: 12
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
|
||||
map.on('load', function () {
|
||||
if (!geojsonGeometry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
geometry: geojsonGeometry
|
||||
};
|
||||
|
||||
map.addSource("detail-zone-source", {
|
||||
type: "geojson",
|
||||
data: feature
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "detail-zone-fill",
|
||||
type: "fill",
|
||||
source: "detail-zone-source",
|
||||
paint: {
|
||||
"fill-color": zoneColor,
|
||||
"fill-opacity": 0.30
|
||||
}
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "detail-zone-line",
|
||||
type: "line",
|
||||
source: "detail-zone-source",
|
||||
paint: {
|
||||
"line-color": zoneColor,
|
||||
"line-width": 3,
|
||||
"line-dasharray": [3, 1.5]
|
||||
}
|
||||
});
|
||||
|
||||
// Fit bounds to polygon
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
const coords = geojsonGeometry.coordinates;
|
||||
const addCoords = (arr) => {
|
||||
if (typeof arr[0] === 'number') {
|
||||
bounds.extend(arr);
|
||||
} else {
|
||||
arr.forEach(addCoords);
|
||||
}
|
||||
};
|
||||
addCoords(coords);
|
||||
|
||||
if (!bounds.isEmpty()) {
|
||||
map.fitBounds(bounds, { padding: 45, maxZoom: 16 });
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock script %}
|
||||
|
|
@ -1,568 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{{ title }} | {{ product_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Breadcrumb & Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-1">
|
||||
<li class="breadcrumb-item"><a href="/">{% translate "Accueil" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'assets:index' %}">{% translate "Assets" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'assets:management_zones_index' %}">{% translate "Zones de gestion" %}</a></li>
|
||||
{% if not is_create and zone %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'assets:management_zone_detail' zone.pk %}">{{ zone.code }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% translate "Modifier" %}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{% translate "Nouvelle zone" %}</li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
<div>
|
||||
{% if not is_create and zone %}
|
||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Annuler" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'assets:management_zones_index' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Annuler" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="mb-4">
|
||||
<h1 class="h3 mb-1">
|
||||
{% if is_create %}
|
||||
<i class="bi bi-plus-circle-fill text-success me-2"></i>{% translate "Créer une zone de gestion" %}
|
||||
{% else %}
|
||||
<i class="bi bi-pencil-square text-success me-2"></i>{% translate "Modifier la zone de gestion" %} {{ zone.code }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% translate "Définissez les paramètres d'entretien, les consignes et le périmètre géographique de la zone." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
<form method="post" action="" class="needs-validation" id="managementZoneForm" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form.geom_geojson }}
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Left Column: Form Fields -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Informations générales -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light fw-bold">
|
||||
<i class="bi bi-info-circle me-2 text-success"></i>{% translate "Informations générales" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label for="{{ form.code.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Code de la zone" %} <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 class="form-text small">{% translate "Identifiant unique (ex: ZG-NAT-001)" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-7">
|
||||
<label for="{{ form.thematic.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Thématique" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.thematic }}
|
||||
{% if form.thematic.errors %}<div class="text-danger small mt-1">{{ form.thematic.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.name_fr.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Nom (FR)" %} <span class="text-danger">*</span>
|
||||
</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-6">
|
||||
<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-8">
|
||||
<label for="{{ form.zone_type.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Type de zone de gestion" %}
|
||||
</label>
|
||||
{{ form.zone_type }}
|
||||
{% if form.zone_type.errors %}<div class="text-danger small mt-1">{{ form.zone_type.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.color.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Couleur" %}
|
||||
</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{{ form.color }}
|
||||
<span class="small text-muted" id="colorHexLabel"></span>
|
||||
</div>
|
||||
{% if form.color.errors %}<div class="text-danger small mt-1">{{ form.color.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="form-check form-switch mt-2">
|
||||
{{ form.is_active }}
|
||||
<label class="form-check-label fw-semibold ms-2" for="{{ form.is_active.id_for_label }}">
|
||||
{% translate "Zone active (affichée sur les cartes d'exploitation)" %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entretien, Calendrier & Consignes -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light fw-bold">
|
||||
<i class="bi bi-calendar-check me-2 text-success"></i>{% translate "Modalités d'entretien & Consignes" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label for="{{ form.target_frequency_days.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Fréquence cible (jours)" %}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
{{ form.target_frequency_days }}
|
||||
<span class="input-group-text">{% translate "jours" %}</span>
|
||||
</div>
|
||||
<div class="form-text small">{% translate "Intervalle indicatif entre passages" %}</div>
|
||||
{% if form.target_frequency_days.errors %}<div class="text-danger small mt-1">{{ form.target_frequency_days.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-7">
|
||||
<label for="{{ form.seasonality_notes.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Saisonnalité / Calendrier" %}
|
||||
</label>
|
||||
{{ form.seasonality_notes }}
|
||||
<div class="form-text small">{% translate "Ex: Fauche en sept; interdiction taille mars-juillet" %}</div>
|
||||
{% if form.seasonality_notes.errors %}<div class="text-danger small mt-1">{{ form.seasonality_notes.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="{{ form.instructions.id_for_label }}" class="form-label fw-semibold text-warning-emphasis">
|
||||
<i class="bi bi-exclamation-triangle me-1 text-warning"></i>{% translate "Consignes et points d'attention" %}
|
||||
</label>
|
||||
{{ form.instructions }}
|
||||
<div class="form-text small">{% translate "Consignes cruciales : sécurité tramway, respect des zones humides, hauteur de coupe, etc." %}</div>
|
||||
{% if form.instructions.errors %}<div class="text-danger small mt-1">{{ form.instructions.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label fw-semibold">
|
||||
{% translate "Description générale" %}
|
||||
</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}<div class="text-danger small mt-1">{{ form.description.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Interactive Map Polygon Drawer -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm mb-4 sticky-top" style="top: 20px;">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-bounding-box-circles me-2 text-success"></i>
|
||||
<span class="fw-bold">{% translate "Périmètre géographique" %}</span>
|
||||
<small class="text-muted fw-normal">({% translate "polygone" %})</small>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="button" id="polyPickerDrawBtn" class="btn btn-sm btn-outline-success">
|
||||
<i class="bi bi-pentagon me-1"></i>{% translate "Dessiner" %}
|
||||
</button>
|
||||
<button type="button" id="polyPickerFinishBtn" class="btn btn-sm btn-success d-none">
|
||||
<i class="bi bi-check-circle me-1"></i>{% translate "Terminer le polygone" %}
|
||||
</button>
|
||||
<button type="button" id="polyPickerClearBtn" class="btn btn-sm btn-outline-danger" title="{% translate 'Effacer le polygone' %}">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0 position-relative">
|
||||
<div id="zonePickerMap" style="height: 480px; width: 100%;"></div>
|
||||
</div>
|
||||
<div class="card-footer bg-white border-top small text-muted d-flex justify-content-between align-items-center">
|
||||
<span id="polyPickerHint">
|
||||
{% translate "Cliquez sur « Dessiner » pour tracer les limites de la zone sur la carte." %}
|
||||
</span>
|
||||
<span id="polyStatusBadge" class="badge bg-secondary">
|
||||
{% translate "Aucun tracé" %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Submit Actions -->
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
{% if not is_create and zone %}
|
||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-secondary">
|
||||
{% translate "Annuler" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'assets:management_zones_index' %}" class="btn btn-outline-secondary">
|
||||
{% translate "Annuler" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-success px-4">
|
||||
<i class="bi bi-check-lg me-1"></i>
|
||||
{% if is_create %}{% translate "Créer la zone de gestion" %}{% else %}{% translate "Enregistrer les modifications" %}{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const mapDiv = document.getElementById("zonePickerMap");
|
||||
const geomInput = document.getElementById("id_geom_geojson");
|
||||
const thematicSelect = document.getElementById("id_thematic");
|
||||
const zoneTypeSelect = document.getElementById("id_zone_type");
|
||||
const colorInput = document.getElementById("id_color");
|
||||
const colorHexLabel = document.getElementById("colorHexLabel");
|
||||
|
||||
if (colorInput && colorHexLabel) {
|
||||
colorHexLabel.textContent = colorInput.value;
|
||||
colorInput.addEventListener("input", () => {
|
||||
colorHexLabel.textContent = colorInput.value;
|
||||
if (map && map.getLayer("poly-preview-fill")) {
|
||||
map.setPaintProperty("poly-preview-fill", "fill-color", colorInput.value);
|
||||
map.setPaintProperty("poly-preview-line", "line-color", colorInput.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamic zone types when thematic changes
|
||||
if (thematicSelect && zoneTypeSelect) {
|
||||
thematicSelect.addEventListener("change", async function () {
|
||||
const thematicId = thematicSelect.value;
|
||||
if (!thematicId) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch("{% url 'assets:management_zone_types_ajax' %}?thematic_id=" + encodeURIComponent(thematicId));
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const currentVal = zoneTypeSelect.value;
|
||||
zoneTypeSelect.innerHTML = '<option value="">-- {% translate "Sélectionner un type" %} --</option>';
|
||||
(data.types || []).forEach(t => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = t.id;
|
||||
opt.textContent = t.name;
|
||||
if (String(t.id) === String(currentVal)) opt.selected = true;
|
||||
zoneTypeSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error updating zone types:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Interactive Map Polygon Drawer
|
||||
if (!mapDiv || typeof maplibregl === "undefined") return;
|
||||
|
||||
let drawing = false;
|
||||
let vertices = [];
|
||||
let vertexMarkers = [];
|
||||
let map = null;
|
||||
|
||||
const drawBtn = document.getElementById("polyPickerDrawBtn");
|
||||
const finishBtn = document.getElementById("polyPickerFinishBtn");
|
||||
const clearBtn = document.getElementById("polyPickerClearBtn");
|
||||
const hintEl = document.getElementById("polyPickerHint");
|
||||
const badgeEl = document.getElementById("polyStatusBadge");
|
||||
|
||||
map = new maplibregl.Map({
|
||||
container: mapDiv,
|
||||
style: window.MAP_STYLE_LIGHT,
|
||||
center: window.MAP_DEFAULT_CENTER || [4.3572, 50.8477],
|
||||
zoom: 12
|
||||
});
|
||||
|
||||
// 1. Fond de plan & Orthophotos (LayerSwitcherControl)
|
||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
||||
const layerSwitcher = new LayerSwitcherControl({
|
||||
layers: [{
|
||||
id: 'basemap',
|
||||
name: "{% translate 'Fonds de plan & Orthophotos' %}",
|
||||
icon: 'satellite',
|
||||
group: 'basemap',
|
||||
beforeLayer: function (mapInstance) {
|
||||
if (mapInstance && mapInstance.getLayer('poly-preview-fill')) {
|
||||
return 'poly-preview-fill';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
layer: {
|
||||
type: 'raster',
|
||||
paint: { 'raster-opacity': 1 }
|
||||
},
|
||||
basemaps: (window.MAP_BACKGROUNDS && window.MAP_BACKGROUNDS.length > 0) ? window.MAP_BACKGROUNDS : undefined,
|
||||
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 || '© Brussels 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');
|
||||
}
|
||||
|
||||
// 2. Navigation Control (Zoom +/- & Pitch/Bearing)
|
||||
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), 'top-right');
|
||||
|
||||
// 3. Outil de recherche d'adresse
|
||||
if (typeof AddressSearchControl !== 'undefined') {
|
||||
const searchCtrl = new AddressSearchControl({
|
||||
autocompleteUrl: "{% url 'common:autocomplete_street' %}",
|
||||
placeholder: "{% translate 'Rechercher une adresse…' %}"
|
||||
});
|
||||
map.addControl(searchCtrl, 'top-left');
|
||||
}
|
||||
|
||||
function getColor() {
|
||||
return colorInput ? colorInput.value : "#198754";
|
||||
}
|
||||
|
||||
function initPolyLayers() {
|
||||
if (!map.getSource("poly-preview")) {
|
||||
map.addSource("poly-preview", {
|
||||
type: "geojson",
|
||||
data: { type: "Feature", geometry: { type: "Polygon", coordinates: [] } }
|
||||
});
|
||||
map.addLayer({
|
||||
id: "poly-preview-fill",
|
||||
type: "fill",
|
||||
source: "poly-preview",
|
||||
paint: {
|
||||
"fill-color": getColor(),
|
||||
"fill-opacity": 0.25
|
||||
}
|
||||
});
|
||||
map.addLayer({
|
||||
id: "poly-preview-line",
|
||||
type: "line",
|
||||
source: "poly-preview",
|
||||
paint: {
|
||||
"line-color": getColor(),
|
||||
"line-width": 2.5
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearMarkers() {
|
||||
vertexMarkers.forEach(m => m.remove());
|
||||
vertexMarkers = [];
|
||||
}
|
||||
|
||||
function addVertexMarker(lng, lat, index) {
|
||||
const el = document.createElement("div");
|
||||
el.style.width = "10px";
|
||||
el.style.height = "10px";
|
||||
el.style.borderRadius = "50%";
|
||||
el.style.backgroundColor = index === 0 ? "#dc3545" : "#198754";
|
||||
el.style.border = "2px solid #ffffff";
|
||||
el.style.boxShadow = "0 0 3px rgba(0,0,0,0.5)";
|
||||
el.style.cursor = index === 0 && drawing && vertices.length >= 3 ? "pointer" : "default";
|
||||
|
||||
if (index === 0) {
|
||||
el.title = "{% translate 'Cliquez pour clore le polygone' %}";
|
||||
el.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (drawing && vertices.length >= 3) {
|
||||
finishPolygon();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el }).setLngLat([lng, lat]).addTo(map);
|
||||
vertexMarkers.push(marker);
|
||||
}
|
||||
|
||||
function renderPoly(coords) {
|
||||
initPolyLayers();
|
||||
let geom;
|
||||
if (coords.length >= 3) {
|
||||
geom = {
|
||||
type: "Polygon",
|
||||
coordinates: [coords.concat([coords[0]])]
|
||||
};
|
||||
} else if (coords.length === 2) {
|
||||
geom = {
|
||||
type: "LineString",
|
||||
coordinates: coords
|
||||
};
|
||||
} else {
|
||||
geom = { type: "Point", coordinates: coords[0] || [0, 0] };
|
||||
}
|
||||
map.getSource("poly-preview").setData({ type: "Feature", geometry: geom });
|
||||
}
|
||||
|
||||
function finishPolygon() {
|
||||
if (vertices.length < 3) {
|
||||
alert("{% translate 'Un polygone doit comporter au moins 3 sommets.' %}");
|
||||
return;
|
||||
}
|
||||
drawing = false;
|
||||
const closed = vertices.concat([vertices[0]]);
|
||||
const polyGeojson = {
|
||||
type: "Polygon",
|
||||
coordinates: [closed]
|
||||
};
|
||||
if (geomInput) {
|
||||
geomInput.value = JSON.stringify(polyGeojson);
|
||||
}
|
||||
renderPoly(vertices);
|
||||
updateUI(false);
|
||||
hintEl.textContent = "{% translate 'Polygone enregistré. Cliquez sur « Dessiner » pour le refaire.' %}";
|
||||
badgeEl.className = "badge bg-success";
|
||||
badgeEl.textContent = `{% translate 'Polygone défini' %} (${vertices.length} {% translate 'sommets' %})`;
|
||||
}
|
||||
|
||||
function updateUI(isDrawing) {
|
||||
drawBtn.classList.toggle("d-none", isDrawing);
|
||||
finishBtn.classList.toggle("d-none", !isDrawing);
|
||||
map.getCanvas().style.cursor = isDrawing ? "crosshair" : "default";
|
||||
}
|
||||
|
||||
function showExistingPolygon(geojsonStr) {
|
||||
if (!geojsonStr) return;
|
||||
try {
|
||||
const geom = JSON.parse(geojsonStr);
|
||||
let coords = null;
|
||||
if (geom.type === "Polygon") {
|
||||
coords = geom.coordinates[0];
|
||||
} else if (geom.type === "MultiPolygon") {
|
||||
coords = geom.coordinates[0][0];
|
||||
}
|
||||
if (!coords || coords.length === 0) return;
|
||||
|
||||
// Strip closing duplicate vertex for marker tracking
|
||||
const rawVertices = (coords[0][0] === coords[coords.length - 1][0] && coords[0][1] === coords[coords.length - 1][1])
|
||||
? coords.slice(0, coords.length - 1)
|
||||
: coords;
|
||||
|
||||
vertices = rawVertices;
|
||||
clearMarkers();
|
||||
vertices.forEach((c, idx) => addVertexMarker(c[0], c[1], idx));
|
||||
renderPoly(vertices);
|
||||
|
||||
// Fit bounds
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
coords.forEach(c => bounds.extend(c));
|
||||
map.fitBounds(bounds, { padding: 50, maxZoom: 16 });
|
||||
|
||||
badgeEl.className = "badge bg-success";
|
||||
badgeEl.textContent = `{% translate 'Polygone existant' %} (${vertices.length} {% translate 'sommets' %})`;
|
||||
hintEl.textContent = "{% translate 'Polygone actuel affiché. Cliquez sur « Dessiner » pour le modifier.' %}";
|
||||
} catch (e) {
|
||||
console.warn("Could not parse existing geometry:", e);
|
||||
}
|
||||
}
|
||||
|
||||
map.on("load", function () {
|
||||
initPolyLayers();
|
||||
if (geomInput && geomInput.value) {
|
||||
showExistingPolygon(geomInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
map.on("click", function (e) {
|
||||
if (!drawing) return;
|
||||
const { lng, lat } = e.lngLat;
|
||||
vertices.push([lng, lat]);
|
||||
addVertexMarker(lng, lat, vertices.length - 1);
|
||||
renderPoly(vertices);
|
||||
|
||||
hintEl.textContent = `{% translate 'Sommets posés' %} : ${vertices.length}. {% translate 'Cliquez sur le premier sommet ou sur « Terminer » pour clore.' %}`;
|
||||
badgeEl.className = "badge bg-warning text-dark";
|
||||
badgeEl.textContent = `{% translate 'Tracé en cours' %} (${vertices.length})`;
|
||||
});
|
||||
|
||||
drawBtn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
drawing = true;
|
||||
vertices = [];
|
||||
clearMarkers();
|
||||
if (geomInput) geomInput.value = "";
|
||||
initPolyLayers();
|
||||
map.getSource("poly-preview").setData({ type: "Feature", geometry: { type: "Polygon", coordinates: [] } });
|
||||
updateUI(true);
|
||||
hintEl.textContent = "{% translate 'Cliquez sur la carte pour poser chaque sommet.' %}";
|
||||
badgeEl.className = "badge bg-warning text-dark";
|
||||
badgeEl.textContent = "{% translate 'Tracé en cours' %}";
|
||||
});
|
||||
|
||||
finishBtn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
finishPolygon();
|
||||
});
|
||||
|
||||
clearBtn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
drawing = false;
|
||||
vertices = [];
|
||||
clearMarkers();
|
||||
if (geomInput) geomInput.value = "";
|
||||
initPolyLayers();
|
||||
map.getSource("poly-preview").setData({ type: "Feature", geometry: { type: "Polygon", coordinates: [] } });
|
||||
updateUI(false);
|
||||
hintEl.textContent = "{% translate 'Périmètre effacé. Cliquez sur « Dessiner » pour recommencer.' %}";
|
||||
badgeEl.className = "badge bg-secondary";
|
||||
badgeEl.textContent = "{% translate 'Aucun tracé' %}";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock script %}
|
||||
|
|
@ -1,340 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% translate "Zones de gestion" %} | {{ product_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Breadcrumb & Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-1">
|
||||
<li class="breadcrumb-item"><a href="/">{% translate "Accueil" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'assets:index' %}">{% translate "Assets" %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% translate "Zones de gestion" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% if can_create %}
|
||||
<a href="{% url 'assets:management_zone_create' %}{% if selected_thematic_code %}?thematic={{ selected_thematic_code }}{% endif %}" class="btn btn-success">
|
||||
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouvelle zone de gestion" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Title Bar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-diagram-3-fill text-success me-2"></i>{% translate "Zones de gestion" %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% translate "Regroupements géographiques et fonctionnels d'équipements pour l'entretien récurrent, les plans de fauche et l'exploitation." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-light text-dark border fs-6 px-3 py-2">
|
||||
<strong>{{ total_count }}</strong> {% translate "zone(s) répertoriée(s)" %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Card -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
<!-- Search text -->
|
||||
<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, consignes...' %}" value="{{ search_query }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thematic -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">{% translate "Thématique" %}</label>
|
||||
<select name="thematic" id="filterThematic" class="form-select form-select-sm">
|
||||
<option value="">-- {% translate "Toutes les thématiques" %} --</option>
|
||||
{% for th in accessible_thematics %}
|
||||
<option value="{{ th.code }}" {% if selected_thematic_code == th.code %}selected{% endif %}>
|
||||
{{ th.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Zone Type -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">{% translate "Type de zone" %}</label>
|
||||
<select name="zone_type" id="filterZoneType" class="form-select form-select-sm">
|
||||
<option value="">-- {% translate "Tous les types" %} --</option>
|
||||
{% for zt in available_types %}
|
||||
<option value="{{ zt.id }}" {% if selected_type_id == zt.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ zt.get_name }} ({{ zt.thematic.name }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small text-muted">{% translate "Statut" %}</label>
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="">-- {% translate "Tous les statuts" %} --</option>
|
||||
<option value="active" {% if selected_status == 'active' %}selected{% endif %}>{% translate "Actives uniquement" %}</option>
|
||||
<option value="inactive" {% if selected_status == 'inactive' %}selected{% endif %}>{% translate "Inactives" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Submit / Reset buttons -->
|
||||
<div class="col-md-1 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100" title="{% translate 'Filtrer' %}">
|
||||
<i class="bi bi-funnel"></i>
|
||||
</button>
|
||||
<a href="{% url 'assets:management_zones_index' %}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Réinitialiser' %}">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Overview Card -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center py-2">
|
||||
<span class="fw-semibold small">
|
||||
<i class="bi bi-map me-1 text-success"></i>{% translate "Aperçu cartographique des périmètres" %}
|
||||
</span>
|
||||
<small class="text-muted" id="mapLoadingStatus">{% translate "Chargement de la carte..." %}</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="zonesOverviewMap" style="height: 320px; width: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zones Table Card -->
|
||||
<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 small text-uppercase text-muted">
|
||||
<tr>
|
||||
<th style="width: 140px;">{% translate "Code" %}</th>
|
||||
<th>{% translate "Nom de la zone" %}</th>
|
||||
<th>{% translate "Thématique" %}</th>
|
||||
<th>{% translate "Type de zone" %}</th>
|
||||
<th>{% translate "Fréquence cible" %}</th>
|
||||
<th>{% translate "Saisonnalité" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th class="text-end" style="width: 160px;">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in zones_list %}
|
||||
{% with zone=item.zone can_edit=item.can_edit %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span style="display:inline-block; width: 14px; height: 14px; border-radius: 3px; background-color: {{ zone.get_color }}; border: 1px solid rgba(0,0,0,0.2); flex-shrink: 0;"></span>
|
||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="fw-bold text-decoration-none">
|
||||
{{ zone.code }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ zone.get_name }}</div>
|
||||
{% if zone.instructions %}
|
||||
<div class="text-muted small text-truncate" style="max-width: 320px;" title="{{ zone.instructions }}">
|
||||
<i class="bi bi-exclamation-triangle text-warning me-1"></i>{{ zone.instructions }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary-subtle text-secondary border">
|
||||
{{ zone.thematic.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if zone.zone_type %}
|
||||
<span class="badge" style="background-color: {{ zone.zone_type.default_color }}; color: #fff;">
|
||||
{{ zone.zone_type.get_name }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if zone.target_frequency_days %}
|
||||
<span class="badge bg-info-subtle text-info border">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>{{ zone.target_frequency_days }} {% translate "jours" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if zone.seasonality_notes %}
|
||||
<span class="small text-muted"><i class="bi bi-calendar3 me-1"></i>{{ zone.seasonality_notes }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if zone.is_active %}
|
||||
<span class="badge bg-success-subtle text-success border">{% translate "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger-subtle text-danger border">{% translate "Inactive" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{% url 'assets:management_zone_detail' zone.pk %}" class="btn btn-outline-primary" title="{% translate 'Détails' %}">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'assets:management_zone_edit' zone.pk %}" class="btn btn-outline-secondary" title="{% translate 'Modifier' %}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'assets:management_zone_delete' zone.pk %}" class="btn btn-outline-danger" title="{% translate 'Supprimer' %}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-diagram-3 fs-1 d-block mb-2 text-muted"></i>
|
||||
<p class="mb-2">{% translate "Aucune zone de gestion ne correspond aux critères sélectionnés." %}</p>
|
||||
{% if can_create %}
|
||||
<a href="{% url 'assets:management_zone_create' %}{% if selected_thematic_code %}?thematic={{ selected_thematic_code }}{% endif %}" class="btn btn-sm btn-success">
|
||||
<i class="bi bi-plus-lg me-1"></i>{% translate "Créer une première zone de gestion" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const mapDiv = document.getElementById("zonesOverviewMap");
|
||||
if (!mapDiv || typeof maplibregl === "undefined") return;
|
||||
|
||||
const thematicParam = "{{ selected_thematic_code|default:'' }}";
|
||||
const geojsonUrl = "{% url 'assets:management_zones_geojson' %}" + (thematicParam ? ("?thematic=" + encodeURIComponent(thematicParam)) : "");
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: mapDiv,
|
||||
style: window.MAP_STYLE_LIGHT,
|
||||
center: window.MAP_DEFAULT_CENTER || [4.3572, 50.8477],
|
||||
zoom: 11
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
|
||||
map.on('load', async function () {
|
||||
try {
|
||||
const resp = await fetch(geojsonUrl, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
if (!resp.ok) {
|
||||
document.getElementById("mapLoadingStatus").textContent = "";
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const features = data.features || [];
|
||||
|
||||
document.getElementById("mapLoadingStatus").textContent = `${features.length} {% translate "périmètre(s) affiché(s)" %}`;
|
||||
|
||||
map.addSource('management-zones-overview', {
|
||||
type: 'geojson',
|
||||
data: data
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: 'management-zones-fill',
|
||||
type: 'fill',
|
||||
source: 'management-zones-overview',
|
||||
paint: {
|
||||
'fill-color': ['coalesce', ['get', 'color'], '#198754'],
|
||||
'fill-opacity': 0.25
|
||||
}
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: 'management-zones-line',
|
||||
type: 'line',
|
||||
source: 'management-zones-overview',
|
||||
paint: {
|
||||
'line-color': ['coalesce', ['get', 'color'], '#198754'],
|
||||
'line-width': 2,
|
||||
'line-dasharray': [3, 1.5]
|
||||
}
|
||||
});
|
||||
|
||||
const popup = new maplibregl.Popup({ closeButton: true, closeOnClick: true, maxWidth: '300px' });
|
||||
|
||||
map.on('click', 'management-zones-fill', function (e) {
|
||||
if (!e.features || !e.features[0]) return;
|
||||
const p = e.features[0].properties;
|
||||
const detailUrl = "/assets/management-zones/" + p.id + "/";
|
||||
const html = `
|
||||
<div style="font-size: 12px; line-height: 1.4;">
|
||||
<div style="display:flex; align-items:center; gap:6px; margin-bottom:4px;">
|
||||
<span style="display:inline-block; width:12px; height:12px; border-radius:3px; background-color:${p.color || '#198754'};"></span>
|
||||
<strong>${p.code}</strong>
|
||||
</div>
|
||||
<div class="fw-semibold mb-1">${p.name}</div>
|
||||
${p.zone_type_name ? `<span class="badge bg-secondary mb-1">${p.zone_type_name}</span>` : ''}
|
||||
${p.target_frequency_days ? `<div class="text-muted small"><i class="bi bi-arrow-repeat me-1"></i>${p.target_frequency_days} {% translate "jours" %}</div>` : ''}
|
||||
<div class="mt-2 text-end">
|
||||
<a href="${detailUrl}" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 11px;">
|
||||
{% translate "Consulter la fiche" %} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
||||
});
|
||||
|
||||
map.on('mouseenter', 'management-zones-fill', () => { map.getCanvas().style.cursor = 'pointer'; });
|
||||
map.on('mouseleave', 'management-zones-fill', () => { map.getCanvas().style.cursor = ''; });
|
||||
|
||||
// Fit bounds to features
|
||||
if (features.length > 0) {
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
features.forEach(f => {
|
||||
const coords = f.geometry?.coordinates;
|
||||
if (!coords) return;
|
||||
const addCoords = (arr) => {
|
||||
if (typeof arr[0] === 'number') {
|
||||
bounds.extend(arr);
|
||||
} else {
|
||||
arr.forEach(addCoords);
|
||||
}
|
||||
};
|
||||
addCoords(coords);
|
||||
});
|
||||
if (!bounds.isEmpty()) {
|
||||
map.fitBounds(bounds, { padding: 40, maxZoom: 15 });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading management zones overview map:", err);
|
||||
document.getElementById("mapLoadingStatus").textContent = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock script %}
|
||||
|
|
@ -1,705 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.nature-kpi-card {
|
||||
border-radius: 10px;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
}
|
||||
.nature-kpi-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
.nature-kpi-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.nature-tab-content {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
#map {
|
||||
width: 100%;
|
||||
height: calc(100vh - 230px);
|
||||
min-height: 520px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.map-card-wrapper {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
}
|
||||
.map-layer-pill {
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-3">
|
||||
|
||||
<!-- Breadcrumb & Navigation -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{% url 'assets:index' %}?thematic=nature" class="text-decoration-none">{% translate "Nature" %}</a></li>
|
||||
<li class="breadcrumb-item active fw-semibold" aria-current="page">{{ section.code }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'assets:index' %}?thematic=nature" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>{% translate "Retour à la carte" %}
|
||||
</a>
|
||||
{% if section.lon and section.lat %}
|
||||
<a href="https://www.google.com/maps?cbll={{ section.lat|floatformat:'6u'}},{{ section.lon|floatformat:'6u'}}&cbp=12,0,0,0,5&layer=c&panoid="
|
||||
target="_blank" class="btn btn-sm btn-outline-primary" style="text-decoration: none;">
|
||||
<img src="{% static 'common/google-street-view-logo.png' %}" alt="{% translate 'Street View' %}" height="18" class="me-1">
|
||||
{% translate "View in Google Street View" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header Card -->
|
||||
<div class="card shadow-sm mb-4 border-0" style="background: linear-gradient(to right, #f8fafc, #ffffff); border-left: 5px solid #16a34a !important;">
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="rounded-circle bg-success-subtle p-2 d-flex align-items-center justify-content-center text-success" style="width: 52px; height: 52px; min-width: 52px;">
|
||||
<i class="bi bi-tree-fill" style="font-size: 1.8rem;"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<h1 class="h4 mb-0 fw-bold text-dark">
|
||||
{{ section.code }} <span class="text-muted fw-normal">— {{ section.get_name }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap text-muted small">
|
||||
{% if section.municipality %}
|
||||
<span><i class="bi bi-geo-alt-fill text-danger me-1"></i>{{ section.municipality }}</span>
|
||||
<span>•</span>
|
||||
{% endif %}
|
||||
<span>
|
||||
<strong>{% translate "Zone:" %}</strong>
|
||||
{% if section.zone %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border">Zone {{ section.zone }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">{% translate "Non définie" %}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
<strong>{% translate "Autoroute:" %}</strong>
|
||||
{% if section.is_highway %}
|
||||
<span class="badge bg-primary-subtle text-primary border">{% translate "Oui" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-secondary border">{% translate "Non" %}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if section.category %}
|
||||
<span>•</span>
|
||||
<span><i class="bi bi-tag me-1"></i>{{ section.category.get_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if section.parent_location or section.child_locations.exists %}
|
||||
<div class="mt-2 pt-2 border-top d-flex gap-3 flex-wrap small text-muted">
|
||||
{% if section.parent_location %}
|
||||
<div>
|
||||
<i class="bi bi-diagram-2 me-1"></i>{% translate "Section parente :" %}
|
||||
<a href="{{ section.parent_location.get_absolute_url }}" class="fw-semibold text-decoration-none">
|
||||
{{ section.parent_location.code }} - {{ section.parent_location.get_name }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if section.child_locations.exists %}
|
||||
<div>
|
||||
<i class="bi bi-diagram-3 me-1"></i>{% translate "Sous-sections :" %}
|
||||
{% for child in section.child_locations.all %}
|
||||
<a href="{{ child.get_absolute_url }}" class="badge bg-light text-dark border text-decoration-none me-1">
|
||||
{{ child.code }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 text-lg-end">
|
||||
<div class="d-inline-flex gap-2 flex-wrap justify-content-lg-end">
|
||||
<div class="badge bg-light text-dark border p-2 text-start">
|
||||
<span class="d-block text-muted" style="font-size: 0.72rem;">{% translate "Total équipements" %}</span>
|
||||
<span class="fw-bold fs-6">{{ total_assets_count }}</span>
|
||||
</div>
|
||||
<div class="badge bg-light text-dark border p-2 text-start">
|
||||
<span class="d-block text-muted" style="font-size: 0.72rem;">{% translate "Interventions" %}</span>
|
||||
<span class="fw-bold fs-6 text-primary">{{ interventions|length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Metric Cards Grid -->
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- 1. Nombre d'arbres -->
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="nature-kpi-icon bg-success-subtle text-success">
|
||||
<i class="bi bi-tree-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold">{% translate "Nombre d'arbres:" %}</div>
|
||||
<div class="fs-4 fw-bold text-dark">{{ trees_count }}</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">
|
||||
{{ trees_active_count }} {% translate "actifs" %}
|
||||
{% if trees_remarkable_count %}
|
||||
• <span class="text-warning fw-semibold"><i class="bi bi-star-fill text-warning"></i> {{ trees_remarkable_count }} {% translate "remarquables" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Superficie totale surfaces vertes -->
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="nature-kpi-icon bg-primary-subtle text-primary">
|
||||
<i class="bi bi-grid-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold">{% translate "Surfaces végétales:" %}</div>
|
||||
<div class="fs-4 fw-bold text-dark">{{ total_green_surface|floatformat:1 }} <span class="fs-6 fw-normal text-muted">m²</span></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">
|
||||
{{ green_surfaces_count }} {% translate "surfaces au total" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Superficie massif arboré -->
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="nature-kpi-icon bg-warning-subtle text-warning-emphasis">
|
||||
<i class="bi bi-flower1"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold">{% translate "Superficie massif arboré:" %}</div>
|
||||
<div class="fs-4 fw-bold text-dark">{{ surface_massif_arbore|floatformat:1 }} <span class="fs-6 fw-normal text-muted">m²</span></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">
|
||||
{{ massif_arbore_count }} {% translate "massifs arborés" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Superficie pré fleuri -->
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card h-100 nature-kpi-card shadow-sm p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="nature-kpi-icon bg-info-subtle text-info-emphasis">
|
||||
<i class="bi bi-flower2"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold">{% translate "Superficie pré fleuri:" %}</div>
|
||||
<div class="fs-4 fw-bold text-dark">{{ surface_pre_fleuri|floatformat:1 }} <span class="fs-6 fw-normal text-muted">m²</span></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">
|
||||
{{ pre_fleuri_count }} {% translate "prés fleuris" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typology breakdown (if available) -->
|
||||
{% if surfaces_by_type %}
|
||||
<div class="card shadow-sm mb-4 border-0">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<span class="small fw-bold text-secondary text-uppercase"><i class="bi bi-pie-chart-fill me-1"></i>{% translate "Répartition des surfaces végétales" %}</span>
|
||||
<span class="small text-muted">{{ total_green_surface|floatformat:1 }} m²</span>
|
||||
</div>
|
||||
<!-- Progress bar -->
|
||||
<div class="progress mb-2" style="height: 10px; border-radius: 6px;">
|
||||
{% for st in surfaces_by_type %}
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width: {{ st.percent }}%; background-color: {{ st.color }};"
|
||||
title="{{ st.name }}: {{ st.total_area|floatformat:1 }} m² ({{ st.percent }}%)"
|
||||
aria-valuenow="{{ st.percent }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Badges -->
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
{% for st in surfaces_by_type %}
|
||||
<span class="badge bg-light text-dark border d-inline-flex align-items-center gap-1 py-1 px-2" style="font-size: 0.8rem;">
|
||||
<span class="d-inline-block rounded-circle" style="width: 8px; height: 8px; background-color: {{ st.color }};"></span>
|
||||
<span class="fw-semibold">{{ st.name }}</span>:
|
||||
<span class="text-muted">{{ st.total_area|floatformat:1 }} m² ({{ st.count }})</span>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Main Content Layout (Split View: Tabs/Tables on Left, Interactive Map on Right) -->
|
||||
<div class="row g-4">
|
||||
<!-- Left Column: Tabs & Tables -->
|
||||
<div class="col-lg-7 col-xl-7">
|
||||
<!-- Tabs Navigation -->
|
||||
<ul class="nav nav-tabs" id="natureLocationTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-semibold" id="surfaces-tab" data-bs-toggle="tab" data-bs-target="#surfaces-pane" type="button" role="tab">
|
||||
<i class="bi bi-grid-fill text-success me-1"></i>{% translate "Surfaces vertes" %}
|
||||
<span class="badge bg-success-subtle text-success ms-1">{{ green_surfaces_count }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-semibold" id="trees-tab" data-bs-toggle="tab" data-bs-target="#trees-pane" type="button" role="tab">
|
||||
<i class="bi bi-tree-fill text-success me-1"></i>{% translate "Arbres" %}
|
||||
<span class="badge bg-success-subtle text-success ms-1">{{ trees_count }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-semibold" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments-pane" type="button" role="tab">
|
||||
<i class="bi bi-flower3 text-info me-1"></i>{% translate "Arbustes & Bancs" %}
|
||||
<span class="badge bg-secondary-subtle text-secondary ms-1">{{ shrubs_count|add:benches_count }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-semibold" id="interventions-tab" data-bs-toggle="tab" data-bs-target="#interventions-pane" type="button" role="tab">
|
||||
<i class="bi bi-tools text-primary me-1"></i>{% translate "Interventions" %}
|
||||
<span class="badge bg-primary-subtle text-primary ms-1">{{ interventions|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-semibold" id="documents-tab" data-bs-toggle="tab" data-bs-target="#documents-pane" type="button" role="tab">
|
||||
<i class="bi bi-files text-secondary me-1"></i>{% translate "Documents" %}
|
||||
<span class="badge bg-secondary-subtle text-secondary ms-1">{{ documents|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tabs Content -->
|
||||
<div class="tab-content nature-tab-content shadow-sm" id="natureLocationTabsContent">
|
||||
|
||||
<!-- TAB 1: Surfaces végétalisées -->
|
||||
<div class="tab-pane fade show active" id="surfaces-pane" role="tabpanel" aria-labelledby="surfaces-tab">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% translate "Code" %}</th>
|
||||
<th>{% translate "Typologie / Modèle" %}</th>
|
||||
<th class="text-end">{% translate "Superficie" %}</th>
|
||||
<th>{% translate "Pente / Tonte" %}</th>
|
||||
<th>{% translate "Bulbes" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th class="text-end">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for gs in green_surfaces %}
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<a href="{% url 'assets:nature_assets_detail' 'naturegreensurface' gs.id %}" class="text-decoration-none">
|
||||
{{ gs.code }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="d-inline-block rounded-circle me-1" style="width: 8px; height: 8px; background-color: {{ gs.model.color|default:'#4caf50' }};"></span>
|
||||
{{ gs.model.get_name|default:"-" }}
|
||||
</td>
|
||||
<td class="text-end fw-semibold">
|
||||
{% if gs.surface_area %}{{ gs.surface_area|floatformat:1 }} m²{% else %}<span class="text-muted">-</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="small text-muted">
|
||||
{% if gs.slope %}{{ gs.slope }}%{% else %}-{% endif %} /
|
||||
{% if gs.mower_frenquency %}{{ gs.mower_frenquency }}x/an{% else %}-{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if gs.bulb_types %}
|
||||
<span class="badge bg-info-subtle text-info border">{{ gs.bulb_types|length }} bulbe(s)</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {% if gs.status == 'active' %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{{ gs.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if gs.lon and gs.lat %}
|
||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ gs.lon|stringformat:'f' }}, {{ gs.lat|stringformat:'f' }}, '{{ gs.code }}')" title="{% translate 'Centrer sur la carte' %}">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{% url 'assets:nature_assets_detail' 'naturegreensurface' gs.id %}" class="btn btn-outline-primary" title="{% translate 'Détail' %}">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<i class="bi bi-grid mb-2 d-block fs-3 text-muted"></i>
|
||||
{% translate "Aucune surface végétalisée enregistrée sur cette voirie." %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: Arbres -->
|
||||
<div class="tab-pane fade" id="trees-pane" role="tabpanel" aria-labelledby="trees-tab">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% translate "Code" %}</th>
|
||||
<th>{% translate "Essence / Modèle" %}</th>
|
||||
<th>{% translate "Stade" %}</th>
|
||||
<th>{% translate "Vitalité" %}</th>
|
||||
<th>{% translate "Risque" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th class="text-end">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tree in trees %}
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<a href="{% url 'assets:nature_assets_detail' 'naturetree' tree.id %}" class="text-decoration-none">
|
||||
{{ tree.code }}
|
||||
</a>
|
||||
{% if tree.is_remarkable %}
|
||||
<i class="bi bi-star-fill text-warning ms-1" title="{% translate 'Arbre remarquable' %}"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ tree.model.get_name|default:"-" }}</div>
|
||||
{% if tree.model.scientific_name %}
|
||||
<div class="small text-muted fst-italic">{{ tree.model.scientific_name }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="small">{{ tree.get_development_stage_display|default:"-" }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if tree.vitality == 'strong' %}
|
||||
<span class="badge bg-success-subtle text-success border">{% translate "Forte" %}</span>
|
||||
{% elif tree.vitality == 'normal' %}
|
||||
<span class="badge bg-info-subtle text-info border">{% translate "Normale" %}</span>
|
||||
{% elif tree.vitality == 'weak' %}
|
||||
<span class="badge bg-warning-subtle text-warning-emphasis border">{% translate "Faible" %}</span>
|
||||
{% elif tree.vitality == 'declining' or tree.vitality == 'dead' %}
|
||||
<span class="badge bg-danger">{{ tree.get_vitality_display }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if tree.risk_level == 'high' or tree.risk_level == 'imminent_danger' %}
|
||||
<span class="badge bg-danger">{{ tree.get_risk_level_display }}</span>
|
||||
{% elif tree.risk_level == 'moderate' %}
|
||||
<span class="badge bg-warning text-dark">{{ tree.get_risk_level_display }}</span>
|
||||
{% else %}
|
||||
<span class="small text-muted">{{ tree.get_risk_level_display|default:"Faible" }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {% if tree.status == 'active' %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{{ tree.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if tree.lon and tree.lat %}
|
||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ tree.lon|stringformat:'f' }}, {{ tree.lat|stringformat:'f' }}, '{{ tree.code }}')" title="{% translate 'Centrer sur la carte' %}">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{% url 'assets:nature_assets_detail' 'naturetree' tree.id %}" class="btn btn-outline-primary" title="{% translate 'Détail' %}">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<i class="bi bi-tree mb-2 d-block fs-3 text-muted"></i>
|
||||
{% translate "Aucun arbre enregistré sur cette voirie." %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 3: Arbustes & Bancs -->
|
||||
<div class="tab-pane fade" id="equipments-pane" role="tabpanel" aria-labelledby="equipments-tab">
|
||||
<!-- Arbustes -->
|
||||
<h6 class="fw-bold mb-2 text-dark"><i class="bi bi-flower3 text-success me-1"></i>{% translate "Arbustes" %} ({{ shrubs_count }})</h6>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% translate "Code" %}</th>
|
||||
<th>{% translate "Modèle" %}</th>
|
||||
<th>{% translate "Hauteur" %}</th>
|
||||
<th>{% translate "Taillé" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th class="text-end">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for shrub in shrubs %}
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<a href="{% url 'assets:nature_assets_detail' 'natureshrub' shrub.id %}" class="text-decoration-none">
|
||||
{{ shrub.code }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ shrub.model.get_name|default:"-" }}</td>
|
||||
<td>{% if shrub.height %}{{ shrub.height }} m{% else %}-{% endif %}</td>
|
||||
<td>
|
||||
{% if shrub.trimmed %}<span class="badge bg-success-subtle text-success">{% translate "Oui" %}</span>{% else %}<span class="badge bg-light text-secondary">{% translate "Non" %}</span>{% endif %}
|
||||
</td>
|
||||
<td><span class="badge bg-secondary">{{ shrub.get_status_display }}</span></td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if shrub.lon and shrub.lat %}
|
||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ shrub.lon|stringformat:'f' }}, {{ shrub.lat|stringformat:'f' }}, '{{ shrub.code }}')">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{% url 'assets:nature_assets_detail' 'natureshrub' shrub.id %}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6" class="text-center text-muted py-2 small">{% translate "Aucun arbuste répertorié." %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Bancs / Mobilier -->
|
||||
<h6 class="fw-bold mb-2 text-dark"><i class="bi bi-inbox text-warning me-1"></i>{% translate "Mobilier & Bancs" %} ({{ benches_count }})</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0" style="font-size: 0.9rem;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% translate "Code" %}</th>
|
||||
<th>{% translate "Modèle" %}</th>
|
||||
<th>{% translate "Matériau" %}</th>
|
||||
<th>{% translate "Dimensions" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th class="text-end">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bench in benches %}
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<a href="{% url 'assets:nature_assets_detail' 'naturebench' bench.id %}" class="text-decoration-none">
|
||||
{{ bench.code }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ bench.model.get_name|default:"-" }}</td>
|
||||
<td>{{ bench.material|default:"-" }}</td>
|
||||
<td>
|
||||
{% if bench.length or bench.width or bench.height %}
|
||||
{{ bench.length|default:"-" }} x {{ bench.width|default:"-" }} x {{ bench.height|default:"-" }} m
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="badge bg-secondary">{{ bench.get_status_display }}</span></td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if bench.lon and bench.lat %}
|
||||
<button type="button" class="btn btn-outline-success" onclick="zoomToAsset({{ bench.lon|stringformat:'f' }}, {{ bench.lat|stringformat:'f' }}, '{{ bench.code }}')">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{% url 'assets:nature_assets_detail' 'naturebench' bench.id %}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6" class="text-center text-muted py-2 small">{% translate "Aucun banc ou mobilier répertorié." %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 4: Interventions -->
|
||||
<div class="tab-pane fade" id="interventions-pane" role="tabpanel" aria-labelledby="interventions-tab">
|
||||
<div class="table-responsive">
|
||||
<table id="interventions-table" class="table table-hover align-middle mb-0"
|
||||
data-pagination="true"
|
||||
data-page-size="25">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th data-field="actions" class="text-center" style="width: 50px;"></th>
|
||||
<th data-field="code">{% translate "Code" %}</th>
|
||||
<th data-field="title">{% translate "Titre" %}</th>
|
||||
<th data-field="begin_date">{% translate "Début" %}</th>
|
||||
<th data-field="status">{% translate "Statut" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for interv in interventions %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'interventions:interventions_index' %}{{ interv.id }}" class="btn btn-sm btn-light detail-btn" title="{% translate 'Détail' %}">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td class="fw-bold">{{ interv.code }}</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 mb-2 d-block fs-3 text-muted"></i>
|
||||
{% translate "Aucune intervention enregistrée sur cette localisation ou ses aménagements." %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 5: Documents -->
|
||||
<div class="tab-pane fade" id="documents-pane" role="tabpanel" aria-labelledby="documents-tab">
|
||||
{% if documents %}
|
||||
<div class="row g-3">
|
||||
{% for document in documents %}
|
||||
<div class="col-6 col-sm-4 col-md-3">
|
||||
<div class="card h-100 shadow-sm border p-2 text-center">
|
||||
<a href="{{ document.url }}" target="_blank" class="text-decoration-none text-dark">
|
||||
<div class="img-thumbnail d-flex align-items-center justify-content-center mx-auto mb-2" style="width: 100%; height: 90px; overflow: hidden; background: #f8fafc;">
|
||||
{% if document.thumbnail_url %}
|
||||
<img src="{{ document.thumbnail_url }}" alt="{{ document.title }}" style="max-height: 100%; max-width: 100%; object-fit: contain;">
|
||||
{% else %}
|
||||
<i class="bi bi-file-earmark-text fs-2 text-secondary"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fw-semibold small text-truncate" title="{{ document.title }}">{{ document.title }}</div>
|
||||
{% if document.tag %}
|
||||
<span class="badge bg-primary-subtle text-primary border my-1" style="font-size: 0.7rem;">{{ document.tag }}</span>
|
||||
{% endif %}
|
||||
{% if document.date %}
|
||||
<div class="text-muted" style="font-size: 0.7rem;">{{ document.date|date:"d/m/Y" }}</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-folder2-open mb-2 d-block fs-2 text-muted"></i>
|
||||
{% translate "Aucun document associé à cette localisation." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Interactive Map -->
|
||||
<div class="col-lg-5 col-xl-5">
|
||||
<div class="card shadow-sm border-0 map-card-wrapper">
|
||||
<div class="card-header bg-white py-2 px-3 d-flex align-items-center justify-content-between flex-wrap gap-2 border-bottom">
|
||||
<span class="fw-bold text-dark d-flex align-items-center">
|
||||
<i class="bi bi-geo-alt-fill text-success me-2"></i>{% translate "Carte & Aménagements" %}
|
||||
</span>
|
||||
<!-- Map Layer Controls / Legend -->
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="form-check form-check-inline m-0 map-layer-pill">
|
||||
<input class="form-check-input" type="checkbox" id="toggle-surfaces" checked onchange="toggleNatureLayer('surfaces', this.checked)">
|
||||
<label class="form-check-label" for="toggle-surfaces"><span class="badge bg-success" style="width: 8px; height: 8px; padding: 0; display: inline-block;"></span> {% translate "Surfaces" %}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline m-0 map-layer-pill">
|
||||
<input class="form-check-input" type="checkbox" id="toggle-trees" checked onchange="toggleNatureLayer('trees', this.checked)">
|
||||
<label class="form-check-label" for="toggle-trees"><span class="badge bg-success" style="border-radius: 50%; width: 8px; height: 8px; padding: 0; display: inline-block;"></span> {% translate "Arbres" %}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline m-0 map-layer-pill">
|
||||
<input class="form-check-input" type="checkbox" id="toggle-equipments" checked onchange="toggleNatureLayer('shrubs', this.checked); toggleNatureLayer('benches', this.checked)">
|
||||
<label class="form-check-label" for="toggle-equipments"><span class="badge bg-warning" style="border-radius: 50%; width: 8px; height: 8px; padding: 0; display: inline-block;"></span> {% translate "Autres" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0 position-relative">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
{{ feature_lon|json_script:"lon_value" }}
|
||||
{{ feature_lat|json_script:"lat_value" }}
|
||||
{{ section_geojson|json_script:"section_geojson_value" }}
|
||||
{{ assets_geojson|json_script:"assets_geojson_value" }}
|
||||
<script>
|
||||
const feature_lon = JSON.parse(document.getElementById("lon_value").textContent);
|
||||
const feature_lat = JSON.parse(document.getElementById("lat_value").textContent);
|
||||
const section_geojson = JSON.parse(document.getElementById("section_geojson_value").textContent);
|
||||
const assets_geojson = JSON.parse(document.getElementById("assets_geojson_value").textContent);
|
||||
</script>
|
||||
<script src="{% static 'common/turf.min.js' %}"></script>
|
||||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||||
<script src="{% static 'assets/nature_location_detail.js' %}"></script>
|
||||
{% endblock script %}
|
||||