Compare commits

...

509 commits
v1.0.0 ... main

Author SHA1 Message Date
f19d09ce5f fix(controls): move new control button next to excel button in toolbar 2026-10-10 20:16:06 +02:00
f10377648a style(controls): darken table row separator borders for better readability 2026-10-10 20:02:17 +02:00
2d221c543c fix(interventions): cancel obsolete and >12m expired tree prescriptions 2026-10-10 16:06:14 +02:00
5ce7179c19 fix(assets): ensure asset geometry layers remain above orthophoto basemap on detail maps 2026-10-10 15:58:12 +02:00
34e4da64a8 feat(assets): add dedicated icon for green surfaces distinct from trees 2026-10-10 15:43:54 +02:00
3037241762 style(controls): increase categories min-width and align project icon with first word 2026-10-10 15:13:35 +02:00
fc8cdb579f refactor(controls): replace bootstrap-table with custom table and column selector 2026-10-10 15:06:26 +02:00
a6378eee90 feat(contracts): add column sorting by code and dates to contracts list 2026-10-10 14:47:28 +02:00
0732f89274 perf(contracts): optimize queries, add server-side pagination, search, thematic/contract filters and redesign UI 2026-10-10 14:39:40 +02:00
34e9743cc3 feat(nature): cancel obsolete tree interventions and compute theoretical next inspection
- Automatically cancel pending interventions whose deadline is earlier than the tree's last diagnostic
- Record cancellation reason in InterventionTimeLine
- Compute theoretical next inspection date as last_phytosanitary_date + pruning_frequency_years
- Add unit test coverage for obsolete intervention cancellation and theoretical inspection calculation
2026-10-10 14:29:57 +02:00
7761f22b77 style(ui): align table and filter modal styles in observations and inspections with assets 2026-10-10 14:16:59 +02:00
af411b2105 feat(nature): create timeline planning events and display due date on asset interventions
- Create InterventionPlanificationTimeLine events (expected_begin and expected_end)
  during Fulcrum tree interventions import and maintenance dates synchronization
- Display planned/completion date instead of creation_time in asset_interventions.html
- Add unit test assertions for planning timeline events
2026-10-10 14:09:15 +02:00
74ef7d3a34 feat: add maintenance dates synchronization and options to tree import command 2026-10-10 13:50:02 +02:00
9af4f99f17 fix(assets): rediriger le fil d'Ariane et retour carte des espaces verts vers la carte unifiée 2026-10-10 13:49:21 +02:00
e201a928b8 feat: add management command to spatially attach trees to green sections with test coverage 2026-10-10 13:37:37 +02:00
958d8dbc65 feat: add management command to import Fulcrum tree inventories and interventions 2026-10-10 12:17:57 +02:00
b953b1f984 feat(assets): add automatic calculation of tree ecological and patrimonial eco-services 2026-10-10 08:34:50 +02:00
e51f698030 feat(assets): allow editing general note field on asset details 2026-10-09 23:22:09 +02:00
15403b8604 fix(inspections): scope viewable inspections and assets by contract and municipality 2026-10-09 22:59:10 +02:00
61ce1d3a4d fix(mobile): enforce absolute positioning on map markers to prevent vertical layout drift 2026-10-09 22:53:42 +02:00
fbd1bc452f fix(mobile): resolve nature tree marker symbology and attribute serialization
- Serialize management_type, managing_authority, is_remarkable, is_dead, vitality, and botanical names in mobile_assets_search
- Allow regional (Bruxelles Mobilité), remarkable, and dead trees to display differentiated icons and colors on mobile map
- Fallback tree display name to vernacular/scientific name
- Update asset_symbology.js cachebuster version in mobile template
- Add unit test coverage for nature tree search serialization
2026-10-09 19:52:11 +02:00
45bc88d1a3 refactor(assets): remove specific naturegreensurface template in favor of detail_generic 2026-10-09 19:28:56 +02:00
b813de3984 fix: update English translation for Surface verte to Green surface 2026-10-09 19:16:19 +02:00
Karl Determe
da9e5c1cb8 feat: add ITS asset models and views for location and bollard management 2026-10-09 15:04:41 +02:00
Karl Determe
cd3f3999cb Merge branch 'main' of https://git.valogeo.be/Valogeo/loko 2026-10-09 14:33:49 +02:00
Karl Determe
3c8dcedccc fix: remove empty deleted file from repository 2026-10-09 14:33:38 +02:00
095161872d feat: add dead tree symbology, icon, and map layer support for nature assets 2026-10-09 14:31:17 +02:00
e65c402395 feat: add basemap preference persistence and layer switcher control support 2026-10-09 14:16:12 +02:00
9bdd68bea2 feat: add regional and remarkable nature asset symbology icons and matching logic 2026-10-09 14:04:08 +02:00
f271d65be2 fix(assets): dynamic nature management_type filter based on visible assets and scope nature endpoints by user accessible municipalities 2026-10-09 13:50:49 +02:00
587f4abf8a feat(nature): distinction arbres régionaux, filtre gestionnaire, tableau compact et essences botaniques 2026-10-09 13:32:46 +02:00
Karl Determe
4823d55f08 Merge branch 'main' of https://git.valogeo.be/Valogeo/loko 2026-10-09 13:17:59 +02:00
Karl Determe
cebe496617 feat: add seed command and fixture for Schuman bollards and CCTV cameras 2026-10-09 13:17:49 +02:00
8afa68e066 feat: add management type and authority fields with visualization and filtering for nature trees 2026-10-09 13:09:58 +02:00
Karl Determe
80c54a083d feat: add unified index, map layers, and ITS asset management views and symbology 2026-10-08 16:45:33 +02:00
Karl Determe
7d5718dec2 feat: add admin, models, views, and templates for ITS assets 2026-10-08 14:29:19 +02:00
bb8d6f98c0 style(home): unify quick action buttons size and center content 2026-10-07 16:08:05 +02:00
dd12e2fde1 feat(home): replace history shortcut with asset search in viewer action bar 2026-10-07 15:58:51 +02:00
875efc67f4 feat(home): add user_history widget for viewers and adjust quick action shortcuts 2026-10-07 15:54:57 +02:00
51e6680763 fix(interventions,contracts): enforce can_view_intervention on detail views and allow contract posts access for global viewers 2026-10-07 14:15:53 +02:00
ea02b12ea5 feat(stock): add product search by code and label in purchase order PDF import 2026-10-07 11:58:17 +02:00
b307791b90 feat(contracts): add global permissions to view contract posts and prices on UserConfig
- Add can_view_all_posts and can_view_all_prices boolean fields to UserConfig
- Add migration 0020 with automatic data migration for Legal Office users
- Update permissions helpers, UserConfigAdmin, copy_user_config and intervention detail view
- Add unit tests for global posts and prices permissions
2026-10-07 11:57:57 +02:00
9a85696d90 fix(stock): escapejs translated option text in batch location selects 2026-10-07 11:40:57 +02:00
499ffe824e refactor: redesign calendar notes and documents view with chronological sequencing blocks 2026-10-07 10:45:37 +02:00
2ef730508f feat(calendar): intégrer les notes et documents dans la modale d'intervention 2026-10-07 10:26:08 +02:00
Karl Determe
775151c72d feat: add notification services and observation views implementation 2026-10-06 16:22:23 +02:00
Karl Determe
e27a979a53 feat: add views for stock management and observations applications 2026-10-06 15:56:21 +02:00
Karl Determe
5825eac998 feat: add index, detail, and form HTML templates for management zones 2026-10-06 15:46:37 +02:00
Karl Determe
e87fe1531d feat: add management zone form template and corresponding unit tests 2026-10-06 15:23:50 +02:00
Karl Determe
0c1ae3163d feat: add observation detail template with interactive map, photos, and discussion features 2026-10-06 11:31:19 +02:00
Karl Determe
496b6d9c8b feat: implement observations module with views, models, UI templates, translations, and migrations 2026-10-06 10:04:54 +02:00
Karl Determe
58c1ccd531 feat: add observation management features with map integration, views, and migrations 2026-10-06 10:04:40 +02:00
384a2b29c3 fix(common): support list type_filter and fix translation function shadowing in assets preferences 2026-10-05 18:22:47 +02:00
288921b4aa fix(assets): declare mergedManagementZonesGeojson to prevent ReferenceError in unified view 2026-10-05 16:38:43 +02:00
3e35c98a46 git add loko/assets/static/assets/unified_index.js \
loko/assets/static/assets/unified_map_layers.js \
        loko/assets/templates/assets/unified_index.html \
        loko/assets/tests_management_zones.py \
        loko/assets/views/core.py \
        loko/assets/views/management_zones.py

git commit -m "feat(assets): integrate management zones into unified assets view

- Add management zones layers and table row support in unified index
- Replace element type radio filters with 3 checkboxes (assets, locations, management zones)
- Add 3rd card in unified add modal for management zone creation
- Include centroid coordinates, area, and unified properties in management zones GeoJSON
- Add comprehensive integration test suite"
2026-10-05 16:00:12 +02:00
b5b0338e09 feat(assets): add management zones CRUD interfaces and permissions
- Add can_edit_management_zones permission to UserThematics model and admin
- Implement ManagementZoneForm with interactive WGS84 polygon capture and SRID 3812 reprojection
- Add views and routes for listing, creating, editing, detailing, and deleting management zones
- Support document attachments (ManagementZoneDocument) and explicit asset linkages
- Add MapLibre polygon drawing tool in zone form and preview map in detail view
- Integrate management zones shortcuts in nature assets map overlay and index
- Add comprehensive unit tests for permissions, forms, and CRUD operations
2026-10-05 15:35:33 +02:00
d36649f3bf feat(assets): add generic ManagementZone model, admin, geojson API and map overlay 2026-10-05 15:12:16 +02:00
8807531eb3 feat(contracts): add contractual lot perimeters overlay on nature assets map 2026-10-05 14:35:31 +02:00
f81ca93590 feat: add geographic polygon and color fields to Contract model and admin 2026-10-05 14:19:07 +02:00
69f5e65495 feat(assets): add basemap and orthophoto switcher to nature location detail map
- Include LayerSwitcherControl on the nature location detail map.
- Support standard vector, aerial orthophotos, and regional basemaps from window.MAP_BACKGROUNDS.
- Ensure proper z-index layer ordering so nature polygons, trees, and equipments remain above raster basemaps.
- Update unit tests to verify layer switcher integration.
2026-10-05 14:04:02 +02:00
11e1674f73 feat: implement nature location detail view with asset summaries, KPIs, and unit tests 2026-10-05 13:44:18 +02:00
f324eb00bd feat(assets): color-code asset types by technical domain for enhanced visual scanning 2026-10-05 11:48:44 +02:00
10e9f1de50 feat(assets): display assets directly in building tree and add collapse/expand all controls 2026-10-05 11:25:55 +02:00
ab1cfb6ab2 fix(assets): remove duplicate closing div breaking page layout in publicbuilding_detail 2026-10-05 10:59:21 +02:00
6802d1e15f fix(assets): fix js coord syntax error and add interactive multi-floor plans viewer 2026-10-05 10:38:35 +02:00
512f6786e3 refactor: move team daily view helper functions and state variables to file scope 2026-10-05 09:56:25 +02:00
63c4a8e539 git add loko/assets/models/buildings.py \
loko/assets/forms.py \
        loko/assets/admin.py \
        loko/assets/templates/assets/buildings/ \
        loko/assets/tests.py \
        loko/assets/migrations/0028_buildingasset_contractor_name_and_more.py

git commit -m "feat(assets): extend building locations and technical equipment models

- Add comprehensive building locations (stairwells, technical rooms, boiler rooms, offices, etc.)
- Add building physical & technical fields (room numbers, floor types, heated status, dimensions)
- Add complete building envelope & technical equipment types (roofs, facades, windows, doors, boilers, heat pumps, etc.)
- Enrich admin, forms, trees, and detail templates with icons, macro-lots, and maintenance tracking
- Add migration 0028 and unit test coverage"
2026-10-05 09:49:08 +02:00
93f3a49983 fix(calendar): fix canEditThisTeam and isPastDay ReferenceError in renderTeamsDailyGrid 2026-10-05 09:48:53 +02:00
6ee0aa470b feat(calendar): enable drag and drop for team members in daily view
- Render team members as individual draggable chips in daily teams calendar view
- Decouple click (modal inspection) from drag and drop (team/date reassignment)
- Add multi-team conflict confirmation modal on drop
- Maintain permission checks and past date protection
2026-10-05 09:42:10 +02:00
03fa3adaf6 feat: suggest product default locations when parsing purchase orders from PDF 2026-10-05 09:24:22 +02:00
1d212194ad feat: add external destination badges and indigo styling to stock and production order templates 2026-10-05 09:09:43 +02:00
9764b525bb refactor: remove local OCR references and escape translated messages in purchase order scripts 2026-10-01 16:07:32 +02:00
4bb53fb71c feat(mobile): add quote photo/pdf OCR import interface in mobile stock
- Add mobile-optimized purchase order from quote view and template
- Support taking multiple page photos directly from smartphone camera
- Display responsive article cards with catalog product matching
- Provide sticky summary with live totals and confirmation CTA
- Link from mobile purchases list and add unit tests
2026-10-01 15:43:52 +02:00
53d579009a feat(stock): support multi-page photo uploads for quote OCR processing
- Allow uploading multiple quote photos at once via multi-file input
- Process photos sequentially with deskewing and OCR line clustering
- Aggregate articles and metadata across all pages seamlessly
- Attach all uploaded photos to the generated purchase order
- Add unit tests for multi-image quotes and multiple document attachments
2026-10-01 15:28:08 +02:00
4010dfb138 fix(stock): prevent merged quote items via auto-deskew, fixed baseline clustering and item splitting
- Add auto-deskewing for tilted smartphone photos and document scans
- Anchor line clustering to initial baseline to avoid vertical centroid drift
- Normalize OCR numerical artifacts (semicolon decimals, glued VAT percentages)
- Purge catalog URLs from product names to improve catalog matching
- Implement fused item splitter to rescue embedded articles from descriptions
- Add regression tests for item splitting and OCR normalization
2026-10-01 15:12:56 +02:00
32cb154815 feat(stock): support photo/scan quotes via local OCR and propose supplier creation
- Enable image uploads (.png, .jpg, .jpeg, .webp, .tiff) alongside PDFs
- Process photos and scans with local RapidOCR and adaptive bounding box clustering
- Propose new supplier creation with pre-filled fields when quote supplier is not found
- Retain manual catalog product association safeguard
- Add automated unit and integration tests
2026-10-01 14:46:09 +02:00
358fb23d7e refactor(stock): restrict PDF purchase order to existing products and loosen matching similarity
- Replace auto-creation of new products with strict association to existing stock items
- Add dimension normalization and token-based similarity matching (rapidfuzz/difflib)
- Provide ranked candidate suggestions in product dropdown during quote review
- Enforce validation preventing purchase order creation with unassociated products
2026-10-01 14:23:01 +02:00
391966da07 refactor: make PDF parser dependencies optional and lazy-load parser 2026-10-01 13:57:12 +02:00
de6828221b feat: add product validation workflow, supervisor notifications, and PDF purchase order parser 2026-10-01 13:51:27 +02:00
65624c343a style: improve photo carousel modal layout and responsiveness 2026-10-01 10:50:09 +02:00
3920648000 fix(interventions): assign controller only when intervention status changes to finished 2026-10-01 09:53:06 +02:00
8df84704dc fix(interventions): resolve UnboundLocalError contracts_qs for external managers 2026-10-01 09:29:54 +02:00
927c049d02 git add loko/contracts/ loko/interventions/
git commit -m "feat(interventions): configure team maintain types for control auto-assignment and remove structures limitation"
2026-09-30 19:20:59 +02:00
41679d82e9 perf(contracts): optimize ContractAdmin queries on add and change views
- Avoid loading all database ContractPost instances when adding a new contract
- Use select_related('contract') on ContractPost to prevent N+1 queries in __str__
- Only include ContractOperationTemplatePostInline on saved contracts
- Add prefetch_related('teams') for CompanyMember in pause reason assignments
- Optimize ContractAdmin changelist queryset with select_related and prefetch_related
2026-09-30 19:04:39 +02:00
6782959253 feat(home): filter controller shortcuts link by assigned controller and remove sub-counters
- Remove 'to_be_corrected' and 'validated' sub-counters from controller shortcuts widget
- Include assigned_controller parameter in widget URL using the new role-person filter
- Update ControllerShortcutsWidgetTests to assert URL parameter and absence of sub-counters
2026-09-30 18:55:56 +02:00
3d9e7c3202 feat(interventions): add combined filter for manager, controller and inspector with shortnames and teams 2026-09-30 18:49:19 +02:00
b86d805cb5 feat(contracts,home): include finished status in controller shortcuts and display assigned controller in order interventions
- Add 'finished' status to controller shortcuts widget filter and links
- Add conditional 'Contrôleur assigné' column in order interventions table for contracts with auto_assign_control_team
- Optimize order interventions query with select_related for controllers
- Update and compile translations (fr, nl, en)
- Add unit tests for order detail controller display and widget status inclusion
2026-09-30 18:17:30 +02:00
aa75bcbfeb feat(projects): upgrade filter UI to modern modal with badge counter and contextual status styles 2026-09-30 17:59:01 +02:00
244b16b3d3 feat(trafficlights): add cable geometry copy, duplication modal and node editing hints in cross plan import
- Add explanatory guidelines for cable node operations (double-click segment to insert node, double-click node to delete, drag to reposition)
- Add floating reminder pill on map during cable editing mode
- Add "Copier tracé..." dropdown on cable cards to import trace from other cables
- Add "Appliquer aux autres câbles..." modal to duplicate active trace across multiple cables at once
- Add direct map jump button on cable rows in comparative table
- Include db_coords in compared_cables context to preserve pre-existing geometries
- Add tests and update translations (fr, nl, en)
2026-09-30 14:39:32 +02:00
1d99cdd09b feat(assets): ignore auxiliary 'com' wiring and allow archive or delete in cross plan import
- Ignore communication and power supply rows ('com', 'comm', 'alim', etc.) in detector sections
- Add user-selectable 'archive' and 'delete' actions for all assets in cross plan preview
- Handle ProtectedError gracefully when deleting referenced assets
- Add unit tests for wire filtering and archive/delete integration actions
- Update and compile translations (fr, nl, en)
2026-09-30 11:51:55 +02:00
71154a0fb4 feat: standardize preparation order numbering format and add recodification command 2026-09-30 11:07:10 +02:00
dc6638c4a5 feat: add daily intervention visibility restrictions and mobile filters for technicians with tests 2026-09-30 09:47:45 +02:00
79154085ee fix(teams): record automatic leave date on team reassignment and reconcile history 2026-09-30 09:24:09 +02:00
a08ef0f035 feat: add order filtering and default descending sort for preparation and production orders 2026-09-30 08:46:06 +02:00
63a9a080de feat(stock): tri chronologique des bons et réouverture/historique des bons de confection
- Trie les bons de préparation (date) et de confection (created_at) par ordre chronologique FIFO
- Ajoute la réouverture a posteriori des bons de confection terminés avec régularisation automatique des stocks
- Ajoute le modèle ProductionOrderHistory et trace l'historique complet des modifications (statuts, composants, documents)
- Intègre le bouton de réouverture et la section d'historique dans les vues desktop et mobile
2026-09-29 22:38:34 +02:00
c25c4a4415 feat: add image zoom, pan, and reset controls to document viewer 2026-09-29 22:17:20 +02:00
b78c9da50e feat(stock): inclure les statuts 'à traiter' et 'en cours' dans le widget de raccourcis 2026-09-29 22:17:06 +02:00
Karl Determe
e00f9cf784 feat: add stock management module including views, models, templates, and tests 2026-09-29 17:07:29 +02:00
Karl Determe
001bee8ca9 feat: add stock batch views, templates, and comprehensive test suite 2026-09-29 15:20:03 +02:00
Karl Determe
25a64b824a feat: add mobile views, stock management views, and preparation order templates 2026-09-29 13:55:26 +02:00
Karl Determe
f429838a39 feat: add stock management module, common models, and mobile views 2026-09-29 13:30:42 +02:00
Karl Determe
ca4d1b3c1b feat: add views and templates for stock batch creation on desktop and mobile 2026-09-29 12:42:27 +02:00
8b13236a26 fix(interventions,mobile): allow multi-thematic visibility for contractor teams and prevent persistent search query 2026-09-28 21:01:59 +02:00
de73de7328 feat(interventions,stock): sync preparation orders and material status on intervention cancellation
- Fix preparation status calculation by ignoring cancelled orders, preventing false green 'stock ready' icon
- Update item prepared state when preparation order is marked as processed
- Automatically cancel unprepared preparation orders when intervention is canceled
- Send alert notification to stock managers when canceled intervention has already prepared orders
- Add alert banner with direct restock/cancellation action on preparation order detail views
- Prevent editing material switches or creating orders for canceled interventions
- Add and compile translations for FR, EN and NL
- Add comprehensive automated unit tests
2026-09-28 20:01:39 +02:00
c00212f79e fix(interventions): translate modal status and apply assigned color on interventions list and map 2026-09-28 16:26:44 +02:00
37c1016b57 feat(interventions): distinguer les interventions 'à traiter' déjà assignées en rose (#d81b60)
- Ajout de la couleur 'to_be_processed_assigned' (#d81b60) dans STATUS_COLORS (Python & JS).
- Ajout de la propriété is_assigned sur le modèle Intervention.
- Prise en compte de la couleur dans les vues calendrier (membres, timeline, backlog, équipes) et sur la carte MapLibre du modal calendrier.
- Enrichissement des popups carte et des badges du backlog avec l'équipe/membre assigné.
- Ajout de tests unitaires validant la couleur et la sérialisation is_assigned.
2026-09-28 14:45:17 +02:00
0bbe267498 feat(contracts): auto-sync team schedule daily and on mobile access
- Register sync_teams_schedule in init_scheduled_tasks (daily at 04:00 local)
- Trigger sync_member_current_teams on mobile intervention list load
- Trigger sync_member_current_teams on user login signal
2026-09-28 14:17:44 +02:00
f4460a5323 feat: add edit note functionality and UI action buttons 2026-09-25 23:36:04 +02:00
205a05cb5b feat(interventions): aligner les permissions des notes par statut et historiser le mobile
- Autoriser les modifications/suppressions de notes jusqu'au statut 'traité' pour les externes
- Autoriser les modifications/suppressions de notes jusqu'au statut 'validé' pour les internes
- Supprimer la contrainte horaire de 1h sur la modification des notes
- Brancher l'historisation de la chronologie sur la création et modification de notes mobile
- Tracer l'upload des photos de formulaires mobiles dans la chronologie
- Ajouter les tests unitaires et d'intégration correspondants
2026-09-25 23:20:09 +02:00
7911369740 feat: restrict intervention timeline history and filter controls to internal users 2026-09-25 22:53:32 +02:00
2df4621e5b feat(interventions): afficher les statuts de la chronologie sous forme de cercles numérotés
- Reprend le code visuel du stepper supérieur pour les transitions de statut dans l'historique
- Affiche une boule colorée avec le numéro canonique (1 à 15) et le libellé sous chaque statut
- Aligne la flèche de transition au centre des cercles
- Ajuste les contrastes de texte (noir/blanc) selon la luminosité du statut
2026-09-25 22:35:29 +02:00
fd794737a8 style(interventions): standardize timeline status badge widths and use distinct purple for status event
- Assign fixed 115px width to all status badges for vertical alignment of transitions
- Unify left event badge width to 85px for a clean tabular layout
- Use purple (#6f42c1) for the 'Statut' badge and node icon to avoid visual collision with status colors
- Update timeline unit tests accordingly
2026-09-25 22:24:05 +02:00
f510a086bd feat(interventions): trace notes, documents and element links in full timeline
- Add 'note', 'document', and 'link' event types to InterventionTimeLine
- Record timeline events for note additions/edits/deletions (standalone and batch)
- Record document uploads and deletions (desktop and mobile APIs)
- Record asset linking/unlinking with friendly names and linked interventions
- Display detailed items exclusively in the toggleable "Historique complet" view
- Maintain shortname display for all event authors
- Update and compile FR, EN and NL translation catalogs
- Add comprehensive test coverage for all new timeline event records
2026-09-25 22:05:22 +02:00
79af86fb7f feat(interventions): amélioration de la chronologie et traçabilité complète de l'historique
- Ajout des types d'événements 'assignment' et 'modification' sur InterventionTimeLine
- Enregistrement des attributs modifiés (field_changes JSON) et des descriptions d'événements
- Création du service InterventionChangeTracker pour historiser les changements de champs et d'assignations
- Refonte visuelle de card_chronologie avec vue par défaut (statuts + assignations) et bascule vers l'historique complet
- Préservation des shortnames pour tous les auteurs des modifications
- Ajout des traductions i18n et de tests unitaires dédiés
2026-09-25 21:29:42 +02:00
71cb8369a0 feat(interventions): improve non-applicable button symbology in operations 2026-09-25 16:14:13 +02:00
ce924d7af4 fix(interventions): import Contract and ContractPost in admin to fix OperationTemplate change view 2026-09-25 14:40:38 +02:00
c8349a3171 feat: add continuous GPS tracking, pulsing location marker, and distance calculation to inspection maps 2026-09-25 14:23:51 +02:00
97ecd8d9fb fix(inspections): prevent modal event bubbling and add bottom add-ouvrage button in GIEP 2026-09-25 11:47:57 +02:00
ca8d8187c1 feat: hide standard inspection sections for GIEP-specific inspections 2026-09-25 11:34:04 +02:00
b30e65e0a1 i18n: remplacer remarques initiales par remarques globales et mettre à jour les traductions 2026-09-25 11:14:16 +02:00
c6ef935e0b feat(inspections): restructurer le formulaire d'inspection GIEP par ouvrage
- Isoler la météo au niveau de la zone GIEP globale
- Structurer chaque ouvrage : Conformité, Photos dédiées, Formulaire technique, Remarques
- Lier les photos aux ouvrages via field_key et auto-consolider statut et remarques
- Enrichir l'affichage de consultation et ajouter la suite de tests unitaires
2026-09-25 10:03:27 +02:00
5e9fced68f feat: add 7j view mode to team planning 2026-09-25 09:27:32 +02:00
40ce03d812 feat: add rolling horizons and aggregated column support to team planning view modes 2026-09-25 09:18:40 +02:00
Karl Determe
096886a194 feat: add Django admin configuration, team services, and test planning for contracts 2026-09-24 11:33:53 +02:00
Karl Determe
f19eae6067 Merge branch 'main' of https://git.valogeo.be/Valogeo/loko 2026-09-24 11:04:00 +02:00
Karl Determe
0231f2ac00 feat: add Django admin configurations and tests for common app 2026-09-24 11:03:47 +02:00
6b526f7056 fix(inspections): fix mobile zone filter and complete mobile translations in lists and maps 2026-09-23 16:57:41 +02:00
61ef7fc3ea fix(trafficlights): correct cyclopieton 'bF' detection and preserve STIB detectors by default
- Recognize combined 'bF' phases as cyclo-piétons with appropriate 2V200 cyclo-piéto model
- Improve lantern match scoring to pair LANxx_bF with existing LANxx lanterns on multi-lantern poles
- Normalize lantern model matching between Excel deduction and DB names
- Propose 'keep' by default for STIB tram detector loops ('dT') absent from the cross plan
- Add unit tests for cyclo-piéton deduction, lantern matching and STIB detector preservation
2026-09-22 21:59:05 +02:00
c7c76f28dc fix: add legacy file fallback and use atomic transactions for document creation 2026-09-22 19:44:35 +02:00
55cad6d6cc refactor: increase map marker badge size, font size, and update zoom layout parameters 2026-09-21 19:12:50 +02:00
c726d8f37c style(home): refine activity symbol sizes and prevent marker overlap with collision detection 2026-09-21 18:50:15 +02:00
f534758f91 fix(home): safe status choices lookup and thematic localized fields in activity_geojson 2026-09-21 18:39:09 +02:00
1b93f3ae88 fix: fix try-catch block in home_index.js and update homewidget choices 2026-09-21 16:39:49 +02:00
de1d1c3b43 perf: optimize activity_geojson query, add caching and parallelize map data fetching 2026-09-21 16:14:56 +02:00
f0fb0df8ec feat: add operation type icons to map activity points 2026-09-21 15:34:39 +02:00
ef40e48163 feat(assets): add create_structure_folders management command 2026-09-21 15:14:26 +02:00
9726e8b618 refactor: move build metadata arguments to end of Dockerfile to improve cache efficiency 2026-09-21 14:50:18 +02:00
f23107e749 feat: add metro structure types and filtering UI to exceptional transport widget 2026-09-21 14:42:23 +02:00
0fa237564c refactor: remove georeferenced plans modal and custom map control from traffic lights import cross plan template 2026-09-21 11:59:11 +02:00
e30faa6ad6 refactor: rename dxfFiles to availableDxfFiles and add fallback to window scope 2026-09-21 11:44:09 +02:00
090c7dd03c refactor: integrate plans and calques into LayerSwitcherControl and remove custom plans control 2026-09-21 11:38:52 +02:00
3f402e3d6a feat: add UI indicators and handling for modified pole and cable geometries in traffic lights import 2026-09-21 11:29:06 +02:00
28d6664fbd feat: handle common wires and intelligent lantern matching in cross plan parser 2026-09-21 10:56:49 +02:00
91e0f6d5dd fix(common): ensure admin and whole-territory users use default Territory directly 2026-09-21 10:23:18 +02:00
e6a09eaf37 feat: add Territory model for default map boundary, and separate daily report quick action to second row 2026-09-21 10:04:39 +02:00
63c888fdbd feat: add territory mapping layers, dashboard widgets, and asset import management commands 2026-09-20 23:01:52 +02:00
cc0facdf92 refactor: migrate user map bounds caching from session to Django cache with signal-based invalidation 2026-09-19 18:40:37 +02:00
73738b6757 feat: support multi-commune contracts, base asset categories, and filtered municipality access 2026-09-19 18:33:39 +02:00
e68e6ccefe feat: optimize context processing 2026-09-19 18:33:17 +02:00
d73a9e482d feat: support DXF-based pole alignment in traffic light imports 2026-09-19 15:53:52 +02:00
6e4a304930 fix(teams): eliminate double scrollbar and white void in planning, highlight week/month scope controls 2026-09-19 14:42:13 +02:00
74c1bf2447 feat: add intervention control team assignment, contract auto-assignment, and controller shortcuts dashboard widget 2026-09-19 14:25:52 +02:00
1ab3c71754 fix: safely resolve AppView urls and purge obsolete schools AppView 2026-09-19 13:54:49 +02:00
63bed7737d fix(docker): remove unversioned exports copy from Dockerfile 2026-09-19 12:25:09 +02:00
c20a8c4142 fix(docker): copy inspections and exports apps in Dockerfile 2026-09-19 12:24:49 +02:00
a0ffebc489 feat: add monthly team planning scopes and operation types to company teams 2026-09-19 12:07:53 +02:00
4d0682efc1 feat(api): add /api/version/ endpoint for Fleet Management and Docker build metadata 2026-09-19 10:47:36 +02:00
aad07929b9 fix(thematics, home): mise à jour des thématiques (Sécurité, Stationnement, Propreté) et sécurisation du chargement de la carte sur l'accueil 2026-09-18 19:22:59 +02:00
bbaa092550 fix(inspections): corriger lookup sub_type dans migration 0013 sur base vierge 2026-09-18 18:55:36 +02:00
f2fbe7e394 feat: promote existing base inspections to HotBoxInspection instances and add pending visual checks UI 2026-09-18 16:14:04 +02:00
a52f205e42 feat: add not_checked option to water drainage in hotbox inspections 2026-09-18 15:39:36 +02:00
e1cf8d5565 fix: restore missing closing brace in updateEditHotBoxRecommendation 2026-09-18 14:20:55 +02:00
3bec7cfd7c fix: remove redundant badge element lookup in edit inspection modal 2026-09-18 14:12:21 +02:00
ecb3bbddd1 feat: require all four hotbox inspection questions to be answered and update validation logic 2026-09-18 13:46:53 +02:00
010466e26f feat: add HotBoxInspection model, views, templates, and admin configuration 2026-09-18 12:02:05 +02:00
250a2b4f2f feat: add related inspections to intervention detail views and templates 2026-09-18 10:44:58 +02:00
6c80028906 feat: add concerned categories and target asset model to intervention subtypes for dynamic inspection targeting 2026-09-17 17:07:51 +02:00
368a167b9c feat: add support for dynamic intervention subtype filtering and validation 2026-09-17 16:27:33 +02:00
396d26414c refactor: support multiple intervention types in subtypes and update non-compliance notifications for company members 2026-09-17 16:05:36 +02:00
7ca27197bf feat: add custom admin assets to dynamically toggle approval_except_urgent field visibility 2026-09-17 15:41:57 +02:00
8fc2798c52 feat: add approval exception for urgent contract interventions and update status transition permissions 2026-09-17 14:35:53 +02:00
Karl Determe
676e781943 feat: add inspections application and update interventions module with new models, views, and migrations 2026-09-17 08:55:47 +02:00
Karl Determe
836b5c8edb feat: add custom notification recipient fields and helpers to Symptom model 2026-09-15 16:10:31 +02:00
9ae7b0631b feat: add robust panel type image normalization, text fallback rendering, and corresponding styles 2026-09-14 16:37:34 +02:00
dcf5477199 feat: add inventory validation notifications with single-claim auto-archiving 2026-09-14 15:54:29 +02:00
5ebcc1e3c9 i18n: remove obsolete PREGO translations and update PMO terms in locale files 2026-09-14 15:43:45 +02:00
d2880eb3b1 fix(inspections): remove duplicate editModalEl declaration in edit modal 2026-09-14 15:03:20 +02:00
bfebdcf3dd feat: make inspection result status mandatory and improve GIEP form scoping 2026-09-14 14:49:34 +02:00
fe535683a1 feat: implement automated status calculation and unit tests for GIEP inspections 2026-09-14 12:15:48 +02:00
660c40f0f7 feat: improve intervention visibility rules to correctly handle active and replanned interventions 2026-09-14 11:33:24 +02:00
9522eed8f8 style: improve typography and contrast for member names in calendar UI 2026-09-14 10:19:44 +02:00
099043ea8b feat: add unified map assemblies and dynamic zoom scaling for mobile assets 2026-09-14 10:05:18 +02:00
3f75b511f2 refactor: align mobile sign and pole marker styles and grouping logic with desktop view 2026-09-13 19:26:06 +02:00
534488814a refactor: improve sign panel marker styling, vertical stacking, and alignment logic on mobile map 2026-09-13 19:02:59 +02:00
641ac314e5 feat: add sign pole map marker styling and conditional rendering for mobile assets 2026-09-13 15:35:02 +02:00
e2be198802 feat: add sign panel map markers, count badges, and asset grouping logic 2026-09-13 15:27:32 +02:00
57976c71eb feat: add signpanel search support, symbology, and coordinate fallback in mobile asset views 2026-09-13 15:01:23 +02:00
bd8897499b feat: add multi-asset bottom sheet and selection UI for overlapping map markers 2026-09-13 14:30:58 +02:00
d33b49647b feat: add vector layers and GeoJSON support for polygons and lines on mobile map 2026-09-13 14:16:57 +02:00
35a20a2936 refactor: extract asset symbology into a dedicated module and update map layers and templates 2026-09-13 14:02:41 +02:00
2ada4f88b7 feat: enhance mobile asset map UI with floating alerts and updated limit handling 2026-09-13 12:33:14 +02:00
4b1bc59f1e feat: add initial view button and no-results zone alert for mobile assets map 2026-09-13 12:13:41 +02:00
27ab3a8c1b feat: allow technicians and observers to access mobile home via query parameter and session state 2026-09-13 11:31:32 +02:00
6d2de3a7c8 feat: add mobile asset views and templates with localization updates 2026-09-13 09:24:55 +02:00
429eb27e2d chore(i18n): update source file paths and creation dates in locale files 2026-09-12 14:50:26 +02:00
2917a38942 refactor: relocate documents and linked assets to sidebar and update card styling in asset detail views 2026-09-12 14:36:55 +02:00
894edc72fe feat: add specific fields, templates, and migration for nature trees 2026-09-12 14:23:50 +02:00
c8e8223ffa fix: prevent future history corruption and handle duplicate entries in company member records 2026-09-12 12:08:23 +02:00
db70f8fba9 fix(sign): filter only active panels in orphan check command 2026-09-12 08:27:29 +02:00
025ddd09fc feat: add duplicate sign panel suppression, inactive status filtering, and management command 2026-09-12 08:11:24 +02:00
82e20f648e feat: enhance panel type filter with group selection, checkboxes, and bulk management controls. 2026-09-11 18:18:18 +02:00
1b38a8059e feat: extend support infrastructure for sign panels with traffic light pole association and custom model fields 2026-09-11 18:08:20 +02:00
48d7796e45 refactor: automate street synchronization and spatial inheritance for sign poles and panels 2026-09-11 15:57:54 +02:00
2c207fcfbb fix(calendar): stack member history modal on top of team daily modal 2026-09-11 14:15:59 +02:00
41898f211f fix(calendar): reactive update of daily team members in calendar grid 2026-09-11 14:06:09 +02:00
fdc315788f fix(calendar): fix team daily members URL generation to avoid duplicate members path 2026-09-11 13:45:42 +02:00
d45feb9f6e feat: implement team daily members AJAX endpoint and add support for managing team composition and member history in calendar views 2026-09-11 13:31:34 +02:00
261ce2dd14 chore: update localization files and templates for stock and intervention modules 2026-09-11 11:37:51 +02:00
fd10b3f8f7 feat: implement per-order toggle-all material status and real-time usage statistics UI 2026-09-11 11:00:46 +02:00
26c07a769f feat: add usage tracking for material items in preparation orders and include status in reporting 2026-09-11 10:48:36 +02:00
4424c2d9c4 refactor: implement strict map layer stacking order for intersection and cross plan visualizations 2026-09-09 23:08:36 +02:00
1e7f8123de fix: resolve VisibilityScope choices attribute error in project document upload 2026-09-09 22:59:09 +02:00
91d7e0b176 feat: enhance notification flag badges with custom styling and expanded metadata 2026-09-09 22:40:25 +02:00
c79d9e6f85 feat: render system messages with HTML content and update UI styles for orange flag buttons in notifications 2026-09-09 22:28:22 +02:00
ae540acacc refactor: replace emoji status flags with Bootstrap icons and enable HTML rendering for system messages in discussions. 2026-09-09 17:30:32 +02:00
9657111a1c feat: implement discussion flagging system with status filtering and UI updates 2026-09-09 17:09:49 +02:00
79697f392c feat: enable multi-team support for company members via updated history service and synchronization logic 2026-09-09 15:14:25 +02:00
5856acf5ed fix: restrict future team member display to explicitly scheduled join dates in calendar view 2026-09-09 14:14:35 +02:00
5bf606315e refactor: display team members in calendar regardless of date while restricting empty team indicator to past days 2026-09-09 13:59:45 +02:00
89794fa294 feat: implement can_tag permission for team mentions and restrict search visibility based on access control 2026-09-09 13:47:32 +02:00
19810d101c feat: add @mention autocomplete UI in discussion replies and implement server-side team mention handling 2026-09-09 11:53:48 +02:00
d5f4d8a765 fix: include past planned interventions in calendar view by updating schedule visibility logic 2026-09-09 11:16:35 +02:00
d9f1970ca6 refactor: improve inspection document UI with file support, lazy loading, and translation fixes. 2026-09-09 10:41:48 +02:00
Karl Determe
1c7e369fe3 feat: add external reference fields and implement migration script for asset data synchronization 2026-09-08 17:04:34 +02:00
Karl Determe
5417ca32b0 feat: add inspection management system including models, views, API endpoints, and front-end modal components 2026-09-08 16:57:39 +02:00
Karl Determe
64e93d7ae4 feat: implement authorization request module including forms, templates, and view logic 2026-09-08 16:06:25 +02:00
787799cf11 refactor: update GIEP inspection logic to use camera inspections, invert defect coloring for specific questions, and update related translations. 2026-09-07 14:02:28 +02:00
5eeda20ee6 fix: prevent round markers from appearing on polygon and line assets by checking geometry type 2026-09-07 12:00:30 +02:00
584df99605 feat: enhance NatureRWIAsset spatial filtering, location handling, and inspection map data binding 2026-09-07 11:48:55 +02:00
75dd0bd50e refactor: move output file resolution to model method with log-based fallback and added unit tests 2026-09-07 11:17:00 +02:00
135dfb41e6 fix(webpush): support Django lazy translation objects in push payload JSON serialization 2026-09-04 19:49:12 +02:00
8385a9e3e2 fix(webpush): conform RFC8292 headers, Crypto-Key fallback and exact delivery feedback 2026-09-04 19:30:31 +02:00
5757b28907 fix(webpush): robust pure cryptography RFC8291 encryption and async test dispatch 2026-09-04 12:09:53 +02:00
589c84b203 fix(notifications): escape translation strings in notification_settings JS 2026-09-04 12:02:30 +02:00
0ac214f459 feat: implement automatic duplication from Friday to following Monday in work week view and update associated tests 2026-09-04 11:53:28 +02:00
300feca9f1 feat: implement VAPID-based web push notifications with subscription management and auto-cleanup logic 2026-09-04 11:44:40 +02:00
23936a5767 refactor: restrict is_blocked_due_to_contract flag to specific intervention statuses and sync via model save method 2026-09-04 11:15:15 +02:00
3a241e3d03 refactor: restrict contract blocked warning to specific intervention statuses and add test coverage 2026-09-04 11:06:31 +02:00
7f257a2ca3 feat: add viewer role permissions for projects, observations, and inspections access 2026-09-04 10:44:40 +02:00
9e2b2a83e6 fix: wait for snap chromium IPC client to finish handshake using subprocess.run 2026-09-04 10:07:49 +02:00
74526cf7a6 feat: add --reset-passwords and improve existing user candidate matching 2026-09-04 10:01:37 +02:00
fe0e04b3ba fix: send deeplink directly to Chromium session without ignoring CLI launch parameters 2026-09-04 09:46:31 +02:00
4bb57962dc fix: use %20 encoding instead of + for Outlook deeplink body and add --only option 2026-09-04 09:41:04 +02:00
a1f8b4412e feat: target running Outlook PWA instance directly via app-id shortcut 2026-09-04 09:36:27 +02:00
946cbcae91 feat: add bulk user creation management command with configuration cloning and email/Outlook draft generation 2026-09-04 09:19:41 +02:00
Karl Determe
74679289a4 feat: add calendar view for interventions with user preference persistence 2026-09-03 17:05:20 +02:00
d131fcb0d6 fix: initialize missing member history and handle bidirectional m2m team updates in signals 2026-09-02 17:38:26 +02:00
9702a19db8 feat: implement member absence tracking and visual styling in calendar interventions 2026-09-02 14:28:12 +02:00
61a099eb4f feat: enhance interventions calendar UI with team color indicators, sticky column styling, and improved grid layouts 2026-09-02 14:12:32 +02:00
fa92404ae7 fix: exclude realized events from direct intervention query in calendar view 2026-09-02 14:03:58 +02:00
0a745be17c refactor: optimize intervention assignment logic and enforce strict deduplication by ID 2026-09-02 12:40:59 +02:00
32053f3635 feat: filter copied absences by allowed team IDs in services_teams.py 2026-09-02 12:28:23 +02:00
fad90d91c9 feat: add team synchronization service, management command, and improve calendar header UI 2026-09-02 12:20:42 +02:00
da7de1ac7d chore: update internationalization translation files and model reference locations 2026-09-02 12:09:55 +02:00
28696e7339 refactor: optimize member history scheduling logic and implement localStorage persistence for team planning UI state 2026-09-02 11:59:12 +02:00
efdafa43de fix: allow direct deletion of future team assignments and remove redundant confirmation dialog 2026-09-02 11:47:21 +02:00
71b445797d fix: restrict future planning visibility to explicit assignments and update team index UI styles. 2026-09-02 11:38:51 +02:00
11c81c8e00 fix(teams-planning): immediate day J assignment, sticky headers, past days greyed out and team distinction 2026-09-02 11:29:29 +02:00
7c8360bfc1 fix(teams): escape quotes and apostrophes in JavaScript translation tags 2026-09-02 11:10:23 +02:00
faafa8ec23 feat: add CompanyMemberAbsence model and integrate absence display into the team planning calendar view 2026-09-02 11:05:16 +02:00
9429402292 fix: reset is_blocked_due_to_contract flag when intervention is assigned to an unblocked contract 2026-09-02 09:56:07 +02:00
4824415290 feat: introduce contract blocking mechanism to suspend interventions and notifications for external users 2026-09-02 09:44:34 +02:00
Karl Determe
da038b7c4f feat: implement mobile-optimized intervention list view with user-specific preference persistence and associated tests 2026-09-01 15:45:28 +02:00
Karl Determe
fecd7430ad feat: add preparation order management features and mobile intervention detail view 2026-09-01 14:43:52 +02:00
Karl Determe
ee90cf3173 feat: implement document management service with recursive versioning and folder navigation logic 2026-09-01 12:58:09 +02:00
Karl Determe
913c00e34b feat: add English, French, and Dutch localization files for stock module 2026-09-01 11:53:11 +02:00
7e80c5b055 fix: delete inactive stock alerts to prevent uniqueness constraint violations during updates 2026-08-31 16:29:10 +02:00
2a6066381e feat: integrate global document viewer modal and update note UI for interventions 2026-08-31 14:29:45 +02:00
2d5166e3f3 style: update NoteType badge classes to use secondary text color 2026-08-31 14:17:31 +02:00
e596be66a2 style: update note type badge color schemes to improve visual categorization 2026-08-31 14:13:56 +02:00
9181c1ab23 style: update intervention note badges to transparent theme and change document sequencing badge to danger style 2026-08-31 14:09:25 +02:00
22efb9a6d9 style: update intervention badges to use subtle variants for better visual consistency 2026-08-31 14:05:36 +02:00
fd2c696780 refactor: consolidate notes and documents into a shared mobile interface component 2026-08-31 13:56:55 +02:00
022217218f feat: introduce 15-minute tolerance for intervention document sequencing and photo validation logic 2026-08-31 13:39:52 +02:00
14ab4d15df refactor: update document sequencing logic and refresh internationalization message catalogs 2026-08-31 13:32:10 +02:00
5f8a771277 feat: introduce document sequencing service to categorize intervention files into temporal blocks and add unit tests 2026-08-31 13:12:42 +02:00
5e2fb023c9 feat: implement dynamic location chips count and interactive date filter timeline preview 2026-08-31 11:55:08 +02:00
5c042531e5 fix: use latest finished status timeline date as effective end date for intervention filtering and summary views 2026-08-31 11:17:42 +02:00
32eec7b442 feat: implement AJAX-based folder autocomplete for Select2 widgets and add corresponding view and tests 2026-08-29 19:41:47 +02:00
15b058e246 feat: add bulk user selection for sharing, implement hierarchical path display, and standardize user naming conventions. 2026-08-29 19:33:02 +02:00
a098a66204 feat: implement document sharing authorization and visibility settings updates 2026-08-29 19:20:15 +02:00
22d0ad9928 feat: implement document and folder visibility levels with thematic filtering support 2026-08-29 18:52:31 +02:00
577fb1232b feat: implement color-based sign verification and add ground truth management support 2026-08-29 16:22:07 +02:00
a364b6161d feat: add thematic code input to location form and add null checks for modal elements 2026-08-28 16:15:53 +02:00
1df0c1b8c1 feat: track status changes in InterventionTimeLine when generating contract documents 2026-08-28 16:05:52 +02:00
ce79d913ea refactor: standardize inspection result status badge styling and update CSS filter components 2026-08-28 14:39:25 +02:00
0fc77df306 feat: implement desktop view of inspection list view with filtering, export capabilities, and map integration 2026-08-28 14:29:42 +02:00
96492a9726 feat: add dynamic signpanel type column to asset export and optimize query performance 2026-08-28 11:09:12 +02:00
33ef94902b refactor: replace ContentType lookup with robust filtering and validation across asset and intervention views 2026-08-28 10:57:41 +02:00
27056b61fa feat: add cancel/restore functionality for controls with granular permissions and API support 2026-08-28 10:45:17 +02:00
10942c81af chore: update Dutch and English translations for unprocessed and overdue intervention labels 2026-08-28 10:13:39 +02:00
Karl Determe
4a2aaf9fd0 feat: implement control management views, offline support, and associated unit tests 2026-08-27 16:40:48 +02:00
Karl Determe
6b8978254b feat: add unit tests and views for asset editing and export functionality 2026-08-27 16:06:34 +02:00
0b190526f8 feat: add external_reference field to assets with permission support, improve intervention contract assignment logic, and sanitize template strings. 2026-08-26 16:52:09 +02:00
Karl Determe
4135c6ef47 feat: implement traffic light asset management models, views, and cross-plan parsing utilities 2026-08-26 09:33:40 +02:00
Karl Determe
ba268f5e2a feat: implement traffic light radar support including models, views, templates, and tests 2026-08-25 16:02:51 +02:00
Karl Determe
6fe9908a11 feat: introduce modular intervention workflow view components and logic for status management 2026-08-25 15:03:20 +02:00
Karl Determe
af949b8b3d feat: add UI template for importing and comparing traffic light cross plans from Excel files 2026-08-25 14:03:30 +02:00
Karl Determe
ac75712828 feat: add traffic light cross-plan import functionality with file upload and data preview/integration support 2026-08-25 13:25:37 +02:00
Karl Determe
442469f3e0 Support cross plan cable headers in any column, dynamic strands, and radar codes 2026-08-25 12:30:26 +02:00
Karl Determe
09e684a07c Ensure cross_plans directory exists before saving JSON in import_cross_plan 2026-08-25 12:23:46 +02:00
Karl Determe
3ede92b24f Fix missing endif for unifiedExportModal in unified_index.html 2026-08-25 12:13:39 +02:00
Karl Determe
682c2bdfcf Fix Django template syntax and set col-map margin-top to 50px 2026-08-25 12:11:24 +02:00
Karl Determe
d82dcde16d feat: implement unified asset index view with dynamic filtering modal and associated JavaScript logic 2026-08-25 12:07:16 +02:00
Karl Determe
2b8cdfabb7 feat: add unified index template and script for map-based asset and location filtering 2026-08-25 12:03:40 +02:00
Karl Determe
0b3d00fdf5 feat: introduce unified asset and location management interface and logic 2026-08-25 11:38:03 +02:00
Karl Determe
97edbde803 feat: implement unified assets and locations management system via new index page and associated scripts 2026-08-25 11:35:45 +02:00
Karl Determe
e6bc68ee42 feat: add index templates for observations, assets, controls, and interventions with custom filtering UI 2026-08-25 11:27:40 +02:00
Karl Determe
be57578aa1 feat: add template for public space occupation authorization request form 2026-08-25 10:40:12 +02:00
Karl Determe
2a78cfc27b feat: create initial default HTML template for new public space occupation authorization requests 2026-08-25 10:11:45 +02:00
Karl Determe
cb9e8a1c8b feat: create new base template for public occupation authorization requests 2026-08-25 10:08:36 +02:00
c90ee9ece5 style: add status-on_pause badge styling and simplify calendar marker rendering to solid colors 2026-08-24 19:00:19 +02:00
7d3362d770 fix: synchronize intervention status history by including all status events and ensuring the final state matches current intervention status. 2026-08-24 18:46:27 +02:00
a21aa59fa9 feat: enable aggregated tooltips for overlapping timeline milestones and update UI styling 2026-08-24 18:40:34 +02:00
04a118d04d feat: add status history and milestone visualization with interactive tooltips to the intervention timeline 2026-08-24 18:32:40 +02:00
a50bebe427 feat: implement interactive dashboard and Excel export for pending interventions and observations 2026-08-24 16:37:37 +02:00
cfdcc41929 feat: enhance pending analysis report with thematic cross-tabulation and TCD-ready data structures 2026-08-24 16:17:11 +02:00
f72fe1bfd7 feat: add management command and service for analyzing and exporting pending interventions and observations 2026-08-24 15:15:17 +02:00
22da9400f3 feat: prevent redundant notification emails when providers update intervention status 2026-08-24 10:58:04 +02:00
d138ee98c4 feat: implement interactive mini-map and fullscreen positioning mode for precise sign placement 2026-08-23 12:10:29 +02:00
d931827c84 feat: implement traffic sign classifier with ONNX model support and updated OCR detection logic 2026-08-23 10:28:22 +02:00
814a04f1ac feat: implement dynamic sign panel rendering with compliance status and icon support on inspection maps 2026-08-22 18:08:12 +02:00
e561a77fc0 feat: integrate YOLOv10n model training pipeline with dataset and environment configuration 2026-08-22 14:56:20 +02:00
f98878f9c4 fix: update default queryset to return none when no filters are applied 2026-08-21 17:31:53 +02:00
0551e012f4 refactor: remove sidebar template logic and update document listing to default to root-level folders 2026-08-21 17:24:27 +02:00
ce48b99a45 refactor: optimize document navigation, add model database indexes, and improve as-built document import logic 2026-08-21 17:17:30 +02:00
5fd6ded07f feat: update import_as_built_documents command to support recursive directory structures and add a --clean-existing option. 2026-08-21 15:53:40 +02:00
04eb087cf1 feat: add ZIP archive support and expand structure directory matching to include location codes 2026-08-21 15:14:15 +02:00
0ab5fee09b feat: add rclone support to import command and implement duplicate document skipping during sync 2026-08-21 15:04:08 +02:00
dda68cabec feat: add As-Built subfolder to structures and implement import_as_built_documents management command 2026-08-21 14:51:13 +02:00
fc7112bd98 refactor: use template variable for exceptional transport widget link title 2026-08-21 12:24:08 +02:00
c40bcb7f8f fix: add structures_autocomplete_assets url route and safely handle user_config in structures_index 2026-08-21 12:17:31 +02:00
4599537be3 fix: update template string quoting and regenerate translation files 2026-08-21 12:09:09 +02:00
90323a5013 feat: update exceptional transport widget with refined UI, new status filters, and improved structure detail display 2026-08-21 11:40:21 +02:00
1ee9164faf feat: add exceptional transport dashboard widget with role-based access and map visualization 2026-08-21 11:32:08 +02:00
3ebe87099d feat: add management command to import exceptional transport data for structures from CSV 2026-08-21 11:13:42 +02:00
3f4ecef343 refactor: consolidate django import statements and simplify Q object references in structures.py 2026-08-21 10:34:43 +02:00
55d021b7ef feat: implement exceptional transport structures management with models, admin, and user permissions 2026-08-21 10:29:26 +02:00
a410cc855f refactor: harden API error handling in frontend modals and update asset code generation prefix in tests 2026-08-21 09:00:14 +02:00
24bf8a9bb5 feat: standardize favicon and apple-touch-icon across all application templates 2026-08-21 08:27:48 +02:00
Karl Determe
76158a3be0 feat: implement calendar functionality and add localization support for intervention models 2026-08-20 13:36:42 +02:00
Karl Determe
56281b4821 Merge remote-tracking branch 'origin/main' into main with visibility permissions fix 2026-08-20 12:08:29 +02:00
Karl Determe
a42c46f336 feat: implement time-restricted intervention visibility and occupation management permissions for teams 2026-08-20 12:05:51 +02:00
28f8204f5a refactor: optimize admin queries and add dependent dropdown logic for contract posts in operation templates 2026-08-19 16:28:51 +02:00
0d35e71cfc feat: implement dynamic filtering for contracts and contract posts in the intervention admin interface 2026-08-19 16:12:54 +02:00
5bfde04f1a feat: allow intervention association with contracts via orders and improve order selection UI in detail view 2026-08-19 16:02:02 +02:00
be04a5e4c0 feat: implement automatic contract post assignment for interventions based on operation templates 2026-08-19 15:19:05 +02:00
cfd11654ab feat: implement granular intervention permissions and support batch field updates via JSON API 2026-08-19 14:25:11 +02:00
cde2c7e52f feat: refactor post filtering logic and automate contract post population based on context counts 2026-08-19 12:19:00 +02:00
e2eb20536d feat: add allow_team_members_edit option to contracts and update permissions to allow intra-team intervention management 2026-08-19 11:55:16 +02:00
94cb7148a0 feat: add support for TrafficLightProgramming management and textarea fields in generic asset templates 2026-08-19 11:30:42 +02:00
80cbb80d06 feat: add controller model name display to traffic light intersection details 2026-08-19 11:02:54 +02:00
46f1d0d384 feat: add reprogrammation intervention type restricted to trafficlights thematic and update forms accordingly 2026-08-19 10:35:55 +02:00
f810041e98 feat: add external interventions widget to home dashboard and update localization file references 2026-08-19 09:51:46 +02:00
e93d875c70 feat: implement comprehensive associated intervention tracking and display for observations 2026-08-19 08:44:57 +02:00
1671d9e449 feat: implement intervention duplication functionality with mobile UI support and comprehensive unit tests 2026-08-19 08:34:29 +02:00
Karl Determe
86979307ae feat: add edit inspection modal component and associated test file 2026-08-19 08:11:03 +02:00
Karl Determe
b15326602b feat: implement preventive intervention templates and operation grouping system 2026-08-19 07:35:50 +02:00
Karl Determe
ca6d09c6b1 feat: add location management, asset creation modal, and intervention proximity tracking to details view 2026-08-19 07:17:15 +02:00
Karl Determe
1bdb771f3b feat: implement asset editing module with modal support and automated asset code generation 2026-08-19 06:56:15 +02:00
Karl Determe
360db5aaad feat: implement structures management module with index views, location geojson endpoints, and modal for adding assets 2026-08-18 16:05:57 +02:00
Karl Determe
ff4042b333 feat: implement document management views, forms, templates, and URL routing 2026-08-18 14:42:29 +02:00
Karl Determe
061fb6dc62 feat: add document management views, URLs, and permission unit tests 2026-08-18 14:29:00 +02:00
Karl Determe
80f2956872 chore: remove obsolete scratch_translations.json file 2026-08-18 14:13:50 +02:00
Karl Determe
192bd1cbc4 feat: implement document and folder permission logic and associated unit tests 2026-08-18 14:13:37 +02:00
Karl Determe
17726812c4 chore: remove unused file configurations and associated logic 2026-08-18 12:36:28 +02:00
781eca84c3 style: update intervention status "on_pause" color to #6b226e and adjust associated text contrast 2026-08-17 17:37:46 +02:00
6e02156034 fix: prevent forced intervention status transition to to_be_planned during unassignment if unauthorized 2026-08-17 17:06:02 +02:00
aa6c0d0603 feat: update intervention drag-and-drop permissions to include top_manager and add assigned status to editable states 2026-08-17 16:49:12 +02:00
122086e2ab feat: enable backlog visibility for external managers in calendar view and expand status access permissions 2026-08-17 16:05:21 +02:00
dcd35d843f feat: restrict calendar contract view to authorized contracts for non-admin users 2026-08-17 16:02:45 +02:00
d00bc6ff6d feat: implement contract-based asset visibility restrictions for external users 2026-08-17 15:56:28 +02:00
1fceffc293 feat: add inspection cancellation functionality with automatic asset compliance state recalculation 2026-08-17 14:57:28 +02:00
fb1ad643a9 feat: implement thematic filtering for inspection list views with updated asset category logic 2026-08-17 14:42:04 +02:00
7614d56112 refactor: redesign mobile control list cards and enhance FAB styling 2026-08-17 12:32:59 +02:00
4024dad7bc chore: update and prune translation files across multiple locales 2026-08-17 12:27:28 +02:00
5878bff447 chore: update and compile application translations 2026-08-17 12:15:13 +02:00
f1f8897ad3 feat: implement mobile controls UI and synchronize application translations 2026-08-17 12:08:52 +02:00
f5acbe27db feat: implement project selection with autocomplete, chips, and responsive column sizing in controls index view 2026-08-17 11:51:33 +02:00
5dc4d0615a feat: add filtering system to controls index with persistent user preferences 2026-08-17 11:43:31 +02:00
68d3e042dc fix(controls): fix ReferenceError hdBtn and preserve query params in cache busting url 2026-08-17 11:14:05 +02:00
06b70a3129 feat: improve photo orientation handling, EXIF transposing, continuous camera rotation, and add photo rotation tool 2026-08-17 11:07:45 +02:00
d5f7b533f2 refactor: standardize button sizes and carousel modal footer layout for responsiveness 2026-08-17 10:52:44 +02:00
adb5f08628 feat: add photo carousel for controls and improve mobile touch gestures for remark images 2026-08-17 10:49:01 +02:00
4e35da62b8 refactor: remove automatic contract assignment logic from intervention detail view 2026-08-17 09:57:09 +02:00
1e7e8ae697 feat: automate asset parent data synchronization for geometry and location fields with new management command and model hooks 2026-08-17 09:32:30 +02:00
016ce202b4 feat: improve contract selection with descriptions, enhanced visibility filtering, and user access controls 2026-08-17 08:18:25 +02:00
8b4c854416 chore: update project assets and branding icons 2026-08-16 19:21:43 +02:00
c295fc5b26 docs: update README with project branding and internationalize BIM viewer title template 2026-08-16 15:12:12 +02:00
1ca77b6689 feat: redesign and update site logo assets and navigation branding 2026-08-16 15:09:27 +02:00
6de48bee77 refactor: rename to loko; implement base template and add static assets for Loko branding 2026-08-16 15:09:01 +02:00
63e22c3166 test: localize home widget tests and update admin display to use mark_safe for conditions overview 2026-08-16 10:59:23 +02:00
Karl Determe
2ca72aa4da feat: implement mobile notification inbox and settings interface 2026-08-14 23:06:13 +02:00
Karl Determe
750abb703e feat: add mobile inbox template for notifications and discussions management 2026-08-14 23:01:43 +02:00
Karl Determe
ef3f1bdaa6 feat: implement notification system, email reminders, and user language preferences 2026-08-14 22:48:09 +02:00
Karl Determe
b1e7f726a2 feat: add intervention on pause widget and associated backend logic 2026-08-14 22:26:41 +02:00
Karl Determe
441af33ddd feat: implement discussion list and notification inbox UI with mention handling tags 2026-08-14 22:08:13 +02:00
Karl Determe
34d8e51626 feat: implement daily intervention summary email and unread notification reminders with associated settings and workflow views 2026-08-14 21:59:00 +02:00
Karl Determe
387f1ae9d6 feat: add management command to send email reminders for unread notifications 2026-08-14 21:42:21 +02:00
Karl Determe
0bdf44c90c test: add integration tests for interventions and update models logic 2026-08-14 21:30:04 +02:00
Karl Determe
6fb2e00a54 feat: add management commands to initialize and run scheduled tasks 2026-08-14 20:56:38 +02:00
Karl Determe
d3484e626d feat: implement notification inbox and discussion management views with corresponding templates and reminder command 2026-08-14 20:50:23 +02:00
07375e3be0 chore: update and standardize English translations for inspection modules 2026-08-13 20:48:55 +02:00
70cdcd9b18 refactor: update template line references in translation files due to inspection list layout changes 2026-08-13 20:08:57 +02:00
0fa9898cfa chore: update verbose name for apply_changes_to_asset field in inspection model 2026-08-13 20:04:29 +02:00
53ab3ff451 feat: implement inspection editing API and modal to allow authorized users to update inspection details 2026-08-13 20:00:44 +02:00
Karl Determe
8283adeeac chore: add and compile internationalization message files for assets, controls, home, and inspections modules 2026-08-13 16:12:55 +02:00
Karl Determe
0521e2624e feat: implement preventive intervention templates with grouping and associated forms 2026-08-13 13:52:08 +02:00
Karl Determe
7b519f16bd feat: add operation template models and preventive intervention logic with associated tests 2026-08-13 11:48:32 +02:00
Karl Determe
3d55121fbc feat: implement CMS-style inline editing with dynamic contract loading on the intervention detail page 2026-08-13 09:21:05 +02:00
526c8185fa refactor: remove TeambuildingVote feature and associated views, templates, and tests 2026-08-12 17:40:59 +02:00
d9ee551e42 chore: update and localize Inspection translations across English, French, and Dutch files 2026-08-12 14:54:44 +02:00
d3cc08cdce feat: add discussion thread support to preparation orders with messaging and access control 2026-08-12 14:50:33 +02:00
2ae78f70f6 chore: update and fix internationalization (i18n) translations across multiple modules 2026-08-12 12:22:23 +02:00
ee765e57f0 feat: implement unread notification reminder management command and user configuration settings 2026-08-12 12:15:15 +02:00
fb9664b65a feat: add reusable new discussion modal with @mention autocompletion support 2026-08-12 11:46:29 +02:00
1edfb85a0e feat: include can_inspect field in user configuration copy process and add corresponding test coverage 2026-08-12 11:12:25 +02:00
37500e1fbc feat: add stock return workflow from preparation orders with automated field prefilling and enhanced search capabilities 2026-08-12 11:05:22 +02:00
91149d2597 refactor: enhance preparation order detail UI to display return statuses and update autocomplete to support intervention search 2026-08-12 10:43:24 +02:00
ea2bb4817c feat: enable preparation order reopening and track returned quantities for deleted items 2026-08-12 10:26:08 +02:00
d5a404037c feat: add reopen functionality to preparation orders and display delta quantities for adjustments 2026-08-12 10:17:38 +02:00
7767856cb5 refactor: implement save state locking and modularize save logic in cms_edit_mode.js 2026-08-12 08:58:55 +02:00
8590ddb17c feat: allow unassigning interventions to revert status to backlog and clear schedule dates 2026-08-12 08:32:59 +02:00
093457ce8d refactor: update intervention calendar to support priority-based backlog sorting and extend pre-processing status visibility 2026-08-12 08:12:09 +02:00
718403c295 feat: add default contract support to user configurations and auto-assign during intervention creation 2026-08-12 05:26:50 +02:00
6cfd80d61e refactor: replace full name logic with short_name template filter and utility across inspection views and templates 2026-08-12 04:44:18 +02:00
e6166b6adb refactor: replace new inspection modal with direct link to map and update tests accordingly 2026-08-11 16:49:29 +02:00
97269fb001 chore: update project translations and correct localization strings across modules 2026-08-11 16:46:07 +02:00
f41b10bff8 refactor: restructure map layers for assets and locations, add SRID 3812 projection, and update UI thematic selection logic. 2026-08-11 16:27:11 +02:00
41aa78e382 feat: implement GIEP inspection question framework with localized labels and template filters 2026-08-11 15:47:17 +02:00
e4d2ca45c5 feat: add GIEP guided inspection framework with new templates, migration, and updated field registry 2026-08-11 12:31:52 +02:00
8c4556a34c feat: prevent automatic intervention re-creation in Observation model using ref check 2026-08-11 11:09:28 +02:00
107575aa60 fix: synchronize table row state with selection logic and update export flow to default to all items when none are selected 2026-08-10 22:47:18 +02:00
f34e26f8f5 feat: implement photo carousel for remark images with navigation, captions, and delete support 2026-08-10 20:22:41 +02:00
Karl Determe
4e4d84b3a4 feat: add custom CSS styles for homepage layout, cards, and map legends 2026-08-10 14:15:53 +02:00
Karl Determe
b736f93b75 feat: add custom CSS styles for homepage layout, cards, and interactive elements 2026-08-10 14:05:29 +02:00
Karl Determe
e3cdf00515 feat: create new quick inspection map template with integrated filters and mobile controls 2026-08-10 13:29:37 +02:00
Karl Determe
682464b1fa feat: implement quick inspection map interface and parking spot component template 2026-08-10 13:03:16 +02:00
Karl Determe
93f568a9ac feat: implement generic inspection system for assets and interventions 2026-08-10 12:28:18 +02:00
bc08402f34 feat: expose inspection result config and update parkingspot inspection fields with translation fixes 2026-08-08 22:06:25 +02:00
7243f33110 feat: implement dynamic inspection form partials with a registry and AJAX loading for asset-specific fields 2026-08-08 21:56:38 +02:00
2eceb7a9b6 feat: rename inspection action to New inspection across English, French, and Dutch translations 2026-08-08 21:35:43 +02:00
b0a8af9a0f feat: restrict thematic inspection access to admins and apply migration to reset non-admin permissions 2026-08-08 21:25:57 +02:00
bd1f084862 feat: add "concepts" help guide and introduce expandable help panel view 2026-08-08 15:46:33 +02:00
757f19ecce feat: refactor inspection features into a dedicated inspections app and migrate existing models and views. 2026-08-08 15:08:04 +02:00
952f3111d6 chore: update and localize internationalization translation files for stock and sign modules 2026-08-08 12:06:21 +02:00
73e71651bb chore: update and prune translation files and clean up inspection view logic 2026-08-08 12:05:28 +02:00
79673367f0 feat: implement asset preselection in quick inspection map with coordinate and filter auto-focus 2026-08-08 10:04:55 +02:00
ccc465bf5d feat: add physical inspection tracking and UI component for asset inspections 2026-08-08 09:40:56 +02:00
66269d0462 feat: add MapLibre visualization for inspections list via geolocated asset markers in a modal 2026-08-07 19:23:29 +02:00
7731ece2ea feat: implement asset code/name search in inspections and restrict thematic visibility based on user configuration 2026-08-07 19:09:06 +02:00
8c85719e45 feat: add configurable map inspection history overlay and detail modal 2026-08-07 18:54:39 +02:00
adaf19ab39 feat: add continuous photo capture functionality for asset inspections and update mobile dashboard navigation 2026-08-07 17:53:00 +02:00
41619256a9 feat: introduce can_inspect permission per thematic with restricted access to inspection views 2026-08-07 16:36:19 +02:00
d725da4c57 feat: implement quick asset inspection modal and associated API endpoints for status reporting and automated intervention creation 2026-08-07 14:51:19 +02:00
6b42824d4e feat: add functionality to create interventions directly from asset views and update asset model content type lookups. 2026-08-07 11:48:58 +02:00
286c3e07f2 feat: add asset interventions list component to asset detail view 2026-08-07 11:33:29 +02:00
c74e96bf29 feat: add parking thematic with dedicated management views, models, and configurable funding program field 2026-08-07 11:30:00 +02:00
dad5acc4fb feat: implement parking spot models and generic asset inspection framework 2026-08-07 10:09:20 +02:00
70c3ff3dea fix: escape JavaScript strings in observation form template translations 2026-08-07 09:38:49 +02:00
57367d825b refactor: replace format_html with mark_safe in admin models and fix translation string syntax in observation form template 2026-08-07 09:28:22 +02:00
297f51077e chore: update localization files and adjust observation form template references 2026-08-06 16:37:50 +02:00
2a90736430 feat: consolidate document deletion logic and expose deletion UI based on granular permissions 2026-08-06 13:28:45 +02:00
64423b3de5 feat: implement recursive folder permission checks and add integration tests for thematic asset management 2026-08-06 12:41:08 +02:00
fe90a55e68 refactor: update translation files and remove obsolete home index references 2026-08-05 15:55:16 +02:00
72ff9852bd refactor: update template to allow changing preference settings for gps. 2026-08-05 15:44:39 +02:00
abfd9c3efd feat: add fill property to interventions index mobile template using hexColor helper 2026-08-05 15:34:36 +02:00
845933c33c feat: implement GPS navigation integration with modal selection and persistent provider preferences 2026-08-05 15:30:06 +02:00
0492d5ca1d feat: implement GeoSyncMixin to automatically synchronize geometry, coordinates, and GeoJSON fields in location models 2026-08-05 14:34:11 +02:00
fd54d5765d Merge remote-tracking branch 'origin/main' 2026-08-05 12:34:36 +02:00
66ab7a2f3d feat: extend intervention filtering to support codes/identifiers for thematics, contracts, and orders, and remove global search hook 2026-08-05 12:32:27 +02:00
Karl Determe
c139343278 Merge branch 'main' of https://github.com/kdeterme/streetup-core 2026-08-04 11:13:27 +02:00
Karl Determe
9806175619 fix: resolve Django Admin 500 error on scheduled tasks list 2026-08-04 11:12:13 +02:00
49019400b8 feat: remove legacy home_index_v1 template and finalize Dutch translations 2026-08-03 12:24:17 +02:00
e72b765adb chore: update and correct application translations across multiple locales 2026-08-03 12:13:01 +02:00
96eecb75c2 fix: update map initialization to explicitly handle maxBounds nullability and conditional assignment 2026-08-03 11:13:21 +02:00
4be78d7c0d fix: resolve broken layout structure by closing unclosed div tags in observation_detail template 2026-07-31 16:43:16 +02:00
cdcff04a38 feat: display warning alert on observation detail when linked intervention is canceled 2026-07-31 16:37:07 +02:00
0620f6a908 chore: update RoadAuthorization model metadata and ordering in migration 0002 2026-07-29 08:38:28 +02:00
3c7547ba5d refactor: standardize terminology to "public space", update email templates, and improve organization branding logic. 2026-07-29 08:23:54 +02:00
9c822719b5 feat: implement user preference persistence for assets index thematics and filters 2026-07-28 15:08:12 +02:00
abf4470441 refactor: simplify port mapping comments in docker-compose.yml 2026-07-28 14:41:37 +02:00
2fb458ebfb feat: update localization translations and add new building asset fields 2026-07-28 14:23:01 +02:00
91283f866b feat: implement 3D IFC model viewer with Web-IFC and Three.js integration 2026-07-28 14:20:09 +02:00
ee6964eb5b docs: update project description and extend supported asset categories in README 2026-07-28 08:24:27 +02:00
a8fbb87841 docs: add EUPL licensing details to README 2026-07-28 08:12:37 +02:00
b04b612dd8 feat: implement outdoor location management system with dedicated models, views, and detail templates 2026-07-27 15:23:39 +02:00
044e01add8 refactor: unify building location hierarchy and consolidate asset management using a recursive tree structure 2026-07-27 14:44:23 +02:00
a4f6769c7d feat(buildings): add interactive map with address search and orthophoto controls for building editing 2026-07-27 10:17:00 +02:00
c86fb5fb70 feat(buildings): add dedicated edit form, view, permission checks and template for PublicBuilding 2026-07-27 10:07:21 +02:00
a88a77e74e i18n: translate all building choices, model fields and regional terminology 2026-07-27 09:59:19 +02:00
ecc36f46cf i18n: translate building attributes, tree counters, equipment headers and clean fuzzy flags 2026-07-27 09:54:03 +02:00
3282c4a728 i18n: complete translations for public buildings list and detail views 2026-07-27 09:48:11 +02:00
77a553b5c4 feat(buildings): UX cards view, 3-tab layout, hierarchical tree, point geometry for unified map, generic tables dropdown, and translations 2026-07-27 09:44:31 +02:00
5db00ec09c feat: add Localisation, Documents and Interventions tabs to public building detail view 2026-07-27 08:29:13 +02:00
ca5d17261b feat: dynamic regional get_usage_type_display method and get_usage_type_choices provider in PublicBuilding model 2026-07-27 08:18:20 +02:00
a1d9c2022c feat: add regional_standard configuration and dynamic terminology context processor 2026-07-26 19:40:39 +02:00
efc0e3f955 fix: change extends template tag from home/base.html to base.html in building templates 2026-07-26 19:21:34 +02:00
24355b2e2f feat: add publicbuildings thematic, models, views, GeoJSON endpoints and unified assets map integration 2026-07-26 19:03:31 +02:00
f3207e946c refactor: remove entire schools application module and clean up related configurations 2026-07-26 14:59:20 +02:00
034cf1e918 refactor: modularize Panoramax dependencies, improve environment variable management, and update Docker container configuration 2026-07-26 09:16:04 +02:00
Karl Determe
bec7153b7a docs: simplify containerization description in README 2026-07-23 15:04:03 +02:00
Karl Determe
771ac2c46a docs: add project README and update gitignore to include it 2026-07-23 15:02:01 +02:00
Karl Determe
bc138c47f0 refactor: remove obsolete docker-compose configurations 2026-07-23 14:57:04 +02:00
Karl Determe
bb5fcab2c2 migration: remove origin_fms_nbr and origin_prego_nbr from intervention model 2026-07-23 09:23:58 +02:00
Karl Determe
c128c07288 chore: update web service image path to support configurable image names 2026-07-23 08:48:24 +02:00
Karl Determe
d3ddb1ec8a fix: Resolve Django lazy translation strings (Promise) to python str before exporting to openpyxl cells 2026-07-23 07:56:15 +02:00
Karl Determe
5b316f2d8a Refactor: Move custom source changed event dispatch to the end of handleSourceChange 2026-07-23 07:49:52 +02:00
Karl Determe
b871c17fbc refactor: implement extensible architecture for streetup-core with hooks and reporting services 2026-07-22 21:03:26 +02:00
2369 changed files with 432007 additions and 116742 deletions

View file

@ -25,10 +25,10 @@ venv_old/
*.sqlite3 *.sqlite3
# Django media & static # Django media & static
streetup/media/ loko/media/
streetup/private_media/ loko/private_media/
streetup/static/ loko/static/
streetup/django.log loko/django.log
*.log *.log
# VS Code / Editor config # VS Code / Editor config

View file

@ -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
View file

@ -7,8 +7,9 @@ node_modules/
.well-known/ .well-known/
*.log *.log
*.md *.md
!streetup/common/help_docs/ !README.md
!streetup/common/help_docs/**/*.md !loko/common/help_docs/
!loko/common/help_docs/**/*.md
media/ media/
private_media/ private_media/
@ -24,19 +25,26 @@ __pycache__/
*$py.class *$py.class
streetup/.env loko/.env
scripts/ /scripts/
!loko/scripts/
!loko/scripts/**
carl_export/ carl_export/
carl_export.* carl_export.*
streetup/static/ loko/static/
deploy.sh deploy.sh
generated_passwords.txt generated_passwords.txt
streetup/generated_passwords.txt loko/generated_passwords.txt
scratch/
Caddyfile Caddyfile
Caddyfile.prod Caddyfile.prod
.vscode/ .vscode/
# AI Training & ML Artifacts
runs/
*.pt
loko/sign/ai/training/
labels_signpanels/

View file

@ -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 # Création de l'environnement conda avec les packages cartographiques et poppler
# gdal, geos, proj et poppler sont installés depuis conda-forge # 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 \ python=3.12 \
gdal \ gdal \
geos \ geos \
@ -35,19 +35,19 @@ RUN mamba create -n streetup-env -y -c conda-forge \
&& mamba clean -afy && mamba clean -afy
# Activation par défaut de l'environnement Conda dans le conteneur # Activation par défaut de l'environnement Conda dans le conteneur
ENV PATH=/opt/conda/envs/streetup-env/bin:$PATH ENV PATH=/opt/conda/envs/loko-env/bin:$PATH
ENV CONDA_DEFAULT_ENV=streetup-env ENV CONDA_DEFAULT_ENV=loko-env
# Configuration explicite des variables GIS pour GeoDjango # Configuration explicite des variables GIS pour GeoDjango
ENV GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so ENV GDAL_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgdal.so
ENV GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so ENV GEOS_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgeos_c.so
ENV PROJ_DATA=/opt/conda/envs/streetup-env/share/proj ENV PROJ_DATA=/opt/conda/envs/loko-env/share/proj
# Argument de build pour activer l'installation des librairies IA (ultralytics, OpenCV...) # Argument de build pour activer l'installation des librairies IA (ultralytics, OpenCV...)
# Passer INSTALL_AI_LIBS=true dans le docker-compose pour les inclure # Passer INSTALL_AI_LIBS=true dans le docker-compose pour les inclure
ARG INSTALL_AI_LIBS=false 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/ COPY requirements/ /app/requirements/
RUN pip install --no-cache-dir -r requirements/base.txt && \ RUN pip install --no-cache-dir -r requirements/base.txt && \
if [ "$INSTALL_AI_LIBS" = "true" ]; then \ if [ "$INSTALL_AI_LIBS" = "true" ]; then \
@ -55,50 +55,60 @@ RUN pip install --no-cache-dir -r requirements/base.txt && \
fi fi
# 1. Copie des répertoires volumineux et statiques (très bonne mise en cache) # 1. Copie des répertoires volumineux et statiques (très bonne mise en cache)
COPY streetup/road_detection/detection_models/ /app/road_detection/detection_models/ COPY loko/scripts/ /app/scripts/
COPY streetup/scripts/ /app/scripts/ COPY loko/common/ /app/common/
COPY streetup/common/ /app/common/ COPY loko/home/ /app/home/
COPY streetup/home/ /app/home/
# 2. Copie des modules de l'application qui changent rarement # 2. Copie des modules de l'application qui changent rarement
COPY streetup/locale/ /app/locale/ COPY loko/locale/ /app/locale/
COPY streetup/templates/ /app/templates/ COPY loko/templates/ /app/templates/
COPY streetup/analytics/ /app/analytics/ COPY loko/analytics/ /app/analytics/
COPY streetup/api/ /app/api/ COPY loko/api/ /app/api/
COPY streetup/authorizations/ /app/authorizations/ COPY loko/authorizations/ /app/authorizations/
COPY streetup/contracts/ /app/contracts/ COPY loko/contracts/ /app/contracts/
COPY streetup/documents/ /app/documents/ COPY loko/documents/ /app/documents/
COPY streetup/maps/ /app/maps/ COPY loko/maps/ /app/maps/
COPY streetup/notifications/ /app/notifications/ COPY loko/notifications/ /app/notifications/
COPY streetup/projects/ /app/projects/ COPY loko/projects/ /app/projects/
COPY streetup/reporting/ /app/reporting/ COPY loko/reporting/ /app/reporting/
COPY streetup/schools/ /app/schools/ COPY loko/bim/ /app/bim/
COPY streetup/sign/ /app/sign/ COPY loko/sign/ /app/sign/
COPY streetup/stock/ /app/stock/ COPY loko/stock/ /app/stock/
# 3. Copie des modules actifs ou modifiés fréquemment # 3. Copie des modules actifs ou modifiés fréquemment
COPY streetup/streetup/ /app/streetup/ COPY loko/loko/ /app/loko/
COPY streetup/mobile/ /app/mobile/ COPY loko/mobile/ /app/mobile/
COPY streetup/observations/ /app/observations/ COPY loko/observations/ /app/observations/
COPY streetup/controls/ /app/controls/ COPY loko/controls/ /app/controls/
COPY streetup/interventions/ /app/interventions/ COPY loko/interventions/ /app/interventions/
COPY streetup/assets/ /app/assets/ 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) # 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 loko/road_detection/__init__.py /app/road_detection/
COPY streetup/road_detection/admin.py /app/road_detection/ COPY loko/road_detection/admin.py /app/road_detection/
COPY streetup/road_detection/apps.py /app/road_detection/ COPY loko/road_detection/apps.py /app/road_detection/
COPY streetup/road_detection/detection.py /app/road_detection/ COPY loko/road_detection/detection.py /app/road_detection/
COPY streetup/road_detection/forms.py /app/road_detection/ COPY loko/road_detection/forms.py /app/road_detection/
COPY streetup/road_detection/models.py /app/road_detection/ COPY loko/road_detection/models.py /app/road_detection/
COPY streetup/road_detection/urls.py /app/road_detection/ COPY loko/road_detection/urls.py /app/road_detection/
COPY streetup/road_detection/views.py /app/road_detection/ COPY loko/road_detection/views.py /app/road_detection/
COPY streetup/road_detection/migrations/ /app/road_detection/migrations/ COPY loko/road_detection/migrations/ /app/road_detection/migrations/
COPY streetup/road_detection/templates/ /app/road_detection/templates/ COPY loko/road_detection/templates/ /app/road_detection/templates/
# 5. Copie des fichiers Python et de configuration restants à la racine # 5. Copie des fichiers Python et de configuration restants à la racine
COPY streetup/*.py /app/ COPY loko/*.py /app/
COPY streetup/generated_passwords.txt* /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 # Exposer le port par défaut
EXPOSE 8000 EXPOSE 8000

70
README.md Normal file
View 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

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documentation StreetUp</title> <title>Documentation Loko</title>
<style> <style>
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@ -112,13 +112,13 @@
</head> </head>
<body> <body>
<div class="container"> <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> 2. <a href="#architecture-de-lapplication">Architecture de l'application</a>
3. <a href="#modules-django">Modules Django</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> 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> 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> 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>Feux de signalisation</strong> (Traffic Lights)</li>
<li><strong>Éclairage public</strong> (Public Lighting)</li> <li><strong>Éclairage public</strong> (Public Lighting)</li>
<li><strong>Panneaux de signalisation</strong> (Road Signs)</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>Patrimoine artistique</strong> (Artworks - monuments, fontaines)</li>
<li><strong>ITS - Systèmes de transport intelligents</strong> (Intelligent Transport Systems)</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> <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/ </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/
├── streetup/ # Configuration principale Django ├── loko/ # Configuration principale Django
│ ├── settings.py # Paramètres de l'application │ ├── settings.py # Paramètres de l'application
│ ├── urls.py # Routes principales │ ├── urls.py # Routes principales
│ └── wsgi.py # Point d'entrée WSGI │ └── wsgi.py # Point d'entrée WSGI
@ -697,7 +697,7 @@
│ContractPost │◄──────┘ │ContractPost │◄──────┘
└─────────────┘ └─────────────┘
</code></pre></p><p>---</p><p><h2>Diagrammes</h2></p><p><h3>Architecture globale</h3></p><p><pre><code class="">┌─────────────────────────────────────────────────────────────────────────────────────┐ </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 │ │ Système de gestion des actifs de voirie │
├─────────────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────────────┤
│ │ │ │
@ -847,7 +847,7 @@
</ul></p><p><h3>API</h3></p><p><ul> </ul></p><p><h3>API</h3></p><p><ul>
<li>Module <code>api/</code> pour les endpoints REST</li> <li>Module <code>api/</code> pour les endpoints REST</li>
<li>Support de l'interface mobile</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 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 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 4. <strong>Géolocalisation</strong> : Support natif des données géospatiales avec PostGIS

View file

@ -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

View file

@ -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/streetup:${IMAGE_TAG:-latest}
container_name: ${CONTAINER_PREFIX}_web
restart: unless-stopped
command: /docker-entrypoint.sh
networks:
- streetup_network
volumes:
- ${SRV_DIR:-/srv/streetup}/media:/app/media
- ${SRV_DIR:-/srv/streetup}/private_media:/app/private_media
- ${SRV_DIR:-/srv/streetup}/static:/app/static
- ${SRV_DIR:-/srv/streetup}/backups:/app/backups
- ${BACKUP_HOST_SSH_KEY:-${HOME}/.ssh/id_ed25519}:/app/backup_key:ro
env_file:
- .env
environment:
- DB_HOST=db
- DB_PORT=5432
- GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so
- GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so
- PROJ_DATA=/opt/conda/envs/streetup-env/share/proj
- LOG_FILE_PATH=/app/django.log
depends_on:
db:
condition: service_healthy
rclone-backup:
image: rclone/rclone:latest
container_name: ${CONTAINER_PREFIX}_backup_rclone
restart: unless-stopped
volumes:
- ${SRV_DIR:-/srv/streetup}/media:/data/media:ro
- ${SRV_DIR:-/srv/streetup}/private_media:/data/private_media:ro
- ./rclone:/config/rclone
environment:
- BACKUP_INTERVAL=86400
- DESTINATION=gdrive:streetup_backup
entrypoint: >
sh -c '
echo "=== Démarrage du daemon de sauvegarde Rclone ===";
while true; do
echo "=== Début de la synchronisation : $$(date) ===";
rclone sync /data/media $${DESTINATION}/media --verbose;
rclone sync /data/private_media $${DESTINATION}/private_media --verbose;
echo "=== Fin de la synchronisation : $$(date) ===";
echo "Sommeil de $${BACKUP_INTERVAL} secondes...";
sleep $${BACKUP_INTERVAL};
done
'
caddy:
image: caddy:latest
container_name: ${CONTAINER_PREFIX}_caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ${CONTAINER_PREFIX}_caddy_data:/data
- ${CONTAINER_PREFIX}_caddy_config:/config
- ${SRV_DIR:-/srv/streetup}/static:/srv/static:ro
- ${SRV_DIR:-/srv/streetup}/media:/srv/media:ro
- /srv/streetup-test/static:/srv/static_test:ro
- /srv/streetup-test/media:/srv/media_test:ro
depends_on:
- web
networks:
- streetup_network
volumes:
streetup_prod_caddy_data:
streetup_prod_caddy_config:
streetup_test_caddy_data:
streetup_test_caddy_config:
networks:
streetup_network:
external: true

View file

@ -1,19 +1,21 @@
services: services:
db: db:
image: postgis/postgis:18-3.6 image: postgis/postgis:18-3.6
container_name: streetup_db container_name: loko_db
restart: unless-stopped restart: unless-stopped
ports: ports:
- "5434:5432" # Mappé sur le port 5434 de l'hôte pour éviter tout conflit avec PostgreSQL local - "5436:5432" # Mappé sur le port 5436 de l'hôte
env_file:
- ./loko/.env
environment: environment:
POSTGRES_DB: streetup POSTGRES_DB: ${DB_NAME:-loko}
POSTGRES_USER: admin POSTGRES_USER: ${DB_USER:-admin}
POSTGRES_PASSWORD: replace_by_your_pwd POSTGRES_PASSWORD: ${DB_PASSWORD:?La variable DB_PASSWORD est obligatoire dans le fichier .env}
volumes: volumes:
- streetup_db_data:/var/lib/postgresql - loko_db_data:/var/lib/postgresql
- ./streetup/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro - ./loko/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
healthcheck: healthcheck:
test: [ "CMD-SHELL", "pg_isready -U admin -d streetup" ] test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-admin} -d ${DB_NAME:-loko}" ]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@ -25,25 +27,25 @@ services:
args: args:
# Mettre "true" pour installer ultralytics, OpenCV et PyTorch dans l'image # Mettre "true" pour installer ultralytics, OpenCV et PyTorch dans l'image
INSTALL_AI_LIBS: "false" INSTALL_AI_LIBS: "false"
container_name: streetup_web container_name: loko_web
restart: unless-stopped restart: unless-stopped
command: python manage.py runserver 0.0.0.0:8000 command: python manage.py runserver 0.0.0.0:8000
ports: ports:
- "8000:8000" - "8003:8000" # Mappé sur le port 8003 de l'hôte
volumes: volumes:
- ./streetup:/app # Volume mount pour le hot-reload local - ./loko:/app # Volume mount pour le hot-reload local
- streetup_media:/app/media - loko_media:/app/media
- streetup_private_media:/app/private_media - loko_private_media:/app/private_media
env_file: env_file:
- ./streetup/.env # Chargement du fichier d'environnement local - ./loko/.env # Chargement du fichier d'environnement local
environment: environment:
# Surcharges d'environnement internes au conteneur pour cibler la base du conteneur "db" # Surcharges d'environnement internes au conteneur pour cibler la base du conteneur "db"
- DB_HOST=db - DB_HOST=db
- DB_PORT=5432 - DB_PORT=5432
# Surcharges des chemins GIS internes au conteneur pour écraser ceux de l'hôte issus du .env # 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 - GDAL_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgdal.so
- GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so - GEOS_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgeos_c.so
- PROJ_DATA=/opt/conda/envs/streetup-env/share/proj - PROJ_DATA=/opt/conda/envs/loko-env/share/proj
# Correction du chemin des logs pour rester dans le conteneur # Correction du chemin des logs pour rester dans le conteneur
- LOG_FILE_PATH=/app/django.log - LOG_FILE_PATH=/app/django.log
depends_on: depends_on:
@ -52,22 +54,20 @@ services:
caddy: caddy:
image: caddy:latest image: caddy:latest
container_name: streetup_caddy container_name: loko_caddy
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:80" - "8083:80" # Mappé sur le port 8083 de l'hôte
- "443:443"
- "443:443/udp"
volumes: volumes:
- ./Caddyfile:/etc/caddy/Caddyfile - ./Caddyfile:/etc/caddy/Caddyfile
- streetup_caddy_data:/data - loko_caddy_data:/data
- streetup_caddy_config:/config - loko_caddy_config:/config
depends_on: depends_on:
- web - web
volumes: volumes:
streetup_db_data: loko_db_data:
streetup_media: loko_media:
streetup_private_media: loko_private_media:
streetup_caddy_data: loko_caddy_data:
streetup_caddy_config: loko_caddy_config:

View file

@ -4,7 +4,7 @@
# "command: python manage.py runserver ..." dans docker-compose.yml. # "command: python manage.py runserver ..." dans docker-compose.yml.
set -e set -e
exec gunicorn streetup.wsgi:application \ exec gunicorn loko.wsgi:application \
--bind 0.0.0.0:8000 \ --bind 0.0.0.0:8000 \
--workers "${GUNICORN_WORKERS:-4}" \ --workers "${GUNICORN_WORKERS:-4}" \
--threads "${GUNICORN_THREADS:-2}" \ --threads "${GUNICORN_THREADS:-2}" \

View file

@ -11,7 +11,7 @@ ROOT_URL=
# - Pour un usage local (hors Docker) : DB_HOST=127.0.0.1, DB_PORT=5432 # - Pour un usage local (hors Docker) : DB_HOST=127.0.0.1, DB_PORT=5432
DB_HOST= DB_HOST=
DB_PORT= DB_PORT=
DB_NAME= DB_NAME=loko
DB_USER= DB_USER=
DB_PASSWORD= DB_PASSWORD=
@ -23,9 +23,9 @@ DB_PASSWORD=
# GDAL_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgdal.so # GDAL_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgdal.so
# GEOS_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgeos_c.so # GEOS_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgeos_c.so
# - Docker (Miniforge/Conda interne - défini par défaut dans le Dockerfile) : # - Docker (Miniforge/Conda interne - défini par défaut dans le Dockerfile) :
# GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so # GDAL_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgdal.so
# GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so # GEOS_LIBRARY_PATH=/opt/conda/envs/loko-env/lib/libgeos_c.so
# PROJ_DATA=/opt/conda/envs/streetup-env/share/proj # PROJ_DATA=/opt/conda/envs/loko-env/share/proj
EMAIL_HOST= EMAIL_HOST=
EMAIL_PORT= EMAIL_PORT=

View file

@ -1,4 +1,5 @@
from django.urls import path, re_path, include from django.urls import path, re_path, include
from .views_version import VersionInfoView
from .views import ( from .views import (
GeoProxyView, GeoProxyView,
InterventionListCreateView, InterventionListCreateView,
@ -10,6 +11,7 @@ from .views import (
app_name = "zapi" app_name = "zapi"
urlpatterns = [ urlpatterns = [
path("version/", VersionInfoView.as_view(), name="system-version"),
# API root (optionnel: une vue d'info/health) # API root (optionnel: une vue d'info/health)
# path("", APIRootView.as_view(), name="root"), # path("", APIRootView.as_view(), name="root"),

59
loko/api/views_version.py Normal file
View 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())

View file

@ -321,7 +321,10 @@ admin.site.register(ArtworkFountainDocument, AssetAdmin)
admin.site.register(ArtworkMonumentDocument, 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): def make_folder_attachment_inline(thematic_code):
@ -359,12 +362,26 @@ StructureFolderAttachmentInline = make_folder_attachment_inline('structures')
ControlCenterFolderAttachmentInline = make_folder_attachment_inline('control_centers') ControlCenterFolderAttachmentInline = make_folder_attachment_inline('control_centers')
TrafficLightFolderAttachmentInline = make_folder_attachment_inline('traffic_lights') 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) @admin.register(Structure)
class StructureAdmin(AssetAdmin): class StructureAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl") search_fields = ("code", "name_fr", "name_nl")
list_display = ("code", "name_fr", "structure_type") list_display = ("code", "name_fr", "structure_type")
list_filter = ("structure_type",) list_filter = ("structure_type",)
inlines = [StructureFolderAttachmentInline] inlines = [StructureFolderAttachmentInline, StructureExceptionalTransportInline, StructureExceptionalTransportNoticeInline]
@admin.register(StructureLocation) @admin.register(StructureLocation)
class StructureLocationAdmin(AssetAdmin): class StructureLocationAdmin(AssetAdmin):
@ -373,9 +390,21 @@ class StructureLocationAdmin(AssetAdmin):
admin.site.register(StructureDocument, AssetAdmin) admin.site.register(StructureDocument, AssetAdmin)
admin.site.register(StructureAssetModel, AssetAdmin) admin.site.register(StructureAssetModel, AssetAdmin)
admin.site.register(StructureGeoAsset, AssetAdmin) admin.site.register(StructureGeoAsset, AssetAdmin)
admin.site.register(StructureContract, 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 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) 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) admin.site.register(ITSAssetModel, AssetAdmin)
@ -439,6 +471,44 @@ class ITSGeoAssetContractAdmin(AssetAdmin):
list_filter = ("contract",) 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 from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
admin.site.register(ITSLocation, AssetAdmin) admin.site.register(ITSLocation, AssetAdmin)
@ -648,6 +718,127 @@ admin.site.register(CleanGlassContainer, AssetAdmin)
admin.site.register(CleanLocationContract, AssetAdmin) admin.site.register(CleanLocationContract, AssetAdmin)
from .models import CleanLitterBinPhoto, CleanLitterBinObservation from .models import CleanLitterBinPhoto, CleanLitterBinObservation
from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument
from .models import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
admin.site.register(CleanLitterBinPhoto, AssetAdmin) class PublicBuildingDocumentInline(admin.TabularInline):
admin.site.register(CleanLitterBinObservation, AssetAdmin) 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"

View file

@ -47,3 +47,22 @@ class AssetsConfig(AppConfig):
).exists() ).exists()
register('assets', 'TrafficLightControllerLogFile', 'file', guard_tl_controller_log) 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
View 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'})

Binary file not shown.

Binary file not shown.

View 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"

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

View 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"

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

View 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"

View file

@ -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.")

View file

@ -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 !"))

View 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}"
))

View 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}"))

View 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()})."))

View 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()}"))

View file

@ -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."))

View file

@ -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)."

File diff suppressed because it is too large Load diff

View 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}")

View 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."))

View 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!"))

View file

@ -0,0 +1,171 @@
# Generated by Django 6.0.7 on 2026-07-26 13:20
import assets.models.buildings
import common.storages
import django.contrib.gis.db.models.fields
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0004_initial'),
('documents', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BuildingLevel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50, verbose_name='Code du niveau')),
('name', models.CharField(max_length=200, verbose_name='Nom du niveau')),
('level_number', models.IntegerField(default=0, verbose_name="Numéro d'étage (0=RDC, 1=1er, -1=Sous-sol)")),
('floor_plan', models.FileField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage")),
],
options={
'verbose_name': 'Niveau / Étage',
'verbose_name_plural': 'Niveaux / Étages',
'ordering': ['building', 'level_number'],
},
),
migrations.CreateModel(
name='PublicBuilding',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
('installation_date', models.DateTimeField(blank=True, null=True)),
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
('last_survey_date', models.DateTimeField(blank=True, null=True)),
('brand', models.CharField(blank=True, max_length=500, null=True)),
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
('updated_at', models.DateTimeField(auto_now=True)),
('lon', models.FloatField(blank=True, null=True)),
('lat', models.FloatField(blank=True, null=True)),
('geojson', models.TextField(blank=True, null=True)),
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise cartographique')),
('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse complète')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('owner', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propriétaire')),
('occupant_entity', models.CharField(blank=True, max_length=255, null=True, verbose_name='Direction / Service occupant')),
('usage_type', models.CharField(choices=[('administrative', 'Administratif / Mairie'), ('education', 'Enseignement / École / Crèche'), ('sports', 'Sport & Équipement sportif'), ('culture', 'Culture / Médiathèque / Musée'), ('social', 'Social / Centre communautaire'), ('technical', 'Technique / Ateliers / Stock'), ('housing', 'Logement communal'), ('other', 'Autre bâtiment public')], default='administrative', max_length=50, verbose_name="Type d'usage principal")),
('building_status', models.CharField(choices=[('in_service', 'En service'), ('under_renovation', 'En travaux / Rénovation'), ('decommissioned', 'Désaffecté'), ('ceded', 'Cédé / Vendu')], default='in_service', max_length=50, verbose_name="Statut d'occupation")),
('construction_year', models.IntegerField(blank=True, null=True, verbose_name='Année de construction')),
('floors_count', models.IntegerField(default=1, verbose_name="Nombre d'étages hors-sol")),
('basement_count', models.IntegerField(default=0, verbose_name='Nombre de niveaux en sous-sol')),
('total_surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface brute totale (m²)')),
('usable_surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface utile (m²)')),
('is_erp', models.BooleanField(default=True, verbose_name='Établissement Recevant du Public (ERP)')),
('erp_category', models.CharField(blank=True, choices=[('1', '1ère catégorie (> 1500 pers.)'), ('2', '2ème catégorie (301 - 1500 pers.)'), ('3', '3ème catégorie (101 - 300 pers.)'), ('4', "4ème catégorie (jusqu'à 100 pers.)"), ('5', '5ème catégorie (seuils spécifiques)')], max_length=10, null=True, verbose_name='Catégorie ERP')),
('erp_type', models.CharField(blank=True, choices=[('J', "J - Structures d'accueil pour personnes âgées ou handicapées"), ('L', 'L - Salles de spectacles, de réunions, polyvalentes'), ('M', 'M - Magasins, centres commerciaux'), ('N', 'N - Restaurants et débits de boissons'), ('O', 'O - Hôtels et pensions de famille'), ('P', 'P - Salles de danse et salles de jeux'), ('R', "R - Établissements d'éveil, d'enseignement, colonies de vacances"), ('S', 'S - Bibliothèques, centres de documentation'), ('T', "T - Salles d'expositions"), ('U', 'U - Établissements de soins'), ('V', 'V - Établissements de culte'), ('W', 'W - Administrations, banques, bureaux'), ('X', 'X - Établissements sportifs couverts'), ('Y', 'Y - Musées')], max_length=10, null=True, verbose_name='Type ERP')),
('max_capacity', models.IntegerField(blank=True, null=True, verbose_name="Jauge maximale d'accueil du public")),
('dpe_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Diagnostic de Performance Énergétique (DPE)')),
('ges_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Émission de Gaz à Effet de Serre (GES)')),
('has_asbestos_diag', models.BooleanField(default=False, verbose_name='Diagnostic Amiante réalisé')),
('has_lead_diag', models.BooleanField(default=False, verbose_name='Diagnostic Plomb réalisé')),
('pmr_accessible', models.BooleanField(default=False, verbose_name='Accessible PMR')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_buildings', to=settings.AUTH_USER_MODEL, verbose_name='Gestionnaire référent')),
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.publicbuilding', verbose_name='Remplacé par')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
],
options={
'verbose_name': 'Bâtiment public',
'verbose_name_plural': 'Bâtiments publics',
'ordering': ['code', 'name_fr'],
},
),
migrations.CreateModel(
name='BuildingRoom',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50, verbose_name='Code du local')),
('name', models.CharField(max_length=200, verbose_name='Nom de la pièce')),
('room_type', models.CharField(blank=True, max_length=100, null=True, verbose_name='Type de local (ex: Bureau, Classe, Sanitaires)')),
('surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface (m²)')),
('level', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rooms', to='assets.buildinglevel', verbose_name='Niveau / Étage')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='assets.publicbuilding', verbose_name='Bâtiment')),
],
options={
'verbose_name': 'Pièce / Local',
'verbose_name_plural': 'Pièces / Locaux',
'ordering': ['building', 'code'],
},
),
migrations.AddField(
model_name='buildinglevel',
name='building',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='assets.publicbuilding', verbose_name='Bâtiment'),
),
migrations.CreateModel(
name='BuildingEquipment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
('installation_date', models.DateTimeField(blank=True, null=True)),
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
('last_survey_date', models.DateTimeField(blank=True, null=True)),
('brand', models.CharField(blank=True, max_length=500, null=True)),
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
('updated_at', models.DateTimeField(auto_now=True)),
('equipment_type', models.CharField(choices=[('hvac', 'Chauffage / CVC / Chaudière'), ('elevator', 'Ascenseur / Monte-charge'), ('fire_safety', 'Sécurité incendie / Extincteur / RIA'), ('electricity', 'Tableau électrique / TGBT'), ('alarm', "Alarme / Contrôle d'accès"), ('meter', 'Compteur (Eau / Gaz / Électricité)'), ('sanitary', 'Plomberie / Sanitaires'), ('other', 'Autre équipement technique')], default='hvac', max_length=50, verbose_name="Type d'équipement")),
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle réglementaire')),
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle réglementaire')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingequipment', verbose_name='Remplacé par')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
('room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipments', to='assets.buildingroom', verbose_name='Local')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipments', to='assets.publicbuilding', verbose_name='Bâtiment')),
],
options={
'verbose_name': 'Équipement technique de bâtiment',
'verbose_name_plural': 'Équipements techniques de bâtiment',
'ordering': ['building', 'code'],
},
),
migrations.CreateModel(
name='PublicBuildingDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, null=True, verbose_name='Description du document')),
('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')),
('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')),
('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.buildings.upload_to_building, verbose_name='Fichier')),
('document_type', models.CharField(default='other', max_length=50, verbose_name='Type de document')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.publicbuilding', verbose_name='Bâtiment')),
('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')),
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')),
],
options={
'verbose_name': 'Document de bâtiment',
'verbose_name_plural': 'Documents de bâtiment',
},
),
]

View file

@ -0,0 +1,219 @@
# Generated by Django 6.0.7 on 2026-07-27 12:00
import common.storages
import django.contrib.gis.db.models.fields
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_buildinglevel_publicbuilding_buildingroom_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='buildinglevel',
name='building',
),
migrations.RemoveField(
model_name='buildingroom',
name='level',
),
migrations.RemoveField(
model_name='buildingroom',
name='building',
),
migrations.AlterModelOptions(
name='publicbuilding',
options={'ordering': ['code', 'name_fr'], 'verbose_name': 'Localisation Bâtiment public', 'verbose_name_plural': 'Localisations Bâtiments publics'},
),
migrations.RemoveField(
model_name='publicbuilding',
name='brand',
),
migrations.RemoveField(
model_name='publicbuilding',
name='category',
),
migrations.RemoveField(
model_name='publicbuilding',
name='installation_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_corrective_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_inspection_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_preventive_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='last_survey_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='marked_as_removed_by',
),
migrations.RemoveField(
model_name='publicbuilding',
name='marked_as_removed_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='note',
),
migrations.RemoveField(
model_name='publicbuilding',
name='obsolescence',
),
migrations.RemoveField(
model_name='publicbuilding',
name='replaced_by',
),
migrations.RemoveField(
model_name='publicbuilding',
name='serial_number',
),
migrations.RemoveField(
model_name='publicbuilding',
name='status',
),
migrations.RemoveField(
model_name='publicbuilding',
name='uninstallation_date',
),
migrations.RemoveField(
model_name='publicbuilding',
name='validation_status',
),
migrations.RemoveField(
model_name='publicbuilding',
name='warranty_duration',
),
migrations.AddField(
model_name='publicbuilding',
name='floor_plan',
field=models.FileField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage"),
),
migrations.AddField(
model_name='publicbuilding',
name='is_main_location',
field=models.BooleanField(default=False, verbose_name='Localisation principale'),
),
migrations.AddField(
model_name='publicbuilding',
name='level_number',
field=models.IntegerField(default=0, verbose_name="Numéro d'étage (ex: 0=RDC, 1=1er, -1=Sous-sol)"),
),
migrations.AddField(
model_name='publicbuilding',
name='location_type',
field=models.CharField(choices=[('site', 'Site / Complexe'), ('building', 'Bâtiment / Bloc'), ('level', 'Niveau / Étage'), ('room', 'Pièce / Local'), ('other', 'Autre localisation')], default='building', max_length=50, verbose_name='Type de localisation'),
),
migrations.AddField(
model_name='publicbuilding',
name='parent_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.publicbuilding', verbose_name='Localisation parente'),
),
migrations.AddField(
model_name='publicbuilding',
name='surface_m2',
field=models.FloatField(blank=True, null=True, verbose_name='Surface spécifique (m²)'),
),
migrations.AlterField(
model_name='publicbuilding',
name='dpe_rating',
field=models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Performance Énergétique (PEB / DPE / EPC)'),
),
migrations.AlterField(
model_name='publicbuilding',
name='erp_category',
field=models.CharField(blank=True, choices=[('1', '1ère catégorie (> 1500 pers.)'), ('2', '2ème catégorie (301 - 1500 pers.)'), ('3', '3ème catégorie (101 - 300 pers.)'), ('4', "4ème catégorie (jusqu'à 100 pers.)"), ('5', '5ème catégorie (seuils spécifiques)')], max_length=10, null=True, verbose_name="Catégorie d'accueil / ERP"),
),
migrations.AlterField(
model_name='publicbuilding',
name='erp_type',
field=models.CharField(blank=True, choices=[('J', "J - Structures d'accueil pour personnes âgées ou handicapées"), ('L', 'L - Salles de spectacles, de réunions, polyvalentes'), ('M', 'M - Magasins, centres commerciaux'), ('N', 'N - Restaurants et débits de boissons'), ('O', 'O - Hôtels et pensions de famille'), ('P', 'P - Salles de danse et salles de jeux'), ('R', "R - Établissements d'éveil, d'enseignement, colonies de vacances"), ('S', 'S - Bibliothèques, centres de documentation'), ('T', "T - Salles d'expositions"), ('U', 'U - Établissements de soins'), ('V', 'V - Établissements de culte'), ('W', 'W - Administrations, banques, bureaux'), ('X', 'X - Établissements sportifs couverts'), ('Y', 'Y - Musées')], max_length=10, null=True, verbose_name="Type d'établissement / ERP"),
),
migrations.AlterField(
model_name='publicbuilding',
name='ges_rating',
field=models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Émissions de Carbone & GES'),
),
migrations.AlterField(
model_name='publicbuilding',
name='is_erp',
field=models.BooleanField(default=True, verbose_name='Accueil & Sécurité du Public (ERP / PMR)'),
),
migrations.AlterField(
model_name='publicbuilding',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'),
),
migrations.AlterField(
model_name='publicbuilding',
name='usage_type',
field=models.CharField(choices=[('administrative', 'Administrative (Town Hall / Municipal Building)'), ('education', 'Enseignement / École / Crèche'), ('sports', 'Sport & Équipement sportif'), ('culture', 'Culture / Médiathèque / Musée'), ('social', 'Social / Centre communautaire'), ('technical', 'Technique / Ateliers / Stock'), ('housing', 'Logement communal'), ('other', 'Autre bâtiment public')], default='administrative', max_length=50, verbose_name="Type d'usage principal"),
),
migrations.CreateModel(
name='BuildingAsset',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=50)),
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
('installation_date', models.DateTimeField(blank=True, null=True)),
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
('last_survey_date', models.DateTimeField(blank=True, null=True)),
('brand', models.CharField(blank=True, max_length=500, null=True)),
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
('updated_at', models.DateTimeField(auto_now=True)),
('lon', models.FloatField(blank=True, null=True)),
('lat', models.FloatField(blank=True, null=True)),
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)),
('geojson', models.TextField(blank=True, null=True)),
('asset_type', models.CharField(choices=[('civil_engineering', 'Génie civil / Bâti'), ('facade', 'Façade / Mur extérieur'), ('wall', 'Mur intérieur / Cloison'), ('roof', 'Toiture / Couverture'), ('hvac', 'Chauffage / CVC / Chaudière'), ('elevator', 'Ascenseur / Monte-charge'), ('electricity', 'Électricité / TGBT'), ('fire_safety', 'Sécurité incendie / Extincteur / RIA'), ('alarm', "Alarme / Contrôle d'accès"), ('meter', 'Compteur (Eau / Gaz / Électricité)'), ('sanitary', 'Plomberie / Sanitaires'), ('other', 'Autre équipement / asset')], default='civil_engineering', max_length=50, verbose_name="Type d'asset / Équipement")),
('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle réglementaire')),
('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle réglementaire')),
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_building_assets', to='assets.publicbuilding', verbose_name='Bâtiment / Site principal')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='location_assets', to='assets.publicbuilding', verbose_name='Localisation spécifique')),
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
('parent_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_assets', to='assets.buildingasset', verbose_name='Asset parent')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingasset', verbose_name='Remplacé par')),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
],
options={
'verbose_name': 'Asset / Équipement de bâtiment',
'verbose_name_plural': 'Assets / Équipements de bâtiment',
'ordering': ['building', 'code'],
},
),
migrations.DeleteModel(
name='BuildingEquipment',
),
migrations.DeleteModel(
name='BuildingLevel',
),
migrations.DeleteModel(
name='BuildingRoom',
),
]

View file

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

View file

@ -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),
),
]

View file

@ -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',),
),
]

View 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),
]

View file

@ -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'),
),
]

View file

@ -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'),
),
]

View file

@ -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",
},
),
]

View 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