Compare commits
504 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f19d09ce5f | |||
| f10377648a | |||
| 2d221c543c | |||
| 5ce7179c19 | |||
| 34e4da64a8 | |||
| 3037241762 | |||
| fc8cdb579f | |||
| a6378eee90 | |||
| 0732f89274 | |||
| 34e9743cc3 | |||
| 7761f22b77 | |||
| af411b2105 | |||
| 74ef7d3a34 | |||
| 9af4f99f17 | |||
| e201a928b8 | |||
| 958d8dbc65 | |||
| b953b1f984 | |||
| e51f698030 | |||
| 15403b8604 | |||
| 61ce1d3a4d | |||
| fbd1bc452f | |||
| 45bc88d1a3 | |||
| b813de3984 | |||
|
|
da9e5c1cb8 | ||
|
|
cd3f3999cb | ||
|
|
3c8dcedccc | ||
| 095161872d | |||
| e65c402395 | |||
| 9bdd68bea2 | |||
| f271d65be2 | |||
| 587f4abf8a | |||
|
|
4823d55f08 | ||
|
|
cebe496617 | ||
| 8afa68e066 | |||
|
|
80c54a083d | ||
|
|
7d5718dec2 | ||
| bb8d6f98c0 | |||
| dd12e2fde1 | |||
| 875efc67f4 | |||
| 51e6680763 | |||
| ea02b12ea5 | |||
| b307791b90 | |||
| 9a85696d90 | |||
| 499ffe824e | |||
| 2ef730508f | |||
|
|
775151c72d | ||
|
|
e27a979a53 | ||
|
|
5825eac998 | ||
|
|
e87fe1531d | ||
|
|
0c1ae3163d | ||
|
|
496b6d9c8b | ||
|
|
58c1ccd531 | ||
| 384a2b29c3 | |||
| 288921b4aa | |||
| 3e35c98a46 | |||
| b5b0338e09 | |||
| d36649f3bf | |||
| 8807531eb3 | |||
| f81ca93590 | |||
| 69f5e65495 | |||
| 11e1674f73 | |||
| f324eb00bd | |||
| 10e9f1de50 | |||
| ab1cfb6ab2 | |||
| 6802d1e15f | |||
| 512f6786e3 | |||
| 63c4a8e539 | |||
| 93f3a49983 | |||
| 6ee0aa470b | |||
| 03fa3adaf6 | |||
| 1d212194ad | |||
| 9764b525bb | |||
| 4bb53fb71c | |||
| 53d579009a | |||
| 4010dfb138 | |||
| 32cb154815 | |||
| 358fb23d7e | |||
| 391966da07 | |||
| de6828221b | |||
| 65624c343a | |||
| 3920648000 | |||
| 8df84704dc | |||
| 927c049d02 | |||
| 41679d82e9 | |||
| 6782959253 | |||
| 3d9e7c3202 | |||
| b86d805cb5 | |||
| aa75bcbfeb | |||
| 244b16b3d3 | |||
| 1d99cdd09b | |||
| 71154a0fb4 | |||
| dc6638c4a5 | |||
| 79154085ee | |||
| a08ef0f035 | |||
| 63a9a080de | |||
| c25c4a4415 | |||
| b78c9da50e | |||
|
|
e00f9cf784 | ||
|
|
001bee8ca9 | ||
|
|
25a64b824a | ||
|
|
f429838a39 | ||
|
|
ca4d1b3c1b | ||
| 8b13236a26 | |||
| de73de7328 | |||
| c00212f79e | |||
| 37c1016b57 | |||
| 0bbe267498 | |||
| f4460a5323 | |||
| 205a05cb5b | |||
| 7911369740 | |||
| 2df4621e5b | |||
| fd794737a8 | |||
| f510a086bd | |||
| 79af86fb7f | |||
| 71cb8369a0 | |||
| ce924d7af4 | |||
| c8349a3171 | |||
| 97ecd8d9fb | |||
| ca8d8187c1 | |||
| b30e65e0a1 | |||
| c6ef935e0b | |||
| 5e9fced68f | |||
| 40ce03d812 | |||
|
|
096886a194 | ||
|
|
f19eae6067 | ||
|
|
0231f2ac00 | ||
| 6b526f7056 | |||
| 61ef7fc3ea | |||
| c7c76f28dc | |||
| 55cad6d6cc | |||
| c726d8f37c | |||
| f534758f91 | |||
| 1b93f3ae88 | |||
| de1d1c3b43 | |||
| f0fb0df8ec | |||
| ef40e48163 | |||
| 9726e8b618 | |||
| f23107e749 | |||
| 0fa237564c | |||
| e30faa6ad6 | |||
| 090c7dd03c | |||
| 3f402e3d6a | |||
| 28d6664fbd | |||
| 91e0f6d5dd | |||
| e6a09eaf37 | |||
| 63c888fdbd | |||
| cc0facdf92 | |||
| 73738b6757 | |||
| e68e6ccefe | |||
| d73a9e482d | |||
| 6e4a304930 | |||
| 74c1bf2447 | |||
| 1ab3c71754 | |||
| 63bed7737d | |||
| c20a8c4142 | |||
| a0ffebc489 | |||
| 4d0682efc1 | |||
| aad07929b9 | |||
| bbaa092550 | |||
| f2fbe7e394 | |||
| a52f205e42 | |||
| e1cf8d5565 | |||
| 3bec7cfd7c | |||
| ecb3bbddd1 | |||
| 010466e26f | |||
| 250a2b4f2f | |||
| 6c80028906 | |||
| 368a167b9c | |||
| 396d26414c | |||
| 7ca27197bf | |||
| 8fc2798c52 | |||
|
|
676e781943 | ||
|
|
836b5c8edb | ||
| 9ae7b0631b | |||
| dcf5477199 | |||
| 5ebcc1e3c9 | |||
| d2880eb3b1 | |||
| bfebdcf3dd | |||
| fe535683a1 | |||
| 660c40f0f7 | |||
| 9522eed8f8 | |||
| 099043ea8b | |||
| 3f75b511f2 | |||
| 534488814a | |||
| 641ac314e5 | |||
| e2be198802 | |||
| 57976c71eb | |||
| bd8897499b | |||
| d33b49647b | |||
| 35a20a2936 | |||
| 2ada4f88b7 | |||
| 4b1bc59f1e | |||
| 27ab3a8c1b | |||
| 6d2de3a7c8 | |||
| 429eb27e2d | |||
| 2917a38942 | |||
| 894edc72fe | |||
| c8e8223ffa | |||
| db70f8fba9 | |||
| 025ddd09fc | |||
| 82e20f648e | |||
| 1b38a8059e | |||
| 48d7796e45 | |||
| 2c207fcfbb | |||
| 41898f211f | |||
| fdc315788f | |||
| d45feb9f6e | |||
| 261ce2dd14 | |||
| fd10b3f8f7 | |||
| 26c07a769f | |||
| 4424c2d9c4 | |||
| 1e7f8123de | |||
| 91d7e0b176 | |||
| c79d9e6f85 | |||
| ae540acacc | |||
| 9657111a1c | |||
| 79697f392c | |||
| 5856acf5ed | |||
| 5bf606315e | |||
| 89794fa294 | |||
| 19810d101c | |||
| d5f4d8a765 | |||
| d9f1970ca6 | |||
|
|
1c7e369fe3 | ||
|
|
5417ca32b0 | ||
|
|
64e93d7ae4 | ||
| 787799cf11 | |||
| 5eeda20ee6 | |||
| 584df99605 | |||
| 75dd0bd50e | |||
| 135dfb41e6 | |||
| 8385a9e3e2 | |||
| 5757b28907 | |||
| 589c84b203 | |||
| 0ac214f459 | |||
| 300feca9f1 | |||
| 23936a5767 | |||
| 3a241e3d03 | |||
| 7f257a2ca3 | |||
| 9e2b2a83e6 | |||
| 74526cf7a6 | |||
| fe0e04b3ba | |||
| 4bb57962dc | |||
| a1f8b4412e | |||
| 946cbcae91 | |||
|
|
74679289a4 | ||
| d131fcb0d6 | |||
| 9702a19db8 | |||
| 61a099eb4f | |||
| fa92404ae7 | |||
| 0a745be17c | |||
| 32053f3635 | |||
| fad90d91c9 | |||
| da7de1ac7d | |||
| 28696e7339 | |||
| efdafa43de | |||
| 71b445797d | |||
| 11c81c8e00 | |||
| 7c8360bfc1 | |||
| faafa8ec23 | |||
| 9429402292 | |||
| 4824415290 | |||
|
|
da038b7c4f | ||
|
|
fecd7430ad | ||
|
|
ee90cf3173 | ||
|
|
913c00e34b | ||
| 7e80c5b055 | |||
| 2a6066381e | |||
| 2d5166e3f3 | |||
| e596be66a2 | |||
| 9181c1ab23 | |||
| 22efb9a6d9 | |||
| fd2c696780 | |||
| 022217218f | |||
| 14ab4d15df | |||
| 5f8a771277 | |||
| 5e2fb023c9 | |||
| 5c042531e5 | |||
| 32eec7b442 | |||
| 15b058e246 | |||
| a098a66204 | |||
| 22d0ad9928 | |||
| 577fb1232b | |||
| a364b6161d | |||
| 1df0c1b8c1 | |||
| ce79d913ea | |||
| 0fc77df306 | |||
| 96492a9726 | |||
| 33ef94902b | |||
| 27056b61fa | |||
| 10942c81af | |||
|
|
4a2aaf9fd0 | ||
|
|
6b8978254b | ||
| 0b190526f8 | |||
|
|
4135c6ef47 | ||
|
|
ba268f5e2a | ||
|
|
6fe9908a11 | ||
|
|
af949b8b3d | ||
|
|
ac75712828 | ||
|
|
442469f3e0 | ||
|
|
09e684a07c | ||
|
|
3ede92b24f | ||
|
|
682c2bdfcf | ||
|
|
d82dcde16d | ||
|
|
2b8cdfabb7 | ||
|
|
0b3d00fdf5 | ||
|
|
97edbde803 | ||
|
|
e6bc68ee42 | ||
|
|
be57578aa1 | ||
|
|
2a78cfc27b | ||
|
|
cb9e8a1c8b | ||
| c90ee9ece5 | |||
| 7d3362d770 | |||
| a21aa59fa9 | |||
| 04a118d04d | |||
| a50bebe427 | |||
| cfdcc41929 | |||
| f72fe1bfd7 | |||
| 22da9400f3 | |||
| d138ee98c4 | |||
| d931827c84 | |||
| 814a04f1ac | |||
| e561a77fc0 | |||
| f98878f9c4 | |||
| 0551e012f4 | |||
| ce48b99a45 | |||
| 5fd6ded07f | |||
| 04eb087cf1 | |||
| 0ab5fee09b | |||
| dda68cabec | |||
| fc7112bd98 | |||
| c40bcb7f8f | |||
| 4599537be3 | |||
| 90323a5013 | |||
| 1ee9164faf | |||
| 3ebe87099d | |||
| 3f4ecef343 | |||
| 55d021b7ef | |||
| a410cc855f | |||
| 24bf8a9bb5 | |||
|
|
76158a3be0 | ||
|
|
56281b4821 | ||
|
|
a42c46f336 | ||
| 28f8204f5a | |||
| 0d35e71cfc | |||
| 5bfde04f1a | |||
| be04a5e4c0 | |||
| cfd11654ab | |||
| cde2c7e52f | |||
| e2eb20536d | |||
| 94cb7148a0 | |||
| 80cbb80d06 | |||
| 46f1d0d384 | |||
| f810041e98 | |||
| e93d875c70 | |||
| 1671d9e449 | |||
|
|
86979307ae | ||
|
|
b15326602b | ||
|
|
ca6d09c6b1 | ||
|
|
1bdb771f3b | ||
|
|
360db5aaad | ||
|
|
ff4042b333 | ||
|
|
061fb6dc62 | ||
|
|
80f2956872 | ||
|
|
192bd1cbc4 | ||
|
|
17726812c4 | ||
| 781eca84c3 | |||
| 6e02156034 | |||
| aa6c0d0603 | |||
| 122086e2ab | |||
| dcd35d843f | |||
| d00bc6ff6d | |||
| 1fceffc293 | |||
| fb1ad643a9 | |||
| 7614d56112 | |||
| 4024dad7bc | |||
| 5878bff447 | |||
| f1f8897ad3 | |||
| f5acbe27db | |||
| 5dc4d0615a | |||
| 68d3e042dc | |||
| 06b70a3129 | |||
| d5f7b533f2 | |||
| adb5f08628 | |||
| 4e35da62b8 | |||
| 1e7e8ae697 | |||
| 016ce202b4 | |||
| 8b4c854416 | |||
| c295fc5b26 | |||
| 1ca77b6689 | |||
| 6de48bee77 | |||
| 63e22c3166 | |||
|
|
2ca72aa4da | ||
|
|
750abb703e | ||
|
|
ef3f1bdaa6 | ||
|
|
b1e7f726a2 | ||
|
|
441af33ddd | ||
|
|
34d8e51626 | ||
|
|
387f1ae9d6 | ||
|
|
0bdf44c90c | ||
|
|
6fb2e00a54 | ||
|
|
d3484e626d | ||
| 07375e3be0 | |||
| 70cdcd9b18 | |||
| 0fa9898cfa | |||
| 53ab3ff451 | |||
|
|
8283adeeac | ||
|
|
0521e2624e | ||
|
|
7b519f16bd | ||
|
|
3d55121fbc | ||
| 526c8185fa | |||
| d9ee551e42 | |||
| d3cc08cdce | |||
| 2ae78f70f6 | |||
| ee765e57f0 | |||
| fb9664b65a | |||
| 1edfb85a0e | |||
| 37500e1fbc | |||
| 91149d2597 | |||
| ea2bb4817c | |||
| d5a404037c | |||
| 7767856cb5 | |||
| 8590ddb17c | |||
| 093457ce8d | |||
| 718403c295 | |||
| 6cfd80d61e | |||
| e6166b6adb | |||
| 97269fb001 | |||
| f41b10bff8 | |||
| 41aa78e382 | |||
| e4d2ca45c5 | |||
| 8c4556a34c | |||
| 107575aa60 | |||
| f34e26f8f5 | |||
|
|
4e4d84b3a4 | ||
|
|
b736f93b75 | ||
|
|
e3cdf00515 | ||
|
|
682464b1fa | ||
|
|
93f568a9ac | ||
| bc08402f34 | |||
| 7243f33110 | |||
| 2eceb7a9b6 | |||
| b0a8af9a0f | |||
| bd1f084862 | |||
| 757f19ecce | |||
| 952f3111d6 | |||
| 73e71651bb | |||
| 79673367f0 | |||
| ccc465bf5d | |||
| 66269d0462 | |||
| 7731ece2ea | |||
| 8c85719e45 | |||
| adaf19ab39 | |||
| 41619256a9 | |||
| d725da4c57 | |||
| 6b42824d4e | |||
| 286c3e07f2 | |||
| c74e96bf29 | |||
| dad5acc4fb | |||
| 70c3ff3dea | |||
| 57367d825b | |||
| 297f51077e | |||
| 2a90736430 | |||
| 64423b3de5 | |||
| fe90a55e68 | |||
| 72ff9852bd | |||
| abfd9c3efd | |||
| 845933c33c | |||
| 0492d5ca1d | |||
| fd54d5765d | |||
| 66ab7a2f3d | |||
|
|
c139343278 | ||
|
|
9806175619 | ||
| 49019400b8 | |||
| e72b765adb | |||
| 96eecb75c2 | |||
| 4be78d7c0d | |||
| cdcff04a38 | |||
| 0620f6a908 | |||
| 3c7547ba5d | |||
| 9c822719b5 | |||
| abf4470441 | |||
| 2fb458ebfb | |||
| 91283f866b | |||
| ee6964eb5b | |||
| a8fbb87841 | |||
| b04b612dd8 | |||
| 044e01add8 | |||
| a4f6769c7d | |||
| c86fb5fb70 | |||
| a88a77e74e | |||
| ecc36f46cf | |||
| 3282c4a728 | |||
| 77a553b5c4 | |||
| 5db00ec09c | |||
| ca5d17261b | |||
| a1d9c2022c | |||
| efc0e3f955 | |||
| 24355b2e2f | |||
| f3207e946c | |||
| 034cf1e918 | |||
|
|
bec7153b7a | ||
|
|
771ac2c46a | ||
|
|
bc138c47f0 |
2368 changed files with 431657 additions and 116286 deletions
|
|
@ -25,10 +25,10 @@ venv_old/
|
|||
*.sqlite3
|
||||
|
||||
# Django media & static
|
||||
streetup/media/
|
||||
streetup/private_media/
|
||||
streetup/static/
|
||||
streetup/django.log
|
||||
loko/media/
|
||||
loko/private_media/
|
||||
loko/static/
|
||||
loko/django.log
|
||||
*.log
|
||||
|
||||
# VS Code / Editor config
|
||||
|
|
|
|||
105
.github/workflows/deploy.yml
vendored
105
.github/workflows/deploy.yml
vendored
|
|
@ -1,105 +0,0 @@
|
|||
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
22
.gitignore
vendored
|
|
@ -7,8 +7,9 @@ node_modules/
|
|||
.well-known/
|
||||
*.log
|
||||
*.md
|
||||
!streetup/common/help_docs/
|
||||
!streetup/common/help_docs/**/*.md
|
||||
!README.md
|
||||
!loko/common/help_docs/
|
||||
!loko/common/help_docs/**/*.md
|
||||
|
||||
media/
|
||||
private_media/
|
||||
|
|
@ -24,19 +25,26 @@ __pycache__/
|
|||
*$py.class
|
||||
|
||||
|
||||
streetup/.env
|
||||
loko/.env
|
||||
|
||||
scripts/
|
||||
/scripts/
|
||||
!loko/scripts/
|
||||
!loko/scripts/**
|
||||
|
||||
carl_export/
|
||||
carl_export.*
|
||||
|
||||
streetup/static/
|
||||
loko/static/
|
||||
|
||||
deploy.sh
|
||||
generated_passwords.txt
|
||||
streetup/generated_passwords.txt
|
||||
scratch/
|
||||
loko/generated_passwords.txt
|
||||
Caddyfile
|
||||
Caddyfile.prod
|
||||
.vscode/
|
||||
|
||||
# AI Training & ML Artifacts
|
||||
runs/
|
||||
*.pt
|
||||
loko/sign/ai/training/
|
||||
labels_signpanels/
|
||||
96
Dockerfile
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 streetup-env -y -c conda-forge \
|
||||
RUN mamba create -n loko-env -y -c conda-forge \
|
||||
python=3.12 \
|
||||
gdal \
|
||||
geos \
|
||||
|
|
@ -35,19 +35,19 @@ RUN mamba create -n streetup-env -y -c conda-forge \
|
|||
&& mamba clean -afy
|
||||
|
||||
# Activation par défaut de l'environnement Conda dans le conteneur
|
||||
ENV PATH=/opt/conda/envs/streetup-env/bin:$PATH
|
||||
ENV CONDA_DEFAULT_ENV=streetup-env
|
||||
ENV PATH=/opt/conda/envs/loko-env/bin:$PATH
|
||||
ENV CONDA_DEFAULT_ENV=loko-env
|
||||
|
||||
# Configuration explicite des variables GIS pour GeoDjango
|
||||
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
|
||||
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
|
||||
|
||||
# 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
|
||||
# Copie et installation des dépendances Python (mis en cache tant que requirements/ ne change pas)
|
||||
COPY requirements/ /app/requirements/
|
||||
RUN pip install --no-cache-dir -r requirements/base.txt && \
|
||||
if [ "$INSTALL_AI_LIBS" = "true" ]; then \
|
||||
|
|
@ -55,50 +55,60 @@ 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 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/
|
||||
COPY loko/scripts/ /app/scripts/
|
||||
COPY loko/common/ /app/common/
|
||||
COPY loko/home/ /app/home/
|
||||
|
||||
# 2. Copie des modules de l'application qui changent rarement
|
||||
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/
|
||||
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/
|
||||
|
||||
# 3. Copie des modules actifs ou modifiés fréquemment
|
||||
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/
|
||||
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/
|
||||
|
||||
# 4. Copie des fichiers de code de road_detection (sans réimporter les modèles de détection)
|
||||
COPY streetup/road_detection/__init__.py /app/road_detection/
|
||||
COPY streetup/road_detection/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/
|
||||
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/
|
||||
|
||||
# 5. Copie des fichiers Python et de configuration restants à la racine
|
||||
COPY streetup/*.py /app/
|
||||
COPY streetup/generated_passwords.txt* /app/
|
||||
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}
|
||||
|
||||
# Exposer le port par défaut
|
||||
EXPOSE 8000
|
||||
|
|
|
|||
70
README.md
Normal file
70
README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# 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 StreetUp</title>
|
||||
<title>Documentation Loko</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 StreetUp</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 Loko</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>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>
|
||||
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>
|
||||
<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="">streetup/
|
||||
├── streetup/ # 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="">loko/
|
||||
├── loko/ # 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="">┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ STREETUP │
|
||||
│ LOKO │
|
||||
│ 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>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
|
||||
</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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
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/${IMAGE_NAME:-streetup-core}:${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,19 +1,21 @@
|
|||
services:
|
||||
db:
|
||||
image: postgis/postgis:18-3.6
|
||||
container_name: streetup_db
|
||||
container_name: loko_db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5434:5432" # Mappé sur le port 5434 de l'hôte pour éviter tout conflit avec PostgreSQL local
|
||||
- "5436:5432" # Mappé sur le port 5436 de l'hôte
|
||||
env_file:
|
||||
- ./loko/.env
|
||||
environment:
|
||||
POSTGRES_DB: streetup
|
||||
POSTGRES_USER: admin
|
||||
POSTGRES_PASSWORD: replace_by_your_pwd
|
||||
POSTGRES_DB: ${DB_NAME:-loko}
|
||||
POSTGRES_USER: ${DB_USER:-admin}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?La variable DB_PASSWORD est obligatoire dans le fichier .env}
|
||||
volumes:
|
||||
- streetup_db_data:/var/lib/postgresql
|
||||
- ./streetup/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||
- loko_db_data:/var/lib/postgresql
|
||||
- ./loko/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U admin -d streetup" ]
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-admin} -d ${DB_NAME:-loko}" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
|
@ -25,25 +27,25 @@ services:
|
|||
args:
|
||||
# Mettre "true" pour installer ultralytics, OpenCV et PyTorch dans l'image
|
||||
INSTALL_AI_LIBS: "false"
|
||||
container_name: streetup_web
|
||||
container_name: loko_web
|
||||
restart: unless-stopped
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "8003:8000" # Mappé sur le port 8003 de l'hôte
|
||||
volumes:
|
||||
- ./streetup:/app # Volume mount pour le hot-reload local
|
||||
- streetup_media:/app/media
|
||||
- streetup_private_media:/app/private_media
|
||||
- ./loko:/app # Volume mount pour le hot-reload local
|
||||
- loko_media:/app/media
|
||||
- loko_private_media:/app/private_media
|
||||
env_file:
|
||||
- ./streetup/.env # Chargement du fichier d'environnement local
|
||||
- ./loko/.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/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
|
||||
- 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
|
||||
# Correction du chemin des logs pour rester dans le conteneur
|
||||
- LOG_FILE_PATH=/app/django.log
|
||||
depends_on:
|
||||
|
|
@ -52,22 +54,20 @@ services:
|
|||
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: streetup_caddy
|
||||
container_name: loko_caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
- "8083:80" # Mappé sur le port 8083 de l'hôte
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- streetup_caddy_data:/data
|
||||
- streetup_caddy_config:/config
|
||||
- loko_caddy_data:/data
|
||||
- loko_caddy_config:/config
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
volumes:
|
||||
streetup_db_data:
|
||||
streetup_media:
|
||||
streetup_private_media:
|
||||
streetup_caddy_data:
|
||||
streetup_caddy_config:
|
||||
loko_db_data:
|
||||
loko_media:
|
||||
loko_private_media:
|
||||
loko_caddy_data:
|
||||
loko_caddy_config:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# "command: python manage.py runserver ..." dans docker-compose.yml.
|
||||
set -e
|
||||
|
||||
exec gunicorn streetup.wsgi:application \
|
||||
exec gunicorn loko.wsgi:application \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--workers "${GUNICORN_WORKERS:-4}" \
|
||||
--threads "${GUNICORN_THREADS:-2}" \
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ ROOT_URL=
|
|||
# - Pour un usage local (hors Docker) : DB_HOST=127.0.0.1, DB_PORT=5432
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
DB_NAME=
|
||||
DB_NAME=loko
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
|
||||
|
|
@ -23,9 +23,9 @@ DB_PASSWORD=
|
|||
# GDAL_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgdal.so
|
||||
# GEOS_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgeos_c.so
|
||||
# - Docker (Miniforge/Conda interne - défini par défaut dans le Dockerfile) :
|
||||
# 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
|
||||
# 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
|
||||
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from django.urls import path, re_path, include
|
||||
from .views_version import VersionInfoView
|
||||
from .views import (
|
||||
GeoProxyView,
|
||||
InterventionListCreateView,
|
||||
|
|
@ -10,6 +11,7 @@ from .views import (
|
|||
app_name = "zapi"
|
||||
|
||||
urlpatterns = [
|
||||
path("version/", VersionInfoView.as_view(), name="system-version"),
|
||||
# API root (optionnel: une vue d'info/health)
|
||||
# path("", APIRootView.as_view(), name="root"),
|
||||
|
||||
59
loko/api/views_version.py
Normal file
59
loko/api/views_version.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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())
|
||||
|
|
@ -321,7 +321,10 @@ admin.site.register(ArtworkFountainDocument, AssetAdmin)
|
|||
admin.site.register(ArtworkMonumentDocument, AssetAdmin)
|
||||
|
||||
|
||||
from .models import Structure, StructureLocation, StructureDocument, StructureAssetModel, StructureGeoAsset, StructureContract
|
||||
from .models import (
|
||||
Structure, StructureLocation, StructureDocument, StructureAssetModel, StructureGeoAsset, StructureContract,
|
||||
StructureExceptionalTransport, StructureExceptionalTransportNotice
|
||||
)
|
||||
|
||||
|
||||
def make_folder_attachment_inline(thematic_code):
|
||||
|
|
@ -359,12 +362,26 @@ StructureFolderAttachmentInline = make_folder_attachment_inline('structures')
|
|||
ControlCenterFolderAttachmentInline = make_folder_attachment_inline('control_centers')
|
||||
TrafficLightFolderAttachmentInline = make_folder_attachment_inline('traffic_lights')
|
||||
|
||||
|
||||
class StructureExceptionalTransportInline(admin.StackedInline):
|
||||
model = StructureExceptionalTransport
|
||||
extra = 0
|
||||
can_delete = False
|
||||
verbose_name = "Transport exceptionnel (Convois lourds)"
|
||||
verbose_name_plural = "Transport exceptionnel (Convois lourds)"
|
||||
|
||||
|
||||
class StructureExceptionalTransportNoticeInline(admin.TabularInline):
|
||||
model = StructureExceptionalTransportNotice
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(Structure)
|
||||
class StructureAdmin(AssetAdmin):
|
||||
search_fields = ("code", "name_fr", "name_nl")
|
||||
list_display = ("code", "name_fr", "structure_type")
|
||||
list_filter = ("structure_type",)
|
||||
inlines = [StructureFolderAttachmentInline]
|
||||
inlines = [StructureFolderAttachmentInline, StructureExceptionalTransportInline, StructureExceptionalTransportNoticeInline]
|
||||
|
||||
@admin.register(StructureLocation)
|
||||
class StructureLocationAdmin(AssetAdmin):
|
||||
|
|
@ -373,9 +390,21 @@ class StructureLocationAdmin(AssetAdmin):
|
|||
admin.site.register(StructureDocument, AssetAdmin)
|
||||
admin.site.register(StructureAssetModel, AssetAdmin)
|
||||
admin.site.register(StructureGeoAsset, AssetAdmin)
|
||||
|
||||
admin.site.register(StructureContract, AssetAdmin)
|
||||
|
||||
@admin.register(StructureExceptionalTransport)
|
||||
class StructureExceptionalTransportAdmin(admin.ModelAdmin):
|
||||
search_fields = ("structure__code", "structure__name_fr", "structure__name_nl", "bridge_class", "national_id", "fod_vnr")
|
||||
list_display = ("structure", "passage_status", "max_authorized_weight_tonnes", "max_calculated_weight_tonnes", "max_speed_kmh", "updated_at")
|
||||
list_filter = ("passage_status", "is_braking_forbidden", "is_isolated_crossing_only", "is_railway_metro_crossing")
|
||||
|
||||
@admin.register(StructureExceptionalTransportNotice)
|
||||
class StructureExceptionalTransportNoticeAdmin(admin.ModelAdmin):
|
||||
search_fields = ("structure__code", "mission_reference")
|
||||
list_display = ("structure", "mission_reference", "notice_date", "result", "evaluated_total_weight_tonnes")
|
||||
list_filter = ("result", "notice_date")
|
||||
|
||||
|
||||
|
||||
|
||||
from .models import ControlCenter, ControlCenterLocation, ControlCenterDocument, ControlCenterAssetModel, ControlCenterGeoAsset, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument
|
||||
|
|
@ -422,7 +451,10 @@ admin.site.register(ComplementaryPoliceRegulation, AssetAdmin)
|
|||
admin.site.register(SignParkingSpot, AssetAdmin)
|
||||
|
||||
|
||||
from .models import ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract
|
||||
from .models import (
|
||||
ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract,
|
||||
ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
||||
)
|
||||
|
||||
admin.site.register(ITSAssetModel, AssetAdmin)
|
||||
|
||||
|
|
@ -439,6 +471,44 @@ class ITSGeoAssetContractAdmin(AssetAdmin):
|
|||
list_filter = ("contract",)
|
||||
|
||||
|
||||
class ITSBollardPlotInline(admin.TabularInline):
|
||||
model = ITSBollardPlot
|
||||
extra = 0
|
||||
fields = ("code", "position", "bollard_type", "model", "serial_number", "status")
|
||||
show_change_link = True
|
||||
|
||||
|
||||
class ITSBollardSystemAssetInline(admin.TabularInline):
|
||||
model = ITSBollardSystemAsset
|
||||
extra = 0
|
||||
fields = ("asset", "role", "comment")
|
||||
autocomplete_fields = ("asset",)
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(ITSBollardSystem)
|
||||
class ITSBollardSystemAdmin(AssetGISAdmin, AssetAdmin):
|
||||
search_fields = ("code", "name_fr", "name_nl", "location__code", "location__name_fr")
|
||||
list_display = ("code", "name_fr", "location", "width", "direction", "status", "plots_count")
|
||||
list_filter = ("status", "direction", "location")
|
||||
inlines = [ITSBollardPlotInline, ITSBollardSystemAssetInline]
|
||||
|
||||
|
||||
@admin.register(ITSBollardPlot)
|
||||
class ITSBollardPlotAdmin(AssetGISAdmin, AssetAdmin):
|
||||
search_fields = ("code", "name_fr", "system__code", "serial_number")
|
||||
list_display = ("code", "system", "position", "bollard_type", "model", "status")
|
||||
list_filter = ("bollard_type", "status", "system")
|
||||
|
||||
|
||||
@admin.register(ITSBollardSystemContract)
|
||||
class ITSBollardSystemContractAdmin(AssetAdmin):
|
||||
search_fields = ("system__code", "contract__contract_number")
|
||||
list_display = ("system", "contract", "status", "maintenance_type")
|
||||
list_filter = ("status", "maintenance_type", "contract")
|
||||
|
||||
|
||||
|
||||
from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
|
||||
|
||||
admin.site.register(ITSLocation, AssetAdmin)
|
||||
|
|
@ -648,6 +718,127 @@ admin.site.register(CleanGlassContainer, AssetAdmin)
|
|||
admin.site.register(CleanLocationContract, AssetAdmin)
|
||||
|
||||
from .models import CleanLitterBinPhoto, CleanLitterBinObservation
|
||||
from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument
|
||||
from .models import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||
|
||||
admin.site.register(CleanLitterBinPhoto, AssetAdmin)
|
||||
admin.site.register(CleanLitterBinObservation, AssetAdmin)
|
||||
class PublicBuildingDocumentInline(admin.TabularInline):
|
||||
model = PublicBuildingDocument
|
||||
extra = 1
|
||||
fk_name = "building"
|
||||
fields = ("document_type", "floor_location", "file", "description")
|
||||
raw_id_fields = ("floor_location",)
|
||||
|
||||
@admin.register(PublicBuilding)
|
||||
class PublicBuildingAdmin(admin.GISModelAdmin):
|
||||
list_display = ("code", "name_fr", "location_type", "parent_location", "room_number", "level_number", "usage_type", "building_status", "is_heated", "is_erp", "dpe_rating")
|
||||
list_filter = ("location_type", "floor_type", "is_heated", "usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible")
|
||||
search_fields = ("code", "name_fr", "name_nl", "address", "owner", "room_number")
|
||||
raw_id_fields = ("parent_location", "manager")
|
||||
inlines = [PublicBuildingDocumentInline]
|
||||
|
||||
@admin.register(BuildingAsset)
|
||||
class BuildingAssetAdmin(admin.GISModelAdmin):
|
||||
list_display = ("code", "name_fr", "asset_type", "building", "location", "parent_asset", "material", "power_kw", "status", "obsolescence", "next_control_date")
|
||||
list_filter = ("asset_type", "energy_source", "glazing_type", "opening_type", "status", "obsolescence")
|
||||
search_fields = ("code", "name_fr", "name_nl", "serial_number", "material", "maintenance_contract_ref", "contractor_name")
|
||||
raw_id_fields = ("building", "location", "parent_asset")
|
||||
|
||||
@admin.register(PublicBuildingDocument)
|
||||
class PublicBuildingDocumentAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "building", "floor_location", "document_type", "file", "upload_date")
|
||||
list_filter = ("document_type",)
|
||||
search_fields = ("description", "building__code", "building__name_fr", "floor_location__code")
|
||||
raw_id_fields = ("building", "floor_location")
|
||||
|
||||
@admin.register(OutdoorLocation)
|
||||
class OutdoorLocationAdmin(admin.GISModelAdmin):
|
||||
list_display = ("code", "name_fr", "site_type", "parent_location", "owner", "manager", "pmr_accessible", "has_water_point", "has_power_supply", "is_fenced")
|
||||
list_filter = ("site_type", "pmr_accessible", "has_water_point", "has_power_supply", "is_fenced")
|
||||
search_fields = ("code", "name_fr", "name_nl", "address", "owner")
|
||||
raw_id_fields = ("parent_location", "manager")
|
||||
|
||||
@admin.register(OutdoorAsset)
|
||||
class OutdoorAssetAdmin(admin.GISModelAdmin):
|
||||
list_display = ("code", "name_fr", "asset_type", "site", "location", "norm", "last_annual_inspection", "next_annual_inspection", "status")
|
||||
list_filter = ("asset_type", "norm", "status", "obsolescence")
|
||||
search_fields = ("code", "name_fr", "name_nl", "serial_number", "certifying_body")
|
||||
raw_id_fields = ("site", "location", "parent_asset")
|
||||
|
||||
admin.site.register(OutdoorLocationDocument, AssetAdmin)
|
||||
|
||||
from .models import ParkingLocation, ParkingSpot
|
||||
|
||||
@admin.register(ParkingLocation)
|
||||
class ParkingLocationAdmin(AssetGISAdmin):
|
||||
list_display = ("code", "name_fr", "name_nl", "address")
|
||||
search_fields = ("code", "name_fr", "name_nl", "address")
|
||||
|
||||
@admin.register(ParkingSpot)
|
||||
class ParkingSpotAdmin(AssetGISAdmin):
|
||||
list_display = (
|
||||
"code", "name_fr", "spot_type", "stand_type",
|
||||
"standard_stand_count", "cargo_stand_count", "drop_off_stand_count", "total_stand_count",
|
||||
"has_scooter_drop_zone", "is_compliant", "inspection_status", "status"
|
||||
)
|
||||
list_filter = (
|
||||
"spot_type", "stand_type", "has_scooter_drop_zone",
|
||||
"is_compliant", "inspection_status", "marking_quality", "is_paid", "status"
|
||||
)
|
||||
search_fields = ("code", "name_fr", "name_nl", "note")
|
||||
filter_horizontal = ("poles", "panels")
|
||||
raw_id_fields = ("location", "street")
|
||||
|
||||
# ─── Management Zones ──────────────────────────────────────────────────────────
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.contrib.contenttypes.admin import GenericTabularInline
|
||||
from .models import ManagementZoneType, ManagementZone, ManagementZoneAsset, ManagementZoneDocument
|
||||
|
||||
|
||||
class ManagementZoneDocumentInline(admin.TabularInline):
|
||||
model = ManagementZoneDocument
|
||||
extra = 1
|
||||
|
||||
|
||||
class ManagementZoneAssetInline(GenericTabularInline):
|
||||
model = ManagementZoneAsset
|
||||
ct_field = 'content_type'
|
||||
ct_fk_field = 'object_id'
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(ManagementZoneType)
|
||||
class ManagementZoneTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('code', 'name_fr', 'name_nl', 'thematic', 'color_badge')
|
||||
list_filter = ('thematic',)
|
||||
search_fields = ('code', 'name_fr', 'name_nl', 'description')
|
||||
|
||||
def color_badge(self, obj):
|
||||
color = obj.default_color or "#198754"
|
||||
return format_html(
|
||||
'<span style="display:inline-block;width:16px;height:16px;background-color:{};border:1px solid #999;border-radius:3px;vertical-align:middle;margin-right:6px;"></span>{}',
|
||||
color, color
|
||||
)
|
||||
color_badge.short_description = "Couleur"
|
||||
|
||||
|
||||
@admin.register(ManagementZone)
|
||||
class ManagementZoneAdmin(AssetGISAdmin):
|
||||
list_display = ('code', 'name_fr', 'thematic', 'zone_type', 'color_badge', 'target_frequency_days', 'is_active', 'has_geom')
|
||||
list_filter = ('thematic', 'zone_type', 'is_active')
|
||||
search_fields = ('code', 'name_fr', 'name_nl', 'instructions', 'description', 'seasonality_notes')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
inlines = [ManagementZoneDocumentInline, ManagementZoneAssetInline]
|
||||
|
||||
def color_badge(self, obj):
|
||||
color = obj.get_color()
|
||||
return format_html(
|
||||
'<span style="display:inline-block;width:16px;height:16px;background-color:{};border:1px solid #999;border-radius:3px;vertical-align:middle;margin-right:6px;"></span>{}',
|
||||
color, color
|
||||
)
|
||||
color_badge.short_description = "Couleur"
|
||||
|
||||
def has_geom(self, obj):
|
||||
return bool(obj.geom)
|
||||
has_geom.boolean = True
|
||||
has_geom.short_description = "Géométrie"
|
||||
|
|
@ -47,3 +47,22 @@ class AssetsConfig(AppConfig):
|
|||
).exists()
|
||||
|
||||
register('assets', 'TrafficLightControllerLogFile', 'file', guard_tl_controller_log)
|
||||
|
||||
# Guard for inspection documents (InspectionDocument)
|
||||
def guard_inspection_document(obj, user):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
return True
|
||||
|
||||
register("assets", "InspectionDocument", "file", guard_inspection_document)
|
||||
|
||||
# Guards for PublicBuilding floor plans and documents
|
||||
def guard_publicbuilding_floor_plan(obj, user):
|
||||
return user.is_authenticated
|
||||
|
||||
register("assets", "PublicBuilding", "floor_plan", guard_publicbuilding_floor_plan)
|
||||
|
||||
def guard_publicbuilding_document(obj, user):
|
||||
return user.is_authenticated
|
||||
|
||||
register("assets", "PublicBuildingDocument", "file", guard_publicbuilding_document)
|
||||
558
loko/assets/forms.py
Normal file
558
loko/assets/forms.py
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
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'})
|
||||
BIN
loko/assets/locale/en/LC_MESSAGES/django.mo
Normal file
BIN
loko/assets/locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load diff
BIN
loko/assets/locale/en/LC_MESSAGES/djangojs.mo
Normal file
BIN
loko/assets/locale/en/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
746
loko/assets/locale/en/LC_MESSAGES/djangojs.po
Normal file
746
loko/assets/locale/en/LC_MESSAGES/djangojs.po
Normal file
|
|
@ -0,0 +1,746 @@
|
|||
# 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"
|
||||
BIN
loko/assets/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
loko/assets/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
8819
loko/assets/locale/fr/LC_MESSAGES/django.po
Normal file
8819
loko/assets/locale/fr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
loko/assets/locale/fr/LC_MESSAGES/djangojs.mo
Normal file
BIN
loko/assets/locale/fr/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
751
loko/assets/locale/fr/LC_MESSAGES/djangojs.po
Normal file
751
loko/assets/locale/fr/LC_MESSAGES/djangojs.po
Normal file
|
|
@ -0,0 +1,751 @@
|
|||
# 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"
|
||||
BIN
loko/assets/locale/nl/LC_MESSAGES/django.mo
Normal file
BIN
loko/assets/locale/nl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
8767
loko/assets/locale/nl/LC_MESSAGES/django.po
Normal file
8767
loko/assets/locale/nl/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
loko/assets/locale/nl/LC_MESSAGES/djangojs.mo
Normal file
BIN
loko/assets/locale/nl/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
748
loko/assets/locale/nl/LC_MESSAGES/djangojs.po
Normal file
748
loko/assets/locale/nl/LC_MESSAGES/djangojs.po
Normal file
|
|
@ -0,0 +1,748 @@
|
|||
# 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"
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
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.")
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
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 !"))
|
||||
210
loko/assets/management/commands/create_structure_folders.py
Normal file
210
loko/assets/management/commands/create_structure_folders.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
"""
|
||||
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}"
|
||||
))
|
||||
373
loko/assets/management/commands/import_as_built_documents.py
Normal file
373
loko/assets/management/commands/import_as_built_documents.py
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
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}"))
|
||||
278
loko/assets/management/commands/import_cse_parking.py
Normal file
278
loko/assets/management/commands/import_cse_parking.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
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()})."))
|
||||
293
loko/assets/management/commands/import_cse_signage.py
Normal file
293
loko/assets/management/commands/import_cse_signage.py
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
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()}"))
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
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."))
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
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)."
|
||||
1305
loko/assets/management/commands/import_fulcrum_trees.py
Normal file
1305
loko/assets/management/commands/import_fulcrum_trees.py
Normal file
File diff suppressed because it is too large
Load diff
125
loko/assets/management/commands/load_base_asset_categories.py
Normal file
125
loko/assets/management/commands/load_base_asset_categories.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
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}")
|
||||
85
loko/assets/management/commands/sync_all_geojson.py
Normal file
85
loko/assets/management/commands/sync_all_geojson.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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."))
|
||||
379
loko/assets/management/commands/sync_asset_parent_data.py
Normal file
379
loko/assets/management/commands/sync_asset_parent_data.py
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
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!"))
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
# Generated by Django 6.0.7 on 2026-07-26 13:20
|
||||
|
||||
import assets.models.buildings
|
||||
import common.storages
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0004_initial'),
|
||||
('documents', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BuildingLevel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50, verbose_name='Code du niveau')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Nom du niveau')),
|
||||
('level_number', models.IntegerField(default=0, verbose_name="Numéro d'étage (0=RDC, 1=1er, -1=Sous-sol)")),
|
||||
('floor_plan', models.FileField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage")),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Niveau / Étage',
|
||||
'verbose_name_plural': 'Niveaux / Étages',
|
||||
'ordering': ['building', 'level_number'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PublicBuilding',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50)),
|
||||
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
||||
('installation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
||||
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
||||
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
||||
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('lon', models.FloatField(blank=True, null=True)),
|
||||
('lat', models.FloatField(blank=True, null=True)),
|
||||
('geojson', models.TextField(blank=True, null=True)),
|
||||
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise cartographique')),
|
||||
('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse complète')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('owner', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propriétaire')),
|
||||
('occupant_entity', models.CharField(blank=True, max_length=255, null=True, verbose_name='Direction / Service occupant')),
|
||||
('usage_type', models.CharField(choices=[('administrative', 'Administratif / Mairie'), ('education', 'Enseignement / École / Crèche'), ('sports', 'Sport & Équipement sportif'), ('culture', 'Culture / Médiathèque / Musée'), ('social', 'Social / Centre communautaire'), ('technical', 'Technique / Ateliers / Stock'), ('housing', 'Logement communal'), ('other', 'Autre bâtiment public')], default='administrative', max_length=50, verbose_name="Type d'usage principal")),
|
||||
('building_status', models.CharField(choices=[('in_service', 'En service'), ('under_renovation', 'En travaux / Rénovation'), ('decommissioned', 'Désaffecté'), ('ceded', 'Cédé / Vendu')], default='in_service', max_length=50, verbose_name="Statut d'occupation")),
|
||||
('construction_year', models.IntegerField(blank=True, null=True, verbose_name='Année de construction')),
|
||||
('floors_count', models.IntegerField(default=1, verbose_name="Nombre d'étages hors-sol")),
|
||||
('basement_count', models.IntegerField(default=0, verbose_name='Nombre de niveaux en sous-sol')),
|
||||
('total_surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface brute totale (m²)')),
|
||||
('usable_surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface utile (m²)')),
|
||||
('is_erp', models.BooleanField(default=True, verbose_name='Établissement Recevant du Public (ERP)')),
|
||||
('erp_category', models.CharField(blank=True, choices=[('1', '1ère catégorie (> 1500 pers.)'), ('2', '2ème catégorie (301 - 1500 pers.)'), ('3', '3ème catégorie (101 - 300 pers.)'), ('4', "4ème catégorie (jusqu'à 100 pers.)"), ('5', '5ème catégorie (seuils spécifiques)')], max_length=10, null=True, verbose_name='Catégorie ERP')),
|
||||
('erp_type', models.CharField(blank=True, choices=[('J', "J - Structures d'accueil pour personnes âgées ou handicapées"), ('L', 'L - Salles de spectacles, de réunions, polyvalentes'), ('M', 'M - Magasins, centres commerciaux'), ('N', 'N - Restaurants et débits de boissons'), ('O', 'O - Hôtels et pensions de famille'), ('P', 'P - Salles de danse et salles de jeux'), ('R', "R - Établissements d'éveil, d'enseignement, colonies de vacances"), ('S', 'S - Bibliothèques, centres de documentation'), ('T', "T - Salles d'expositions"), ('U', 'U - Établissements de soins'), ('V', 'V - Établissements de culte'), ('W', 'W - Administrations, banques, bureaux'), ('X', 'X - Établissements sportifs couverts'), ('Y', 'Y - Musées')], max_length=10, null=True, verbose_name='Type ERP')),
|
||||
('max_capacity', models.IntegerField(blank=True, null=True, verbose_name="Jauge maximale d'accueil du public")),
|
||||
('dpe_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Diagnostic de Performance Énergétique (DPE)')),
|
||||
('ges_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Émission de Gaz à Effet de Serre (GES)')),
|
||||
('has_asbestos_diag', models.BooleanField(default=False, verbose_name='Diagnostic Amiante réalisé')),
|
||||
('has_lead_diag', models.BooleanField(default=False, verbose_name='Diagnostic Plomb réalisé')),
|
||||
('pmr_accessible', models.BooleanField(default=False, verbose_name='Accessible PMR')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
||||
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_buildings', to=settings.AUTH_USER_MODEL, verbose_name='Gestionnaire référent')),
|
||||
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.publicbuilding', verbose_name='Remplacé par')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Bâtiment public',
|
||||
'verbose_name_plural': 'Bâtiments publics',
|
||||
'ordering': ['code', 'name_fr'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BuildingRoom',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50, verbose_name='Code du local')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Nom de la pièce')),
|
||||
('room_type', models.CharField(blank=True, max_length=100, null=True, verbose_name='Type de local (ex: Bureau, Classe, Sanitaires)')),
|
||||
('surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface (m²)')),
|
||||
('level', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rooms', to='assets.buildinglevel', verbose_name='Niveau / Étage')),
|
||||
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='assets.publicbuilding', verbose_name='Bâtiment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Pièce / Local',
|
||||
'verbose_name_plural': 'Pièces / Locaux',
|
||||
'ordering': ['building', 'code'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildinglevel',
|
||||
name='building',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='assets.publicbuilding', verbose_name='Bâtiment'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BuildingEquipment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50)),
|
||||
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
||||
('installation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
||||
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
||||
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
||||
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('equipment_type', models.CharField(choices=[('hvac', 'Chauffage / CVC / Chaudière'), ('elevator', 'Ascenseur / Monte-charge'), ('fire_safety', 'Sécurité incendie / Extincteur / RIA'), ('electricity', 'Tableau électrique / TGBT'), ('alarm', "Alarme / Contrôle d'accès"), ('meter', 'Compteur (Eau / Gaz / Électricité)'), ('sanitary', 'Plomberie / Sanitaires'), ('other', 'Autre équipement technique')], default='hvac', max_length=50, verbose_name="Type d'équipement")),
|
||||
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle réglementaire')),
|
||||
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle réglementaire')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
||||
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingequipment', verbose_name='Remplacé par')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||
('room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipments', to='assets.buildingroom', verbose_name='Local')),
|
||||
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipments', to='assets.publicbuilding', verbose_name='Bâtiment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Équipement technique de bâtiment',
|
||||
'verbose_name_plural': 'Équipements techniques de bâtiment',
|
||||
'ordering': ['building', 'code'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PublicBuildingDocument',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description du document')),
|
||||
('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')),
|
||||
('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')),
|
||||
('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.buildings.upload_to_building, verbose_name='Fichier')),
|
||||
('document_type', models.CharField(default='other', max_length=50, verbose_name='Type de document')),
|
||||
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.publicbuilding', verbose_name='Bâtiment')),
|
||||
('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')),
|
||||
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document de bâtiment',
|
||||
'verbose_name_plural': 'Documents de bâtiment',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
# Generated by Django 6.0.7 on 2026-07-27 12:00
|
||||
|
||||
import common.storages
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0005_buildinglevel_publicbuilding_buildingroom_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='buildinglevel',
|
||||
name='building',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='buildingroom',
|
||||
name='level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='buildingroom',
|
||||
name='building',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='publicbuilding',
|
||||
options={'ordering': ['code', 'name_fr'], 'verbose_name': 'Localisation Bâtiment public', 'verbose_name_plural': 'Localisations Bâtiments publics'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='brand',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='category',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='installation_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='last_corrective_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='last_inspection_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='last_preventive_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='last_survey_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='marked_as_removed_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='marked_as_removed_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='note',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='obsolescence',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='replaced_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='serial_number',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='uninstallation_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='validation_status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='publicbuilding',
|
||||
name='warranty_duration',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='floor_plan',
|
||||
field=models.FileField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='is_main_location',
|
||||
field=models.BooleanField(default=False, verbose_name='Localisation principale'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='level_number',
|
||||
field=models.IntegerField(default=0, verbose_name="Numéro d'étage (ex: 0=RDC, 1=1er, -1=Sous-sol)"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='location_type',
|
||||
field=models.CharField(choices=[('site', 'Site / Complexe'), ('building', 'Bâtiment / Bloc'), ('level', 'Niveau / Étage'), ('room', 'Pièce / Local'), ('other', 'Autre localisation')], default='building', max_length=50, verbose_name='Type de localisation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='parent_location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.publicbuilding', verbose_name='Localisation parente'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publicbuilding',
|
||||
name='surface_m2',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Surface spécifique (m²)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='dpe_rating',
|
||||
field=models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Performance Énergétique (PEB / DPE / EPC)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='erp_category',
|
||||
field=models.CharField(blank=True, choices=[('1', '1ère catégorie (> 1500 pers.)'), ('2', '2ème catégorie (301 - 1500 pers.)'), ('3', '3ème catégorie (101 - 300 pers.)'), ('4', "4ème catégorie (jusqu'à 100 pers.)"), ('5', '5ème catégorie (seuils spécifiques)')], max_length=10, null=True, verbose_name="Catégorie d'accueil / ERP"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='erp_type',
|
||||
field=models.CharField(blank=True, choices=[('J', "J - Structures d'accueil pour personnes âgées ou handicapées"), ('L', 'L - Salles de spectacles, de réunions, polyvalentes'), ('M', 'M - Magasins, centres commerciaux'), ('N', 'N - Restaurants et débits de boissons'), ('O', 'O - Hôtels et pensions de famille'), ('P', 'P - Salles de danse et salles de jeux'), ('R', "R - Établissements d'éveil, d'enseignement, colonies de vacances"), ('S', 'S - Bibliothèques, centres de documentation'), ('T', "T - Salles d'expositions"), ('U', 'U - Établissements de soins'), ('V', 'V - Établissements de culte'), ('W', 'W - Administrations, banques, bureaux'), ('X', 'X - Établissements sportifs couverts'), ('Y', 'Y - Musées')], max_length=10, null=True, verbose_name="Type d'établissement / ERP"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='ges_rating',
|
||||
field=models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Émissions de Carbone & GES'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='is_erp',
|
||||
field=models.BooleanField(default=True, verbose_name='Accueil & Sécurité du Public (ERP / PMR)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='updated_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='publicbuilding',
|
||||
name='usage_type',
|
||||
field=models.CharField(choices=[('administrative', 'Administrative (Town Hall / Municipal Building)'), ('education', 'Enseignement / École / Crèche'), ('sports', 'Sport & Équipement sportif'), ('culture', 'Culture / Médiathèque / Musée'), ('social', 'Social / Centre communautaire'), ('technical', 'Technique / Ateliers / Stock'), ('housing', 'Logement communal'), ('other', 'Autre bâtiment public')], default='administrative', max_length=50, verbose_name="Type d'usage principal"),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BuildingAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50)),
|
||||
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
||||
('installation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
||||
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
||||
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
||||
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('lon', models.FloatField(blank=True, null=True)),
|
||||
('lat', models.FloatField(blank=True, null=True)),
|
||||
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)),
|
||||
('geojson', models.TextField(blank=True, null=True)),
|
||||
('asset_type', models.CharField(choices=[('civil_engineering', 'Génie civil / Bâti'), ('facade', 'Façade / Mur extérieur'), ('wall', 'Mur intérieur / Cloison'), ('roof', 'Toiture / Couverture'), ('hvac', 'Chauffage / CVC / Chaudière'), ('elevator', 'Ascenseur / Monte-charge'), ('electricity', 'Électricité / TGBT'), ('fire_safety', 'Sécurité incendie / Extincteur / RIA'), ('alarm', "Alarme / Contrôle d'accès"), ('meter', 'Compteur (Eau / Gaz / Électricité)'), ('sanitary', 'Plomberie / Sanitaires'), ('other', 'Autre équipement / asset')], default='civil_engineering', max_length=50, verbose_name="Type d'asset / Équipement")),
|
||||
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle réglementaire')),
|
||||
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle réglementaire')),
|
||||
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_building_assets', to='assets.publicbuilding', verbose_name='Bâtiment / Site principal')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
||||
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='location_assets', to='assets.publicbuilding', verbose_name='Localisation spécifique')),
|
||||
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
||||
('parent_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_assets', to='assets.buildingasset', verbose_name='Asset parent')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingasset', verbose_name='Remplacé par')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Asset / Équipement de bâtiment',
|
||||
'verbose_name_plural': 'Assets / Équipements de bâtiment',
|
||||
'ordering': ['building', 'code'],
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BuildingEquipment',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BuildingLevel',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BuildingRoom',
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
# Generated by Django 6.0.7 on 2026-07-27 12:54
|
||||
|
||||
import assets.models.outdoor
|
||||
import common.storages
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0006_remove_buildinglevel_building_and_more'),
|
||||
('documents', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OutdoorLocation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50)),
|
||||
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('lon', models.FloatField(blank=True, null=True)),
|
||||
('lat', models.FloatField(blank=True, null=True)),
|
||||
('geojson', models.TextField(blank=True, null=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise cartographique')),
|
||||
('site_type', models.CharField(choices=[('playground', 'Aire / Plaine de jeux'), ('sports_ground', 'Terrain de sport extérieur'), ('multisports_agora', 'Terrain multisports / City-stadium'), ('skatepark_pumptrack', 'Skatepark / Pumptrack / Bowl'), ('outdoor_fitness', 'Fitness plein air / Street Workout'), ('open_air_theater', 'Théâtre de verdure / Scène extérieure'), ('park_recreation', 'Parc & Espace récréatif'), ('dog_park', 'Espace canin / Canisite'), ('cemetery', 'Cimetière communal'), ('event_plaza', 'Place publique / Zone événementielle'), ('other', 'Autre espace plein air')], default='playground', max_length=50, verbose_name='Type de site plein air')),
|
||||
('is_main_location', models.BooleanField(default=False, verbose_name='Localisation principale')),
|
||||
('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse complète')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('owner', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propriétaire')),
|
||||
('occupant_entity', models.CharField(blank=True, max_length=255, null=True, verbose_name='Direction / Service gestionnaire')),
|
||||
('surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface totale (m²)')),
|
||||
('max_capacity', models.IntegerField(blank=True, null=True, verbose_name="Jauge / Capacité d'accueil")),
|
||||
('pmr_accessible', models.BooleanField(default=False, verbose_name='Accessible PMR')),
|
||||
('has_water_point', models.BooleanField(default=False, verbose_name="Point d'eau potable")),
|
||||
('has_power_supply', models.BooleanField(default=False, verbose_name='Alimentation électrique disponible')),
|
||||
('is_fenced', models.BooleanField(default=False, verbose_name='Site clôturé / Sécurisé')),
|
||||
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_outdoor_locations', to=settings.AUTH_USER_MODEL, verbose_name='Gestionnaire référent')),
|
||||
('parent_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.outdoorlocation', verbose_name='Localisation parente')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Espace public d'extérieur",
|
||||
'verbose_name_plural': "Espaces publics d'extérieur",
|
||||
'ordering': ['code', 'name_fr'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OutdoorAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50)),
|
||||
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
||||
('installation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
||||
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
||||
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
||||
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
||||
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
||||
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
||||
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('lon', models.FloatField(blank=True, null=True)),
|
||||
('lat', models.FloatField(blank=True, null=True)),
|
||||
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)),
|
||||
('geojson', models.TextField(blank=True, null=True)),
|
||||
('asset_type', models.CharField(choices=[('play_structure', 'Structure combinée / Tour de jeu'), ('swing', 'Balançoire / Portique'), ('spring_rider', 'Jeu à ressort / Bascule'), ('slide', 'Toboggan'), ('zip_line', 'Tyrolienne'), ('amortizing_surface', 'Sol amortissant / Réception'), ('sports_goal', 'But / Cage / Panneau de basket'), ('fitness_station', 'Agrès de fitness outdoor / Workout'), ('skate_module', 'Module de skate / Rampe / Rail'), ('stage_platform', 'Scène extérieure / Podium'), ('audience_seating', 'Gradins / Bancs de spectateurs'), ('event_power_box', 'Borne foraine / Armoire électrique événementielle'), ('outdoor_lighting', 'Éclairage plein air / Projecteur scénique'), ('bench_table', 'Banc / Table de pique-nique'), ('shade_shelter', "Kiosque / Voile d'ombrage / Abri"), ('drinking_fountain', "Fontaine d'eau potable"), ('fence_gate', 'Clôture / Portillon de sécurité'), ('info_panel', "Panneau d'information / Consignes"), ('other', 'Autre équipement plein air')], default='play_structure', max_length=50, verbose_name="Type d'asset / Équipement")),
|
||||
('norm', models.CharField(blank=True, choices=[('EN_1176', "EN 1176 - Équipements & sols d'aires de jeux"), ('EN_16630', "EN 16630 - Modules de fitness d'extérieur"), ('EN_14974', 'EN 14974 - Équipements de skatepark'), ('EN_15312', 'EN 15312 - Équipements multisports en accès libre'), ('other', 'Autre norme / Non spécifié')], max_length=50, null=True, verbose_name='Norme de sécurité appliquée')),
|
||||
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle visuel/opérationnel')),
|
||||
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle requis')),
|
||||
('last_annual_inspection', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle annuel certifié')),
|
||||
('next_annual_inspection', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle annuel certifié')),
|
||||
('certifying_body', models.CharField(blank=True, max_length=255, null=True, verbose_name='Organisme de contrôle certifié')),
|
||||
('impact_surface_type', models.CharField(blank=True, max_length=150, null=True, verbose_name="Type de sol d'amortissement")),
|
||||
('critical_fall_height_m', models.FloatField(blank=True, null=True, verbose_name='Hauteur critique de chute (m)')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
||||
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
||||
('parent_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_assets', to='assets.outdoorasset', verbose_name='Asset parent')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.outdoorasset', verbose_name='Remplacé par')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='location_assets', to='assets.outdoorlocation', verbose_name='Localisation spécifique')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_outdoor_assets', to='assets.outdoorlocation', verbose_name='Site / Espace principal')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Asset / Équipement plein air',
|
||||
'verbose_name_plural': 'Assets / Équipements plein air',
|
||||
'ordering': ['site', 'code'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OutdoorLocationDocument',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description du document')),
|
||||
('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')),
|
||||
('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')),
|
||||
('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.outdoor.upload_to_outdoor, verbose_name='Fichier')),
|
||||
('document_type', models.CharField(default='other', max_length=50, verbose_name='Type de document')),
|
||||
('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.outdoorlocation', verbose_name='Espace extérieur')),
|
||||
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Document d'espace extérieur",
|
||||
'verbose_name_plural': "Documents d'espace extérieur",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
# 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',),
|
||||
),
|
||||
]
|
||||
74
loko/assets/migrations/0010_add_parking_thematic.py
Normal file
74
loko/assets/migrations/0010_add_parking_thematic.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
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),
|
||||
]
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# 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",
|
||||
},
|
||||
),
|
||||
]
|
||||
20
loko/assets/migrations/0014_alter_inspectiondocument_file.py
Normal file
20
loko/assets/migrations/0014_alter_inspectiondocument_file.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue