feat: add traffic light cross-plan import functionality with file upload and data preview/integration support
This commit is contained in:
parent
442469f3e0
commit
ac75712828
3 changed files with 508 additions and 33 deletions
|
|
@ -215,6 +215,11 @@
|
|||
<i class="bi bi-bezier2 me-2"></i> {% translate "Câbles" %} <span class="badge bg-secondary ms-1">{{ cables|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link px-4 py-3 border-0 rounded-0" id="other-assets-tab" data-bs-toggle="tab" data-bs-target="#other-assets-pane" type="button" role="tab" aria-controls="other-assets-pane" aria-selected="false">
|
||||
<i class="bi bi-diagram-3 me-2"></i> {% translate "Autres équipements" %} <span class="badge bg-secondary ms-1">{{ other_assets|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -264,6 +269,11 @@
|
|||
{% elif item.status == 'db_only' %}
|
||||
<span class="badge bg-danger">{% translate "DB uniquement" %}</span>
|
||||
{% endif %}
|
||||
{% for diff in item.details_diff %}
|
||||
<div class="small text-muted mt-1" style="font-size: 0.75rem; line-height: 1.25;">
|
||||
<i class="bi bi-info-circle me-1"></i>{{ diff }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
||||
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||
|
|
@ -292,7 +302,13 @@
|
|||
<td class="pe-4">
|
||||
{% if item.status != 'db_only' %}
|
||||
<select name="model_pole_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||
<option value="">-- {% translate "Laisser par défaut" %} --</option>
|
||||
<option value="">
|
||||
{% if item.status == 'new' or item.status == 'modified' %}
|
||||
-- {% translate "Modèle déduit" %} --
|
||||
{% else %}
|
||||
-- {% translate "Modèle actuel" %} --
|
||||
{% endif %}
|
||||
</option>
|
||||
{% for m in pole_models %}
|
||||
<option value="{{ m.id }}">{{ m.name_fr }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -348,6 +364,11 @@
|
|||
{% elif item.status == 'db_only' %}
|
||||
<span class="badge bg-danger">{% translate "DB uniquement" %}</span>
|
||||
{% endif %}
|
||||
{% for diff in item.details_diff %}
|
||||
<div class="small text-muted mt-1" style="font-size: 0.75rem; line-height: 1.25;">
|
||||
<i class="bi bi-info-circle me-1"></i>{{ diff }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
||||
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||
|
|
@ -376,7 +397,13 @@
|
|||
<td class="pe-4">
|
||||
{% if item.status != 'db_only' %}
|
||||
<select name="model_lantern_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||
<option value="">-- {% translate "Laisser par défaut" %} --</option>
|
||||
<option value="">
|
||||
{% if item.status == 'new' or item.status == 'modified' %}
|
||||
-- {% translate "Modèle déduit" %} --
|
||||
{% else %}
|
||||
-- {% translate "Modèle actuel" %} --
|
||||
{% endif %}
|
||||
</option>
|
||||
{% for m in lantern_models %}
|
||||
<option value="{{ m.id }}">{{ m.name_fr }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -432,6 +459,11 @@
|
|||
{% elif item.status == 'db_only' %}
|
||||
<span class="badge bg-danger">{% translate "DB uniquement" %}</span>
|
||||
{% endif %}
|
||||
{% for diff in item.details_diff %}
|
||||
<div class="small text-muted mt-1" style="font-size: 0.75rem; line-height: 1.25;">
|
||||
<i class="bi bi-info-circle me-1"></i>{{ diff }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
||||
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||
|
|
@ -460,7 +492,13 @@
|
|||
<td class="pe-4">
|
||||
{% if item.status != 'db_only' %}
|
||||
<select name="model_detector_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||
<option value="">-- {% translate "Laisser par défaut" %} --</option>
|
||||
<option value="">
|
||||
{% if item.status == 'new' or item.status == 'modified' %}
|
||||
-- {% translate "Modèle déduit" %} --
|
||||
{% else %}
|
||||
-- {% translate "Modèle actuel" %} --
|
||||
{% endif %}
|
||||
</option>
|
||||
{% for m in detector_models %}
|
||||
<option value="{{ m.id }}">{{ m.name_fr }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -484,7 +522,7 @@
|
|||
<div class="tab-pane fade" id="cables-pane" role="tabpanel" aria-labelledby="cables-tab">
|
||||
<div class="p-3 bg-light border-bottom d-flex align-items-center justify-content-between">
|
||||
<span class="text-secondary small">
|
||||
<i class="bi bi-info-circle"></i> {% translate "Vous pouvez tracer précisément les câbles d'interconnexion le long des tranchées de la carte." %}
|
||||
<i class="bi bi-info-circle"></i> {% translate "Vous pouvez tracer précisément les câbles d'interconnexion le long des tranchées de la carte." %}
|
||||
</span>
|
||||
<button type="button" class="btn btn-outline-success btn-sm rounded-3 show-map-btn" data-target-tab="cables">
|
||||
<i class="bi bi-bezier2 me-1"></i> {% translate "Tracer les câbles sur la carte" %}
|
||||
|
|
@ -522,6 +560,11 @@
|
|||
{% elif item.status == 'db_only' %}
|
||||
<span class="badge bg-danger">{% translate "DB uniquement" %}</span>
|
||||
{% endif %}
|
||||
{% for diff in item.details_diff %}
|
||||
<div class="small text-muted mt-1" style="font-size: 0.75rem; line-height: 1.25;">
|
||||
<i class="bi bi-info-circle me-1"></i>{{ diff }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
||||
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||
|
|
@ -550,7 +593,13 @@
|
|||
<td class="pe-4">
|
||||
{% if item.status != 'db_only' %}
|
||||
<select name="model_cable_{{ item.code }}" class="form-select form-select-sm rounded-2">
|
||||
<option value="">-- {% translate "Laisser par défaut" %} --</option>
|
||||
<option value="">
|
||||
{% if item.status == 'new' or item.status == 'modified' %}
|
||||
-- {% translate "Modèle déduit" %} --
|
||||
{% else %}
|
||||
-- {% translate "Modèle actuel" %} --
|
||||
{% endif %}
|
||||
</option>
|
||||
{% for m in cable_models %}
|
||||
<option value="{{ m.id }}">{{ m.name_fr }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -562,7 +611,74 @@
|
|||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">{% translate "Aucun câble détecté." %}</td>
|
||||
<td colspan="8" class="text-center py-4 text-muted">{% translate "Aucun câble détecté." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. OTHER ASSOCIATED ASSETS PANEL -->
|
||||
<div class="tab-pane fade" id="other-assets-pane" role="tabpanel" aria-labelledby="other-assets-tab">
|
||||
<div class="p-3 bg-light border-bottom d-flex align-items-center justify-content-between">
|
||||
<span class="text-secondary small">
|
||||
<i class="bi bi-info-circle"></i> {% translate "Équipements associés existants en base (panneaux, caméras ITS, contrôleurs, armoires, etc.) non décrits dans le plan de croix Excel. Par défaut, ils sont conservés." %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-4" style="width: 40px;"><input type="checkbox" id="select-all-other" class="form-check-input" checked></th>
|
||||
<th>{% translate "Catégorie" %}</th>
|
||||
<th>{% translate "Code Asset" %}</th>
|
||||
<th>{% translate "Nom / Modèle" %}</th>
|
||||
<th>{% translate "Support / Liaison" %}</th>
|
||||
<th>{% translate "Statut DB" %}</th>
|
||||
<th class="pe-4">{% translate "Décision" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in other_assets %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<input type="checkbox" name="selected_other_{{ item.unique_key }}" class="form-check-input other-checkbox" checked>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ item.badge_class }}">{{ item.category_label }}</span>
|
||||
</td>
|
||||
<td class="font-weight-medium">
|
||||
<code>{{ item.code }}</code>
|
||||
<input type="hidden" name="id_other_{{ item.unique_key }}" value="{{ item.id }}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ item.name }}</div>
|
||||
{% if item.model_name and item.model_name != "—" %}
|
||||
<div class="small text-muted">{{ item.model_name }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ item.support }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-secondary border">{% translate "En base (Actif)" %}</span>
|
||||
{% for diff in item.details_diff %}
|
||||
<div class="small text-muted mt-1" style="font-size: 0.75rem; line-height: 1.25;">
|
||||
<i class="bi bi-info-circle me-1"></i>{{ diff }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="pe-4">
|
||||
<select name="action_other_{{ item.unique_key }}" class="form-select form-select-sm rounded-2">
|
||||
<option value="keep" selected>{% translate "Conserver (Par défaut)" %}</option>
|
||||
<option value="archive">{% translate "Archiver" %}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">{% translate "Aucun autre équipement associé en base pour ce carrefour." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -883,6 +999,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
bindSelectAll('select-all-lanterns', 'lantern-checkbox');
|
||||
bindSelectAll('select-all-detectors', 'detector-checkbox');
|
||||
bindSelectAll('select-all-cables', 'cable-checkbox');
|
||||
bindSelectAll('select-all-other', 'other-checkbox');
|
||||
|
||||
// 2. Interactive Switch Logic between Table View and Map View
|
||||
const tableView = document.getElementById('table-view');
|
||||
|
|
|
|||
|
|
@ -1323,6 +1323,90 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
|||
self.assertIsNotNone(new_active)
|
||||
self.assertEqual(old_lantern.replaced_by, new_active)
|
||||
|
||||
def test_other_associated_assets_preview_and_integration(self):
|
||||
from assets.models import TrafficLightPole, TrafficLightController, TrafficLightControllerHardware, TrafficLightAccessory
|
||||
from assets.models.sign import SignPanel
|
||||
from assets.models.its import ITSGeoAsset
|
||||
|
||||
pole = TrafficLightPole.objects.create(
|
||||
intersection=self.intersection,
|
||||
code="SWB01_A01",
|
||||
status="active"
|
||||
)
|
||||
ctrl = TrafficLightController.objects.create(
|
||||
intersection=self.intersection,
|
||||
code="SWB01_C01",
|
||||
status="active"
|
||||
)
|
||||
hw = TrafficLightControllerHardware.objects.create(
|
||||
controller=ctrl,
|
||||
code="SWB01_HW_1",
|
||||
status="active"
|
||||
)
|
||||
sign = SignPanel.objects.create(
|
||||
trafficlight_pole=pole,
|
||||
code="SWB01_SP01",
|
||||
status="active"
|
||||
)
|
||||
its = ITSGeoAsset.objects.create(
|
||||
trafficlights_pole=pole,
|
||||
code="SWB01_ITS01",
|
||||
status="active"
|
||||
)
|
||||
acc = TrafficLightAccessory.objects.create(
|
||||
intersection=self.intersection,
|
||||
pole=pole,
|
||||
code="SWB01_ACC01",
|
||||
status="active"
|
||||
)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
real_excel_path = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files', '18ac47df841-2808f-DOC-23270.xlsx')
|
||||
if not os.path.exists(real_excel_path):
|
||||
self.skipTest("Real Excel file for test not found")
|
||||
|
||||
with open(real_excel_path, 'rb') as f:
|
||||
excel_content = f.read()
|
||||
|
||||
uploaded_file = SimpleUploadedFile("test_cross_plan.xlsx", excel_content, 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)
|
||||
self.assertIn("other_assets", response.context)
|
||||
other_assets = response.context["other_assets"]
|
||||
self.assertTrue(len(other_assets) >= 4)
|
||||
|
||||
# Test selective integration: archive controller, keep sign panel, replace pole
|
||||
url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id])
|
||||
response_integrate = self.client.post(url_integrate, {
|
||||
"cross_plan_filename": response.context["cross_plan_filename"],
|
||||
f"selected_other_controller_{ctrl.id}": "on",
|
||||
f"action_other_controller_{ctrl.id}": "archive",
|
||||
f"selected_other_signpanel_{sign.id}": "on",
|
||||
f"action_other_signpanel_{sign.id}": "keep",
|
||||
"selected_pole_SWB01_A01": "on",
|
||||
"action_pole_SWB01_A01": "replace",
|
||||
"db_id_pole_SWB01_A01": str(pole.id)
|
||||
})
|
||||
self.assertEqual(response_integrate.status_code, 302)
|
||||
|
||||
ctrl.refresh_from_db()
|
||||
self.assertEqual(ctrl.status, "archived")
|
||||
hw.refresh_from_db()
|
||||
self.assertEqual(hw.status, "archived")
|
||||
|
||||
sign.refresh_from_db()
|
||||
self.assertEqual(sign.status, "active")
|
||||
pole.refresh_from_db()
|
||||
self.assertEqual(pole.status, "archived")
|
||||
# Sign panel should have been re-linked to new replacement pole
|
||||
self.assertEqual(sign.trafficlight_pole, pole.replaced_by)
|
||||
|
||||
def test_parse_telecom_detectors(self):
|
||||
import openpyxl
|
||||
import tempfile
|
||||
|
|
|
|||
|
|
@ -4080,17 +4080,17 @@ def import_cross_plan(request, intersection_id):
|
|||
|
||||
if code in existing_poles:
|
||||
ep = existing_poles[code]
|
||||
is_diff = ep.model_id != db_model.id
|
||||
status = 'modified' if is_diff else 'identical'
|
||||
# Since cross plans do not specify pole physical models (height, color), keep existing DB model
|
||||
status = 'identical'
|
||||
compared_poles.append({
|
||||
'code': code,
|
||||
'short_code': dp['short_code'],
|
||||
'name': ep.name_fr or dp.get('name', dp['short_code']),
|
||||
'status': status,
|
||||
'deduced_model_name': model_name,
|
||||
'deduced_model_name': ep.model.name_fr if ep.model else model_name,
|
||||
'db_model_name': ep.model.name_fr if ep.model else '',
|
||||
'db_id': ep.id,
|
||||
'details_diff': f"Modèle différent (DB: {ep.model.name_fr if ep.model else 'Aucun'} vs Excel: {model_name})" if is_diff else ''
|
||||
'details_diff': []
|
||||
})
|
||||
# remove from processed
|
||||
existing_poles.pop(code)
|
||||
|
|
@ -4103,7 +4103,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'deduced_model_name': model_name,
|
||||
'db_model_name': '',
|
||||
'db_id': None,
|
||||
'details_diff': ''
|
||||
'details_diff': []
|
||||
})
|
||||
# Add remaining existing active poles (orphans)
|
||||
for code, ep in existing_poles.items():
|
||||
|
|
@ -4115,7 +4115,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'deduced_model_name': '',
|
||||
'db_model_name': ep.model.name_fr if ep.model else '',
|
||||
'db_id': ep.id,
|
||||
'details_diff': _("Absent du fichier Excel (Sera conservé ou archivé)")
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")]
|
||||
})
|
||||
|
||||
# ── 2. CABLES COMPARISON ──
|
||||
|
|
@ -4137,7 +4137,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'deduced_model_name': model_name,
|
||||
'db_model_name': ec.model.name_fr if ec.model else '',
|
||||
'db_id': ec.id,
|
||||
'details_diff': f"Modèle différent (DB: {ec.model.name_fr if ec.model else 'Aucun'} vs Excel: {model_name})" if is_diff else '',
|
||||
'details_diff': [f"Modèle : {ec.model.name_fr if ec.model else 'Aucun'} → {model_name}"] if is_diff else [],
|
||||
'connected_poles': dc.get("connected_poles", [])
|
||||
})
|
||||
existing_cables.pop(code)
|
||||
|
|
@ -4149,7 +4149,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'deduced_model_name': model_name,
|
||||
'db_model_name': '',
|
||||
'db_id': None,
|
||||
'details_diff': '',
|
||||
'details_diff': [],
|
||||
'connected_poles': dc.get("connected_poles", [])
|
||||
})
|
||||
for code, ec in existing_cables.items():
|
||||
|
|
@ -4160,7 +4160,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'deduced_model_name': '',
|
||||
'db_model_name': ec.model.name_fr if ec.model else '',
|
||||
'db_id': ec.id,
|
||||
'details_diff': _("Absent du fichier Excel")
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")]
|
||||
})
|
||||
|
||||
# ── 3. LANTERNS COMPARISON ──
|
||||
|
|
@ -4187,9 +4187,9 @@ def import_cross_plan(request, intersection_id):
|
|||
|
||||
details = []
|
||||
if is_rename:
|
||||
details.append(f"Renommer le code (DB: {el.code} vs Excel: {code})")
|
||||
details.append(f"Code : {el.code} → {code}")
|
||||
if is_model_diff:
|
||||
details.append(f"Modèle différent (DB: {el.model.name_fr if el.model else 'Aucun'} vs Excel: {model_name})")
|
||||
details.append(f"Modèle : {el.model.name_fr if el.model else 'Aucun'} → {model_name}")
|
||||
|
||||
compared_lanterns.append({
|
||||
'code': code,
|
||||
|
|
@ -4199,7 +4199,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': el.model.name_fr if el.model else '',
|
||||
'db_id': el.id,
|
||||
'pole_code': dl['pole_code'],
|
||||
'details_diff': ", ".join(details) if details else ''
|
||||
'details_diff': details
|
||||
})
|
||||
existing_lanterns.pop(match_key)
|
||||
else:
|
||||
|
|
@ -4211,7 +4211,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': '',
|
||||
'db_id': None,
|
||||
'pole_code': dl['pole_code'],
|
||||
'details_diff': ''
|
||||
'details_diff': []
|
||||
})
|
||||
for code, el in existing_lanterns.items():
|
||||
compared_lanterns.append({
|
||||
|
|
@ -4222,7 +4222,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': el.model.name_fr if el.model else '',
|
||||
'db_id': el.id,
|
||||
'pole_code': el.pole.code,
|
||||
'details_diff': _("Absent du fichier Excel")
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")]
|
||||
})
|
||||
|
||||
# ── 4. DETECTORS COMPARISON ──
|
||||
|
|
@ -4248,9 +4248,9 @@ def import_cross_plan(request, intersection_id):
|
|||
is_diff = ed.model_id != db_model.id
|
||||
details = []
|
||||
if match_key != code:
|
||||
details.append(f"Renommer le code (DB: {match_key} vs Excel: {code})")
|
||||
details.append(f"Code : {match_key} → {code}")
|
||||
if is_diff:
|
||||
details.append(f"Modèle différent (DB: {ed.model.name_fr if ed.model else 'Aucun'} vs Excel: {model_name})")
|
||||
details.append(f"Modèle : {ed.model.name_fr if ed.model else 'Aucun'} → {model_name}")
|
||||
|
||||
status = 'modified' if (is_diff or match_key != code) else 'identical'
|
||||
compared_detectors.append({
|
||||
|
|
@ -4261,7 +4261,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': ed.model.name_fr if ed.model else '',
|
||||
'db_id': ed.id,
|
||||
'pole_code': dd['pole_code'],
|
||||
'details_diff': ", ".join(details) if details else ''
|
||||
'details_diff': details
|
||||
})
|
||||
existing_detectors.pop(match_key)
|
||||
else:
|
||||
|
|
@ -4273,7 +4273,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': '',
|
||||
'db_id': None,
|
||||
'pole_code': dd['pole_code'],
|
||||
'details_diff': ''
|
||||
'details_diff': []
|
||||
})
|
||||
for code, ed in existing_detectors.items():
|
||||
compared_detectors.append({
|
||||
|
|
@ -4284,9 +4284,212 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': ed.model.name_fr if ed.model else '',
|
||||
'db_id': ed.id,
|
||||
'pole_code': ed.pole.code if ed.pole else '',
|
||||
'details_diff': _("Absent du fichier Excel")
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")]
|
||||
})
|
||||
|
||||
# ── 5. OTHER ASSOCIATED ASSETS (NOT IN CROSS PLAN) ──
|
||||
other_assets = []
|
||||
try:
|
||||
from assets.models.sign import SignPanel
|
||||
for sp in SignPanel.objects.filter(trafficlight_pole__intersection=intersection, status='active').select_related('model', 'signpanel_type', 'trafficlight_pole'):
|
||||
model_display = sp.model.name_fr if sp.model else (sp.signpanel_type.name_fr if sp.signpanel_type else (sp.signpanel_text or "—"))
|
||||
other_assets.append({
|
||||
'type': 'signpanel',
|
||||
'category_label': _("Panneau de signalisation"),
|
||||
'badge_class': 'bg-info text-dark',
|
||||
'id': sp.id,
|
||||
'unique_key': f"signpanel_{sp.id}",
|
||||
'code': sp.code or f"SP_{sp.id}",
|
||||
'name': sp.name_fr or sp.code or _("Panneau"),
|
||||
'model_name': model_display,
|
||||
'support': sp.trafficlight_pole.code if sp.trafficlight_pole else _("Poteau"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB (attaché au poteau)")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
from assets.models.its import ITSGeoAsset
|
||||
for its in ITSGeoAsset.objects.filter(trafficlights_pole__intersection=intersection, status='active').select_related('model', 'trafficlights_pole'):
|
||||
other_assets.append({
|
||||
'type': 'itsasset',
|
||||
'category_label': _("Caméra ANPR / ITS"),
|
||||
'badge_class': 'bg-primary',
|
||||
'id': its.id,
|
||||
'unique_key': f"itsasset_{its.id}",
|
||||
'code': its.code or f"ITS_{its.id}",
|
||||
'name': its.name_fr or its.code or _("Équipement ITS"),
|
||||
'model_name': its.model.name_fr if its.model else "—",
|
||||
'support': its.trafficlights_pole.code if its.trafficlights_pole else _("Poteau"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB (attaché au poteau)")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for c in TrafficLightController.objects.filter(intersection=intersection, status='active').select_related('model'):
|
||||
hw_count = c.hardwares.filter(status='active').count()
|
||||
diff_note = _("Présent en DB")
|
||||
if hw_count > 0:
|
||||
diff_note += f" ({hw_count} hardwares)"
|
||||
other_assets.append({
|
||||
'type': 'controller',
|
||||
'category_label': _("Contrôleur"),
|
||||
'badge_class': 'bg-secondary',
|
||||
'id': c.id,
|
||||
'unique_key': f"controller_{c.id}",
|
||||
'code': c.code or f"CTRL_{c.id}",
|
||||
'name': c.name_fr or c.code or _("Contrôleur"),
|
||||
'model_name': c.model.name_fr if c.model else "—",
|
||||
'support': _("Carrefour"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [diff_note],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for nh in TrafficLightNetworkHardware.objects.filter(intersection=intersection, controller__isnull=True, status='active'):
|
||||
other_assets.append({
|
||||
'type': 'networkhardware',
|
||||
'category_label': _("Matériel Réseau"),
|
||||
'badge_class': 'bg-dark',
|
||||
'id': nh.id,
|
||||
'unique_key': f"networkhardware_{nh.id}",
|
||||
'code': nh.code or f"NH_{nh.id}",
|
||||
'name': nh.name or nh.hardware or _("Matériel réseau"),
|
||||
'model_name': nh.hardware or "—",
|
||||
'support': _("Carrefour"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB (autonome)")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for vh in TrafficLightVoltageHardware.objects.filter(intersection=intersection, controller__isnull=True, status='active'):
|
||||
other_assets.append({
|
||||
'type': 'voltagehardware',
|
||||
'category_label': _("Matériel Tension"),
|
||||
'badge_class': 'bg-warning text-dark',
|
||||
'id': vh.id,
|
||||
'unique_key': f"voltagehardware_{vh.id}",
|
||||
'code': vh.code or f"VH_{vh.id}",
|
||||
'name': vh.name or vh.hardware or _("Matériel tension"),
|
||||
'model_name': vh.hardware or "—",
|
||||
'support': vh.controller.code if vh.controller else _("Carrefour"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB (lié au contrôleur)")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for ec in TrafficLightElectricalCabinet.objects.filter(intersection=intersection, status='active').select_related('model'):
|
||||
other_assets.append({
|
||||
'type': 'electricalcabinet',
|
||||
'category_label': _("Armoire électrique"),
|
||||
'badge_class': 'bg-secondary',
|
||||
'id': ec.id,
|
||||
'unique_key': f"electricalcabinet_{ec.id}",
|
||||
'code': ec.code or f"CAB_{ec.id}",
|
||||
'name': ec.name_fr or ec.code or _("Armoire"),
|
||||
'model_name': ec.model.name_fr if ec.model else (ec.ean_code or "—"),
|
||||
'support': _("Carrefour"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for acc in TrafficLightAccessory.objects.filter(intersection=intersection, status='active').select_related('model', 'pole'):
|
||||
other_assets.append({
|
||||
'type': 'accessory',
|
||||
'category_label': _("Accessoire"),
|
||||
'badge_class': 'bg-light text-dark border',
|
||||
'id': acc.id,
|
||||
'unique_key': f"accessory_{acc.id}",
|
||||
'code': acc.code or f"ACC_{acc.id}",
|
||||
'name': acc.name_fr or acc.code or _("Accessoire"),
|
||||
'model_name': acc.model.name_fr if acc.model else "—",
|
||||
'support': acc.pole.code if acc.pole else _("Carrefour"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for rad in TrafficLightRadar.objects.filter(intersection=intersection, status='active').select_related('model', 'trafficlights_pole', 'pole'):
|
||||
sup = rad.trafficlights_pole.code if rad.trafficlights_pole else (rad.pole.code if rad.pole else _("Carrefour"))
|
||||
other_assets.append({
|
||||
'type': 'radar',
|
||||
'category_label': _("Radar"),
|
||||
'badge_class': 'bg-danger',
|
||||
'id': rad.id,
|
||||
'unique_key': f"radar_{rad.id}",
|
||||
'code': rad.code or f"RAD_{rad.id}",
|
||||
'name': rad.name_fr or rad.code or _("Radar"),
|
||||
'model_name': rad.model.name_fr if rad.model else "—",
|
||||
'support': sup,
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for prog in TrafficLightProgramming.objects.filter(intersection=intersection, status='active'):
|
||||
other_assets.append({
|
||||
'type': 'programming',
|
||||
'category_label': _("Programmation"),
|
||||
'badge_class': 'bg-info text-dark',
|
||||
'id': prog.id,
|
||||
'unique_key': f"programming_{prog.id}",
|
||||
'code': prog.code or f"PROG_{prog.id}",
|
||||
'name': prog.plan or prog.index or _("Programmation"),
|
||||
'model_name': prog.index or "—",
|
||||
'support': _("Carrefour"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
from assets.models.publiclighting import PublicLightingLightFixture
|
||||
for lf in PublicLightingLightFixture.objects.filter(traffic_light_pole__intersection=intersection, status='active').select_related('model', 'traffic_light_pole'):
|
||||
other_assets.append({
|
||||
'type': 'lightfixture',
|
||||
'category_label': _("Luminaire EP"),
|
||||
'badge_class': 'bg-warning text-dark',
|
||||
'id': lf.id,
|
||||
'unique_key': f"lightfixture_{lf.id}",
|
||||
'code': lf.code or f"LF_{lf.id}",
|
||||
'name': lf.name_fr or lf.code or _("Luminaire"),
|
||||
'model_name': lf.model.name_fr if lf.model else "—",
|
||||
'support': lf.traffic_light_pole.code if lf.traffic_light_pole else _("Poteau"),
|
||||
'status': 'db_only',
|
||||
'details_diff': [_("Présent en DB (attaché au poteau)")],
|
||||
'action_default': 'keep',
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
other_assets.sort(key=lambda x: (x['category_label'], x['code']))
|
||||
|
||||
# Sort compared lists alphabetically by code for preview
|
||||
compared_poles.sort(key=lambda x: x['code'])
|
||||
compared_cables.sort(key=lambda x: x['code'])
|
||||
|
|
@ -4308,6 +4511,7 @@ def import_cross_plan(request, intersection_id):
|
|||
'cables': compared_cables,
|
||||
'lanterns': compared_lanterns,
|
||||
'detectors': compared_detectors,
|
||||
'other_assets': other_assets,
|
||||
'pole_models': pole_models,
|
||||
'lantern_models': lantern_models,
|
||||
'detector_models': detector_models,
|
||||
|
|
@ -4479,6 +4683,22 @@ def integrate_cross_plan(request, intersection_id):
|
|||
p.save()
|
||||
created_poles[dp["code"]] = p
|
||||
|
||||
# Re-link existing active attached assets (signs, ITS, accessories, radars, light fixtures) to new poles matching code
|
||||
try:
|
||||
from assets.models.sign import SignPanel
|
||||
from assets.models.its import ITSGeoAsset
|
||||
from assets.models.publiclighting import PublicLightingLightFixture
|
||||
from assets.models.trafficlights import TrafficLightAccessory, TrafficLightRadar
|
||||
|
||||
for code, new_pole in created_poles.items():
|
||||
SignPanel.objects.filter(trafficlight_pole__intersection=intersection, trafficlight_pole__code=code, status='active').update(trafficlight_pole=new_pole)
|
||||
ITSGeoAsset.objects.filter(trafficlights_pole__intersection=intersection, trafficlights_pole__code=code, status='active').update(trafficlights_pole=new_pole)
|
||||
TrafficLightAccessory.objects.filter(intersection=intersection, pole__code=code, status='active').update(pole=new_pole)
|
||||
TrafficLightRadar.objects.filter(intersection=intersection, trafficlights_pole__code=code, status='active').update(trafficlights_pole=new_pole)
|
||||
PublicLightingLightFixture.objects.filter(traffic_light_pole__intersection=intersection, traffic_light_pole__code=code, status='active').update(traffic_light_pole=new_pole)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Re-create all cables
|
||||
for dc in deduced["cables"]:
|
||||
db_model = resolve_cable_model(dc["model_name"])
|
||||
|
|
@ -4545,14 +4765,6 @@ def integrate_cross_plan(request, intersection_id):
|
|||
action = request.POST.get(f"action_pole_{code}", "keep")
|
||||
else:
|
||||
action = "keep"
|
||||
selected_model_id = request.POST.get(f"model_pole_{code}")
|
||||
|
||||
db_model = None
|
||||
if selected_model_id:
|
||||
db_model = TrafficLightPoleModel.objects.filter(pk=selected_model_id).first()
|
||||
if not db_model:
|
||||
db_model = resolve_pole_model(dp["model"])
|
||||
|
||||
db_id = request.POST.get(f"db_id_pole_{code}")
|
||||
existing = None
|
||||
if db_id:
|
||||
|
|
@ -4560,6 +4772,16 @@ def integrate_cross_plan(request, intersection_id):
|
|||
if not existing:
|
||||
existing = TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').first()
|
||||
|
||||
selected_model_id = request.POST.get(f"model_pole_{code}")
|
||||
db_model = None
|
||||
if selected_model_id:
|
||||
db_model = TrafficLightPoleModel.objects.filter(pk=selected_model_id).first()
|
||||
if not db_model:
|
||||
if existing and existing.model:
|
||||
db_model = existing.model
|
||||
else:
|
||||
db_model = resolve_pole_model(dp["model"])
|
||||
|
||||
custom_name = request.POST.get(f"name_pole_{code}", dp.get('name', dp['short_code']))
|
||||
if action == "add" or (action == "replace" and not existing):
|
||||
p = TrafficLightPole(
|
||||
|
|
@ -4603,6 +4825,17 @@ def integrate_cross_plan(request, intersection_id):
|
|||
existing.replaced_by = new_pole
|
||||
existing.save()
|
||||
created_poles[code] = new_pole
|
||||
try:
|
||||
from assets.models.sign import SignPanel
|
||||
from assets.models.its import ITSGeoAsset
|
||||
from assets.models.publiclighting import PublicLightingLightFixture
|
||||
SignPanel.objects.filter(trafficlight_pole=existing).update(trafficlight_pole=new_pole)
|
||||
ITSGeoAsset.objects.filter(trafficlights_pole=existing).update(trafficlights_pole=new_pole)
|
||||
TrafficLightAccessory.objects.filter(pole=existing).update(pole=new_pole)
|
||||
TrafficLightRadar.objects.filter(trafficlights_pole=existing).update(trafficlights_pole=new_pole)
|
||||
PublicLightingLightFixture.objects.filter(traffic_light_pole=existing).update(traffic_light_pole=new_pole)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# 'keep'
|
||||
if existing:
|
||||
|
|
@ -4867,6 +5100,47 @@ def integrate_cross_plan(request, intersection_id):
|
|||
status='archived', updated_by=request.user
|
||||
)
|
||||
|
||||
# --- 5. Other associated assets integration ---
|
||||
for key in request.POST.keys():
|
||||
if key.startswith("action_other_"):
|
||||
suffix = key.replace("action_other_", "")
|
||||
if "_" in suffix:
|
||||
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":
|
||||
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)
|
||||
except (ValueError, TypeError, Exception):
|
||||
pass
|
||||
|
||||
messages.success(request, "Les modifications d'assets ont été appliquées avec succès.")
|
||||
|
||||
# Cleanup parsed file
|
||||
|
|
|
|||
Loading…
Reference in a new issue