diff --git a/loko/assets/locale/en/LC_MESSAGES/django.mo b/loko/assets/locale/en/LC_MESSAGES/django.mo
index 0f0847f..4d3b7fe 100644
Binary files a/loko/assets/locale/en/LC_MESSAGES/django.mo and b/loko/assets/locale/en/LC_MESSAGES/django.mo differ
diff --git a/loko/assets/locale/en/LC_MESSAGES/django.po b/loko/assets/locale/en/LC_MESSAGES/django.po
index f51db82..d1a0a55 100644
--- a/loko/assets/locale/en/LC_MESSAGES/django.po
+++ b/loko/assets/locale/en/LC_MESSAGES/django.po
@@ -8573,6 +8573,21 @@ msgstr "GIEP zones"
msgid "Aucune localisation GIEP trouvée à proximité"
msgstr "No location found"
+msgid "Supprimer de la DB"
+msgstr "Delete from DB"
+
+msgid "Supprimer l'existant de la DB"
+msgstr "Delete existing from DB"
+
+msgid "Ne pas créer (Ignorer)"
+msgstr "Do not create (Ignore)"
+
+msgid "Archiver l'existant (Ne pas recréer)"
+msgstr "Archive existing (do not recreate)"
+
+msgid "Archiver l'existant"
+msgstr "Archive existing"
+
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
#~ msgstr "Missing from Excel file (Will be kept or archived)"
diff --git a/loko/assets/locale/fr/LC_MESSAGES/django.mo b/loko/assets/locale/fr/LC_MESSAGES/django.mo
index 26b0c00..6254f81 100644
Binary files a/loko/assets/locale/fr/LC_MESSAGES/django.mo and b/loko/assets/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/loko/assets/locale/fr/LC_MESSAGES/django.po b/loko/assets/locale/fr/LC_MESSAGES/django.po
index 77dd8bd..c4257d3 100644
--- a/loko/assets/locale/fr/LC_MESSAGES/django.po
+++ b/loko/assets/locale/fr/LC_MESSAGES/django.po
@@ -8624,6 +8624,21 @@ msgstr "Zones GIEP"
msgid "Aucune localisation GIEP trouvée à proximité"
msgstr "Aucune localisation GIEP trouvée à proximité"
+msgid "Supprimer de la DB"
+msgstr "Supprimer de la DB"
+
+msgid "Supprimer l'existant de la DB"
+msgstr "Supprimer l'existant de la DB"
+
+msgid "Ne pas créer (Ignorer)"
+msgstr "Ne pas créer (Ignorer)"
+
+msgid "Archiver l'existant (Ne pas recréer)"
+msgstr "Archiver l'existant (Ne pas recréer)"
+
+msgid "Archiver l'existant"
+msgstr "Archiver l'existant"
+
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
#~ msgstr "Absent du fichier Excel (Sera conservé ou archivé)"
diff --git a/loko/assets/locale/nl/LC_MESSAGES/django.mo b/loko/assets/locale/nl/LC_MESSAGES/django.mo
index 5c4777a..9196b46 100644
Binary files a/loko/assets/locale/nl/LC_MESSAGES/django.mo and b/loko/assets/locale/nl/LC_MESSAGES/django.mo differ
diff --git a/loko/assets/locale/nl/LC_MESSAGES/django.po b/loko/assets/locale/nl/LC_MESSAGES/django.po
index 9676da3..e1c4ac1 100644
--- a/loko/assets/locale/nl/LC_MESSAGES/django.po
+++ b/loko/assets/locale/nl/LC_MESSAGES/django.po
@@ -8572,6 +8572,21 @@ msgstr "GIEP-zones"
msgid "Aucune localisation GIEP trouvée à proximité"
msgstr "Geen locatie gevonden."
+msgid "Supprimer de la DB"
+msgstr "Verwijderen uit de database"
+
+msgid "Supprimer l'existant de la DB"
+msgstr "Bestaande verwijderen uit de database"
+
+msgid "Ne pas créer (Ignorer)"
+msgstr "Niet aanmaken (Negeren)"
+
+msgid "Archiver l'existant (Ne pas recréer)"
+msgstr "Bestaande archiveren (niet opnieuw aanmaken)"
+
+msgid "Archiver l'existant"
+msgstr "Bestaande archiveren"
+
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
#~ msgstr "Ontbreekt in het Excel-bestand (Wordt bewaard of gearchiveerd)"
diff --git a/loko/assets/templates/assets/trafficlights_import_cross_plan.html b/loko/assets/templates/assets/trafficlights_import_cross_plan.html
index 99021ae..349b975 100644
--- a/loko/assets/templates/assets/trafficlights_import_cross_plan.html
+++ b/loko/assets/templates/assets/trafficlights_import_cross_plan.html
@@ -342,20 +342,25 @@
{% else %}
{% endif %}
@@ -438,19 +443,25 @@
{% else %}
{% endif %}
@@ -537,19 +548,25 @@
{% else %}
{% endif %}
@@ -652,20 +669,25 @@
{% else %}
{% endif %}
@@ -753,6 +775,7 @@
diff --git a/loko/assets/tests.py b/loko/assets/tests.py
index 89215f0..d22c291 100644
--- a/loko/assets/tests.py
+++ b/loko/assets/tests.py
@@ -3025,6 +3025,308 @@ class TrafficLightCrossPlanImportTest(TestCase):
if os.path.exists(tmp_path):
os.remove(tmp_path)
+ def test_parse_cross_plan_com_and_alim_wires_ignored_in_detection_sections(self):
+ """
+ Verify that auxiliary wiring rows like 'Com', 'Comm', 'Alim', 'Contact', 'Radar'
+ in detection cable / Detecteur sections are not parsed as detectors or lanterns.
+ This fixes the bug where 'Com' lines were erroneously created as 'Traficam wide angle' detectors.
+ """
+ import openpyxl
+ import tempfile
+ from assets.utils.cross_plan_parser import parse_cross_plan
+
+ wb = openpyxl.Workbook()
+ ws = wb.active
+ ws.title = "kruisjesplan"
+ ws.cell(row=1, column=1, value="Sleutel:")
+ ws.cell(row=1, column=2, value="SWB01")
+ ws.cell(row=5, column=1, value="Câble 48 TWAVB 10x2x0,8 mm²")
+ ws.cell(row=6, column=3, value="Detecteur")
+ ws.cell(row=6, column=4, value="kringen")
+ ws.cell(row=7, column=5, value="A01")
+ ws.cell(row=7, column=6, value="A05")
+
+ # Row 8: Com on A01
+ ws.cell(row=8, column=2, value="1")
+ ws.cell(row=8, column=3, value="Com")
+ ws.cell(row=8, column=4, value="COM")
+ ws.cell(row=8, column=5, value="X")
+
+ # Row 9: RpC on A01
+ ws.cell(row=9, column=2, value="2")
+ ws.cell(row=9, column=3, value="RpC")
+ ws.cell(row=9, column=4, value="TMA-122 M")
+ ws.cell(row=9, column=5, value="X")
+
+ # Row 10: Alim on A05
+ ws.cell(row=10, column=2, value="3")
+ ws.cell(row=10, column=3, value="Alim")
+ ws.cell(row=10, column=4, value="24V")
+ ws.cell(row=10, column=6, value="X")
+
+ # Row 11: RpB on A05
+ ws.cell(row=11, column=2, value="4")
+ ws.cell(row=11, column=3, value="RpB")
+ ws.cell(row=11, column=4, value="TMA-122 M")
+ ws.cell(row=11, column=6, value="X")
+
+ with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
+ tmp_path = tmp.name
+ wb.save(tmp_path)
+
+ try:
+ deduced = parse_cross_plan(tmp_path)
+ detectors = deduced["detectors"]
+ detector_codes = [d["code"] for d in detectors]
+
+ # Only RpC on A01 and RpB on A05 should be recognized
+ self.assertEqual(len(detectors), 2)
+ self.assertIn("SWB01_A01_RPC", detector_codes)
+ self.assertIn("SWB01_A05_RPB", detector_codes)
+
+ # Neither Com nor Alim should be in detectors
+ self.assertNotIn("SWB01_A01_COM", detector_codes)
+ self.assertNotIn("SWB01_A05_ALIM", detector_codes)
+ self.assertFalse(any("COM" in code for code in detector_codes))
+ self.assertFalse(any("ALIM" in code for code in detector_codes))
+ self.assertEqual(len(deduced["lanterns"]), 0)
+ finally:
+ import os
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+ def test_integrate_cross_plan_archive_and_delete_actions(self):
+ """
+ Verify that users can choose 'archive' or 'delete' for assets during cross plan integration:
+ - When 'archive' is selected, the asset's status becomes 'archived'.
+ - When 'delete' is selected, the asset is removed from the database.
+ """
+ import openpyxl
+ import tempfile
+ from assets.models import (
+ TrafficLightPole, TrafficLightLantern, TrafficLightDetector,
+ TrafficLightCable, TrafficLightPoleModel, TrafficLightLanternModel,
+ TrafficLightDetectorModel, TrafficLightCableModel
+ )
+ from assets.models.sign import SignPanel, SignPanelModel
+
+ pole_model = TrafficLightPoleModel.objects.first()
+ lantern_model = TrafficLightLanternModel.objects.first()
+ detector_model = TrafficLightDetectorModel.objects.first()
+ cable_model = TrafficLightCableModel.objects.first()
+ sign_model, _ = SignPanelModel.objects.get_or_create(code="sp_test_mod", defaults={"name_fr": "Model SP Test"})
+
+ # Assets to delete
+ pole_del = TrafficLightPole.objects.create(intersection=self.intersection, code="SWB01_P_DEL", model=pole_model, status="active")
+ lan_del = TrafficLightLantern.objects.create(pole=pole_del, code="SWB01_L_DEL", model=lantern_model, status="active")
+ det_del = TrafficLightDetector.objects.create(intersection=self.intersection, code="SWB01_D_DEL", model=detector_model, status="active")
+ cab_del = TrafficLightCable.objects.create(intersection=self.intersection, code="SWB01_C_DEL", model=cable_model, status="active")
+ sp_del = SignPanel.objects.create(trafficlight_pole=pole_del, code="SWB01_SP_DEL", model=sign_model, status="active")
+ pole_standalone_del = TrafficLightPole.objects.create(intersection=self.intersection, code="SWB01_P_STANDALONE_DEL", model=pole_model, status="active")
+
+ # Assets to archive
+ pole_arc = TrafficLightPole.objects.create(intersection=self.intersection, code="SWB01_P_ARC", model=pole_model, status="active")
+ lan_arc = TrafficLightLantern.objects.create(pole=pole_arc, code="SWB01_L_ARC", model=lantern_model, status="active")
+ det_arc = TrafficLightDetector.objects.create(intersection=self.intersection, pole=pole_arc, code="SWB01_D_ARC", model=detector_model, status="active")
+ cab_arc = TrafficLightCable.objects.create(intersection=self.intersection, code="SWB01_C_ARC", model=cable_model, status="active")
+ sp_arc = SignPanel.objects.create(trafficlight_pole=pole_arc, code="SWB01_SP_ARC", model=sign_model, status="active")
+
+ # Minimal workbook so we can run import_cross_plan
+ wb = openpyxl.Workbook()
+ ws = wb.active
+ ws.title = "kruisjesplan"
+ ws.cell(row=1, column=1, value="Sleutel:")
+ ws.cell(row=1, column=2, value="SWB01")
+ ws.cell(row=5, column=1, value="Câble 01 SVAVB")
+ ws.cell(row=6, column=3, value="richting")
+ ws.cell(row=6, column=4, value="kringen")
+ ws.cell(row=7, column=5, value="P_ARC")
+ ws.cell(row=8, column=3, value="A")
+ ws.cell(row=8, column=5, value="X")
+
+ with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
+ tmp_path = tmp.name
+ wb.save(tmp_path)
+
+ try:
+ self.client.force_login(self.user)
+ from django.core.files.uploadedfile import SimpleUploadedFile
+ with open(tmp_path, "rb") as f:
+ uploaded_file = SimpleUploadedFile("plan_test_archive_delete.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+
+ url_import = reverse("assets:import_cross_plan", args=[self.intersection.id])
+ response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file})
+ self.assertEqual(response.status_code, 200)
+
+ # Test POST integrate with delete on db-only assets and archive on others
+ url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id])
+ post_data = {
+ "cross_plan_filename": response.context["cross_plan_filename"],
+ # Delete DB-only standalone pole
+ "selected_pole_db_SWB01_P_STANDALONE_DEL": "on",
+ "action_pole_db_SWB01_P_STANDALONE_DEL": "delete",
+ "db_id_pole_SWB01_P_STANDALONE_DEL": str(pole_standalone_del.id),
+ # Delete DB-only lantern
+ "selected_lantern_db_SWB01_L_DEL": "on",
+ "action_lantern_db_SWB01_L_DEL": "delete",
+ "db_id_lantern_SWB01_L_DEL": str(lan_del.id),
+ # Delete DB-only detector
+ "selected_detector_db_SWB01_D_DEL": "on",
+ "action_detector_db_SWB01_D_DEL": "delete",
+ "db_id_detector_SWB01_D_DEL": str(det_del.id),
+ # Delete DB-only cable
+ "selected_cable_db_SWB01_C_DEL": "on",
+ "action_cable_db_SWB01_C_DEL": "delete",
+ "db_id_cable_SWB01_C_DEL": str(cab_del.id),
+ # Delete other asset (signpanel)
+ f"selected_other_signpanel_{sp_del.id}": "on",
+ f"action_other_signpanel_{sp_del.id}": "delete",
+
+ # Archive DB-only pole
+ "selected_pole_db_SWB01_P_ARC": "on",
+ "action_pole_db_SWB01_P_ARC": "archive",
+ "db_id_pole_SWB01_P_ARC": str(pole_arc.id),
+ # Archive DB-only lantern
+ "selected_lantern_db_SWB01_L_ARC": "on",
+ "action_lantern_db_SWB01_L_ARC": "archive",
+ "db_id_lantern_SWB01_L_ARC": str(lan_arc.id),
+ # Archive DB-only detector
+ "selected_detector_db_SWB01_D_ARC": "on",
+ "action_detector_db_SWB01_D_ARC": "archive",
+ "db_id_detector_SWB01_D_ARC": str(det_arc.id),
+ # Archive DB-only cable
+ "selected_cable_db_SWB01_C_ARC": "on",
+ "action_cable_db_SWB01_C_ARC": "archive",
+ "db_id_cable_SWB01_C_ARC": str(cab_arc.id),
+ # Archive other asset (signpanel)
+ f"selected_other_signpanel_{sp_arc.id}": "on",
+ f"action_other_signpanel_{sp_arc.id}": "archive",
+ }
+ response_integrate = self.client.post(url_integrate, post_data)
+ self.assertEqual(response_integrate.status_code, 302)
+
+ # Verification of deleted items
+ self.assertFalse(TrafficLightPole.objects.filter(pk=pole_standalone_del.id).exists())
+ self.assertFalse(TrafficLightLantern.objects.filter(pk=lan_del.id).exists())
+ self.assertFalse(TrafficLightDetector.objects.filter(pk=det_del.id).exists())
+ self.assertFalse(TrafficLightCable.objects.filter(pk=cab_del.id).exists())
+ self.assertFalse(SignPanel.objects.filter(pk=sp_del.id).exists())
+
+ # Verification of archived items
+ pole_arc.refresh_from_db()
+ self.assertEqual(pole_arc.status, "archived")
+ lan_arc.refresh_from_db()
+ self.assertEqual(lan_arc.status, "archived")
+ det_arc.refresh_from_db()
+ self.assertEqual(det_arc.status, "archived")
+ cab_arc.refresh_from_db()
+ self.assertEqual(cab_arc.status, "archived")
+ sp_arc.refresh_from_db()
+ self.assertEqual(sp_arc.status, "archived")
+
+ finally:
+ import os
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+ def test_integrate_cross_plan_plan_asset_archive_and_delete_actions(self):
+ """
+ Verify that for assets that exist both in DB and in the plan (status identical or modified),
+ the user can select 'archive' (archiver l'existant sans recréer) or 'delete' (supprimer l'existant de la DB).
+ """
+ import openpyxl
+ import tempfile
+ from assets.models import (
+ TrafficLightPole, TrafficLightLantern, TrafficLightDetector,
+ TrafficLightCable, TrafficLightPoleModel, TrafficLightLanternModel,
+ TrafficLightDetectorModel, TrafficLightCableModel
+ )
+ from django.core.files.uploadedfile import SimpleUploadedFile
+
+ pole_model = TrafficLightPoleModel.objects.first()
+ lantern_model = TrafficLightLanternModel.objects.first()
+ detector_model = TrafficLightDetectorModel.objects.first()
+ cable_model = TrafficLightCableModel.objects.first()
+
+ existing_pole = TrafficLightPole.objects.create(intersection=self.intersection, code="SWB01_A01", model=pole_model, status="active")
+ existing_lantern = TrafficLightLantern.objects.create(pole=existing_pole, code="SWB01_A01_LAN01_A", model=lantern_model, status="active")
+ existing_detector = TrafficLightDetector.objects.create(intersection=self.intersection, code="SWB01_A01_RPC", model=detector_model, status="active")
+ existing_cable = TrafficLightCable.objects.create(intersection=self.intersection, code="SWB01_CAB01", model=cable_model, status="active")
+
+ wb = openpyxl.Workbook()
+ ws = wb.active
+ ws.title = "kruisjesplan"
+ ws.cell(row=1, column=1, value="Sleutel:")
+ ws.cell(row=1, column=2, value="SWB01")
+ ws.cell(row=5, column=1, value="Câble 01 SVAVB")
+ ws.cell(row=6, column=3, value="richting")
+ ws.cell(row=6, column=4, value="kringen")
+ ws.cell(row=7, column=5, value="A01")
+ ws.cell(row=8, column=3, value="A")
+ ws.cell(row=8, column=5, value="X")
+ ws.cell(row=9, column=3, value="RpC")
+ ws.cell(row=9, column=5, value="X")
+
+ with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
+ tmp_path = tmp.name
+ wb.save(tmp_path)
+
+ try:
+ self.client.force_login(self.user)
+ with open(tmp_path, "rb") as f:
+ uploaded_file = SimpleUploadedFile("plan_test_plan_actions.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+
+ url_import = reverse("assets:import_cross_plan", args=[self.intersection.id])
+ response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file})
+ self.assertEqual(response.status_code, 200)
+
+ # Post integrate:
+ # - detector: action="delete"
+ # - lantern: action="archive"
+ # - cable: action="delete"
+ # - pole: action="archive"
+ url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id])
+ post_data = {
+ "cross_plan_filename": response.context["cross_plan_filename"],
+ "selected_detector_SWB01_A01_RPC": "on",
+ "action_detector_SWB01_A01_RPC": "delete",
+ "db_id_detector_SWB01_A01_RPC": str(existing_detector.id),
+
+ "selected_lantern_SWB01_A01_LAN01_A": "on",
+ "action_lantern_SWB01_A01_LAN01_A": "archive",
+ "db_id_lantern_SWB01_A01_LAN01_A": str(existing_lantern.id),
+
+ "selected_cable_SWB01_CAB01": "on",
+ "action_cable_SWB01_CAB01": "delete",
+ "db_id_cable_SWB01_CAB01": str(existing_cable.id),
+
+ "selected_pole_SWB01_A01": "on",
+ "action_pole_SWB01_A01": "archive",
+ "db_id_pole_SWB01_A01": str(existing_pole.id),
+ }
+ response_integrate = self.client.post(url_integrate, post_data)
+ self.assertEqual(response_integrate.status_code, 302)
+
+ # Detector must be deleted
+ self.assertFalse(TrafficLightDetector.objects.filter(pk=existing_detector.id).exists())
+ # Cable must be deleted
+ self.assertFalse(TrafficLightCable.objects.filter(pk=existing_cable.id).exists())
+
+ # Lantern must be archived
+ existing_lantern.refresh_from_db()
+ self.assertEqual(existing_lantern.status, "archived")
+
+ # Pole must be archived
+ existing_pole.refresh_from_db()
+ self.assertEqual(existing_pole.status, "archived")
+
+ finally:
+ import os
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+
+
class AssetDefaultPositionTest(TestCase):
def test_assign_default_position_hierarchy(self):
diff --git a/loko/assets/utils/cross_plan_parser.py b/loko/assets/utils/cross_plan_parser.py
index d7be181..cf6c1d5 100644
--- a/loko/assets/utils/cross_plan_parser.py
+++ b/loko/assets/utils/cross_plan_parser.py
@@ -26,6 +26,38 @@ def is_hp_or_ls(text):
return True
return False
+def is_wiring_or_non_equipment(text):
+ """
+ Checks if the wire/direction/circuit text refers to auxiliary wiring, communication,
+ power supply, contact lines, or reserve wires that must NOT be treated as equipment
+ (neither lantern nor detector).
+ In cross plans, 'Com' / 'Comm' / 'Communication' represents communication wiring/bus
+ for detectors/cameras or radar power supplies, NOT an actual detector.
+ """
+ if not text:
+ return False
+ t_str = str(text).strip()
+ t_lower = t_str.lower()
+ t_clean = re.sub(r'[\s/\\_\-\.:;]+', ' ', t_lower).strip()
+ t_upper = re.sub(r'\s+', '', t_str.upper())
+
+ # 1. Direct keyword matches (word boundary)
+ if re.search(r'\b(?:com|comm|communication|communicatie|alim|alimentation|voeding|tension|contact|reserve|reservee|réservé|réservée|réserve|libre|vrij|spaar|terre|masse|neutre)\b', t_clean, re.IGNORECASE):
+ return True
+
+ # 2. String starts with or equals COM, COMM, ALIM, etc.
+ if re.match(r'^(?:COM|COMM|ALIM|CONTACT|TENSION|RESERVE)\d*$', t_upper, re.IGNORECASE):
+ return True
+
+ if t_upper.startswith(("COM_", "COMM_", "ALIM_", "COM-", "COMM-", "ALIM-")):
+ return True
+
+ # 3. Radar power supply (not a detector code like RA, RB, RC, RD, RM, RP)
+ if t_upper == "RADAR" or t_upper.startswith("RADAR") or (bool(re.search(r'\bradar\b', t_str, re.IGNORECASE)) and not bool(re.match(r'^(?:RA|RB|RC|RD|RM|RP)\d*$', t_upper, re.IGNORECASE))):
+ return True
+
+ return False
+
def is_cyclopieton_phase(phase):
"""
Checks if a phase code corresponds to a combined pedestrian + cyclist signal (cyclo-piéton).
@@ -283,6 +315,11 @@ def parse_cross_plan(excel_path):
pole_has_hp[p] = True
continue
+ # Check if wire refers to communication, power supply, contact, reserve, etc.
+ # It must NOT be treated as an equipment (neither lantern nor detector).
+ if is_wiring_or_non_equipment(richting) or is_wiring_or_non_equipment(kringen):
+ continue
+
# Detectors
# Check if push button (e.g. Dka, Dkb, PBa1, BPf4...)
if richting.lower().startswith(("dk", "pb", "bp")):
@@ -383,7 +420,7 @@ def parse_cross_plan(excel_path):
# Richting codes are typically: A, B, C, T1, T2, F1, F2, a, b, c...
# Ignore wiring/cabling categories like ALIM, Radar, Com, contact, HP/LS, etc.
normalized = re.sub(r'\s+', ' ', richting).strip()
- if is_hp_or_ls(richting) or is_hp_or_ls(kringen) or re.search(r'(?:alim|com|contact|radar|tension|comm|\bhp\b|\bls\b|hp/ls|ls/hp)', normalized, re.IGNORECASE):
+ if is_hp_or_ls(richting) or is_hp_or_ls(kringen) or is_wiring_or_non_equipment(richting) or is_wiring_or_non_equipment(kringen) or re.search(r'(?:alim|com|contact|radar|tension|comm|\bhp\b|\bls\b|hp/ls|ls/hp)', normalized, re.IGNORECASE):
continue
if p not in pole_phases:
diff --git a/loko/assets/views/trafficlights.py b/loko/assets/views/trafficlights.py
index feaff8a..3cb0a5c 100644
--- a/loko/assets/views/trafficlights.py
+++ b/loko/assets/views/trafficlights.py
@@ -4867,12 +4867,18 @@ def integrate_cross_plan(request, intersection_id):
from django.shortcuts import get_object_or_404, redirect
from django.contrib import messages
from django.db import transaction
- import json
+ from django.db.models import ProtectedError
from assets.models.trafficlights import (
TrafficLightIntersection, TrafficLightPole, TrafficLightLantern,
TrafficLightDetector, TrafficLightCable, TrafficLightPoleModel,
- TrafficLightLanternModel, TrafficLightDetectorModel, TrafficLightCableModel
+ TrafficLightLanternModel, TrafficLightDetectorModel, TrafficLightCableModel,
+ TrafficLightController, TrafficLightControllerHardware, TrafficLightNetworkHardware,
+ TrafficLightVoltageHardware, TrafficLightElectricalCabinet, TrafficLightAccessory,
+ TrafficLightRadar, TrafficLightProgramming
)
+ from assets.models.sign import SignPanel
+ from assets.models.its import ITSGeoAsset
+ from assets.models.publiclighting import PublicLightingLightFixture
intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id)
filename = request.POST.get('cross_plan_filename')
@@ -5406,6 +5412,18 @@ def integrate_cross_plan(request, intersection_id):
PublicLightingLightFixture.objects.filter(traffic_light_pole=existing).update(traffic_light_pole=new_pole)
except Exception:
pass
+ elif action == "archive" and existing:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ elif action == "delete" and existing:
+ try:
+ existing.delete()
+ except ProtectedError:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ messages.warning(request, f"Le poteau {existing.code} n'a pas pu être supprimé car d'autres éléments y font référence ; il a été archivé à la place.")
else:
# 'keep' (Ne rien faire)
if existing:
@@ -5424,6 +5442,17 @@ def integrate_cross_plan(request, intersection_id):
TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').update(
status='archived', updated_by=request.user
)
+ elif action == "delete":
+ db_id = request.POST.get(f"db_id_pole_{code}")
+ qs = TrafficLightPole.objects.filter(pk=db_id) if db_id else TrafficLightPole.objects.filter(intersection=intersection, code=code)
+ for pole_to_del in qs:
+ try:
+ pole_to_del.delete()
+ except ProtectedError:
+ pole_to_del.status = 'archived'
+ pole_to_del.updated_by = request.user
+ pole_to_del.save()
+ messages.warning(request, f"Le poteau {pole_to_del.code} n'a pas pu être supprimé car d'autres éléments y font référence ; il a été archivé à la place.")
# Fetch active poles to match parent relation for lanterns/detectors if not resolved above
active_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')}
@@ -5492,6 +5521,18 @@ def integrate_cross_plan(request, intersection_id):
new_cable.save()
existing.replaced_by = new_cable
existing.save()
+ elif action == "archive" and existing:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ elif action == "delete" and existing:
+ try:
+ existing.delete()
+ except ProtectedError:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ messages.warning(request, f"Le câble {existing.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
else:
# 'keep' (Ne rien faire)
if existing:
@@ -5509,6 +5550,17 @@ def integrate_cross_plan(request, intersection_id):
TrafficLightCable.objects.filter(intersection=intersection, code=code, status='active').update(
status='archived', updated_by=request.user
)
+ elif action == "delete":
+ db_id = request.POST.get(f"db_id_cable_{code}")
+ qs = TrafficLightCable.objects.filter(pk=db_id) if db_id else TrafficLightCable.objects.filter(intersection=intersection, code=code)
+ for cable_to_del in qs:
+ try:
+ cable_to_del.delete()
+ except ProtectedError:
+ cable_to_del.status = 'archived'
+ cable_to_del.updated_by = request.user
+ cable_to_del.save()
+ messages.warning(request, f"Le câble {cable_to_del.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
# --- 3. Lanterns integration ---
for dl in deduced["lanterns"]:
@@ -5570,6 +5622,18 @@ def integrate_cross_plan(request, intersection_id):
)
existing.replaced_by = new_lantern
existing.save()
+ elif action == "archive" and existing:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ elif action == "delete" and existing:
+ try:
+ existing.delete()
+ except ProtectedError:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ messages.warning(request, f"La lanterne {existing.code} n'a pas pu être supprimée car elle est référencée ; elle a été archivée à la place.")
else:
# 'keep'
if existing:
@@ -5589,6 +5653,17 @@ def integrate_cross_plan(request, intersection_id):
TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code, status='active').update(
status='archived', updated_by=request.user
)
+ elif action == "delete":
+ db_id = request.POST.get(f"db_id_lantern_{code}")
+ qs = TrafficLightLantern.objects.filter(pk=db_id) if db_id else TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code)
+ for lan_to_del in qs:
+ try:
+ lan_to_del.delete()
+ except ProtectedError:
+ lan_to_del.status = 'archived'
+ lan_to_del.updated_by = request.user
+ lan_to_del.save()
+ messages.warning(request, f"La lanterne {lan_to_del.code} n'a pas pu être supprimée car elle est référencée ; elle a été archivée à la place.")
# --- 4. Detectors integration ---
for dd in deduced["detectors"]:
@@ -5658,6 +5733,18 @@ def integrate_cross_plan(request, intersection_id):
)
existing.replaced_by = new_det
existing.save()
+ elif action == "archive" and existing:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ elif action == "delete" and existing:
+ try:
+ existing.delete()
+ except ProtectedError:
+ existing.status = 'archived'
+ existing.updated_by = request.user
+ existing.save()
+ messages.warning(request, f"Le détecteur {existing.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
else:
# 'keep'
if existing:
@@ -5678,6 +5765,17 @@ def integrate_cross_plan(request, intersection_id):
TrafficLightDetector.objects.filter(intersection=intersection, code=code, status='active').update(
status='archived', updated_by=request.user
)
+ elif action == "delete":
+ db_id = request.POST.get(f"db_id_detector_{code}")
+ qs = TrafficLightDetector.objects.filter(pk=db_id) if db_id else TrafficLightDetector.objects.filter(intersection=intersection, code=code)
+ for det_to_del in qs:
+ try:
+ det_to_del.delete()
+ except ProtectedError:
+ det_to_del.status = 'archived'
+ det_to_del.updated_by = request.user
+ det_to_del.save()
+ messages.warning(request, f"Le détecteur {det_to_del.code} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
# --- 5. Other associated assets integration ---
for key in request.POST.keys():
@@ -5687,36 +5785,39 @@ def integrate_cross_plan(request, intersection_id):
asset_type, asset_id_str = suffix.split("_", 1)
is_selected = request.POST.get(f"selected_other_{suffix}") == "on"
action = request.POST.get(key, "keep")
- if is_selected and action == "archive":
+ if is_selected and action in ("archive", "delete"):
try:
asset_id = int(asset_id_str)
- if asset_type == "signpanel":
- from assets.models.sign import SignPanel
- SignPanel.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "itsasset":
- from assets.models.its import ITSGeoAsset
- ITSGeoAsset.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "controller":
- TrafficLightController.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- from assets.models.trafficlights import TrafficLightControllerHardware
- TrafficLightControllerHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
- TrafficLightNetworkHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
- TrafficLightVoltageHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "networkhardware":
- TrafficLightNetworkHardware.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "voltagehardware":
- TrafficLightVoltageHardware.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "electricalcabinet":
- TrafficLightElectricalCabinet.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "accessory":
- TrafficLightAccessory.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "radar":
- TrafficLightRadar.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "programming":
- TrafficLightProgramming.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
- elif asset_type == "lightfixture":
- from assets.models.publiclighting import PublicLightingLightFixture
- PublicLightingLightFixture.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
+ model_map = {
+ "signpanel": SignPanel,
+ "itsasset": ITSGeoAsset,
+ "controller": TrafficLightController,
+ "networkhardware": TrafficLightNetworkHardware,
+ "voltagehardware": TrafficLightVoltageHardware,
+ "electricalcabinet": TrafficLightElectricalCabinet,
+ "accessory": TrafficLightAccessory,
+ "radar": TrafficLightRadar,
+ "programming": TrafficLightProgramming,
+ "lightfixture": PublicLightingLightFixture,
+ }
+ model_cls = model_map.get(asset_type)
+ if model_cls:
+ if action == "archive":
+ model_cls.objects.filter(pk=asset_id).update(status='archived', updated_by=request.user)
+ if asset_type == "controller":
+ TrafficLightControllerHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
+ TrafficLightNetworkHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
+ TrafficLightVoltageHardware.objects.filter(controller_id=asset_id).update(status='archived', updated_by=request.user)
+ elif action == "delete":
+ obj = model_cls.objects.filter(pk=asset_id).first()
+ if obj:
+ try:
+ obj.delete()
+ except ProtectedError:
+ obj.status = 'archived'
+ obj.updated_by = request.user
+ obj.save()
+ messages.warning(request, f"L'équipement {obj} n'a pas pu être supprimé car il est référencé ; il a été archivé à la place.")
except (ValueError, TypeError, Exception):
pass