diff --git a/loko/assets/templates/assets/trafficlights_import_cross_plan.html b/loko/assets/templates/assets/trafficlights_import_cross_plan.html
index 665686d..5cef657 100644
--- a/loko/assets/templates/assets/trafficlights_import_cross_plan.html
+++ b/loko/assets/templates/assets/trafficlights_import_cross_plan.html
@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
+{% load l10n %}
{% load static %}
{% block content %}
@@ -147,10 +148,10 @@
{% for item in poles %}
{% if item.status != 'db_only' %}
-
-
-
-
+
+
+
+
{% endif %}
{% endfor %}
@@ -187,11 +188,22 @@
{% translate "Tout décocher" %}
+
+
+
+
+
@@ -253,6 +265,7 @@
{% translate "Code Asset" %} |
{% translate "Nom" %} |
{% translate "Statut" %} |
+ {% translate "Position / Géométrie" %} |
{% translate "Modèle Déduit (Excel)" %} |
{% translate "Modèle Actuel (DB)" %} |
{% translate "Décision" %} |
@@ -269,7 +282,7 @@
{% if item.status == 'new' %}
- {% translate "Nouveau" %}
+ {% translate "Nouveau" %}
{% elif item.status == 'modified' %}
{% translate "Modifié" %}
{% elif item.status == 'identical' %}
@@ -283,6 +296,45 @@
{% endfor %}
|
+
+ {% if item.geom_modified %}
+
+
+ {% if item.distance_diff is not None %}
+ {% translate "Décalage" %} {{ item.distance_diff }} m
+ {% else %}
+ {% translate "Position modifiée" %}
+ {% endif %}
+
+
+ {% elif item.new_lon is not None %}
+ {% if item.status == 'new' %}
+
+ {% if item.new_x_3812 %}DXF{% else %}{% translate "Plan" %}{% endif %}
+
+ {% elif item.status == 'db_only' %}
+
+ DB
+
+ {% else %}
+
+ {% translate "Inchangée" %}
+
+ {% endif %}
+
+ {% else %}
+
+ {% translate "Non positionné" %}
+
+
+ {% endif %}
+ |
{{ item.deduced_model_name|default:"—" }} |
{{ item.db_model_name|default:"—" }} |
@@ -302,6 +354,7 @@
{% elif item.status == 'identical' %}
+
{% endif %}
@@ -328,7 +381,7 @@
{% empty %}
|
- | {% translate "Aucun poteau détecté." %} |
+ {% translate "Aucun poteau détecté." %} |
{% endfor %}
@@ -544,6 +597,7 @@
{% translate "Code Asset" %} |
{% translate "Nom" %} |
{% translate "Statut" %} |
+ {% translate "Géométrie (DB)" %} |
{% translate "Modèle Déduit (Excel)" %} |
{% translate "Modèle Actuel (DB)" %} |
{% translate "Décision" %} |
@@ -574,6 +628,15 @@
{% endfor %}
+
+ {% if item.has_db_geom %}
+
+ {{ item.db_geom_length }} m
+
+ {% else %}
+ {% translate "Non tracé" %}
+ {% endif %}
+ |
{{ item.deduced_model_name|default:"—" }} |
{{ item.db_model_name|default:"—" }} |
@@ -593,6 +656,7 @@
{% elif item.status == 'identical' %}
+
{% endif %}
@@ -619,7 +683,7 @@
{% empty %}
|
- | {% translate "Aucun câble détecté." %} |
+ {% translate "Aucun câble détecté." %} |
{% endfor %}
@@ -984,6 +1048,11 @@
document.getElementById('global_action_input').value = 'replace_all';
document.getElementById('combined-import-form').submit();
}
+ } else if (actionType === 'update_all') {
+ if (confirm("{% translate 'Voulez-vous mettre à jour les assets existants sans les archiver, et créer les nouveaux assets présents dans le plan ? Les géométries et modèles modifiés seront appliqués.' %}")) {
+ document.getElementById('global_action_input').value = 'update_all';
+ document.getElementById('combined-import-form').submit();
+ }
}
}
@@ -1031,6 +1100,45 @@ document.addEventListener('DOMContentLoaded', () => {
btnUncheckAllGlobal.addEventListener('click', () => setAllCheckboxesState(false));
}
+ // Batch Decision Buttons (Tout Mettre à jour / Tout Remplacer)
+ const btnBatchUpdate = document.getElementById('btn-batch-update');
+ if (btnBatchUpdate) {
+ btnBatchUpdate.addEventListener('click', () => {
+ const allDecisionSelects = document.querySelectorAll('select[name^="action_pole_"], select[name^="action_cable_"], select[name^="action_lantern_"], select[name^="action_detector_"]');
+ allDecisionSelects.forEach(sel => {
+ if (sel.name.includes('_db_')) {
+ if (sel.querySelector('option[value="keep"]')) sel.value = 'keep';
+ } else {
+ if (sel.querySelector('option[value="modify"]')) {
+ sel.value = 'modify';
+ } else if (sel.querySelector('option[value="add"]')) {
+ sel.value = 'add';
+ } else if (sel.querySelector('option[value="keep"]')) {
+ sel.value = 'keep';
+ }
+ }
+ });
+ });
+ }
+
+ const btnBatchReplace = document.getElementById('btn-batch-replace');
+ if (btnBatchReplace) {
+ btnBatchReplace.addEventListener('click', () => {
+ const allDecisionSelects = document.querySelectorAll('select[name^="action_pole_"], select[name^="action_cable_"], select[name^="action_lantern_"], select[name^="action_detector_"]');
+ allDecisionSelects.forEach(sel => {
+ if (sel.name.includes('_db_')) {
+ if (sel.querySelector('option[value="archive"]')) sel.value = 'archive';
+ } else {
+ if (sel.querySelector('option[value="replace"]')) {
+ sel.value = 'replace';
+ } else if (sel.querySelector('option[value="add"]')) {
+ sel.value = 'add';
+ }
+ }
+ });
+ });
+ }
+
// 2. Interactive Switch Logic between Table View and Map View
const tableView = document.getElementById('table-view');
const mapView = document.getElementById('map-view');
@@ -1089,7 +1197,17 @@ document.addEventListener('DOMContentLoaded', () => {
{
code: "{{ item.code|escapejs }}",
short_code: "{{ item.short_code|escapejs }}",
- status: "{{ item.status|escapejs }}"
+ status: "{{ item.status|escapejs }}",
+ new_lon: {% if item.new_lon is not None %}{{ item.new_lon|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ new_lat: {% if item.new_lat is not None %}{{ item.new_lat|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ new_x_3812: {% if item.new_x_3812 is not None %}{{ item.new_x_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ new_y_3812: {% if item.new_y_3812 is not None %}{{ item.new_y_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ db_lon: {% if item.db_lon is not None %}{{ item.db_lon|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ db_lat: {% if item.db_lat is not None %}{{ item.db_lat|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ db_x_3812: {% if item.db_x_3812 is not None %}{{ item.db_x_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ db_y_3812: {% if item.db_y_3812 is not None %}{{ item.db_y_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
+ geom_modified: {{ item.geom_modified|yesno:"true,false" }},
+ distance_diff: {% if item.distance_diff is not None %}{{ item.distance_diff|default_if_none:''|unlocalize }}{% else %}null{% endif %}
},
{% endfor %}
];
@@ -1408,10 +1526,42 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.dxfFile && window.dxfFile !== "") {
loadDxfBackground(window.dxfFile);
}
+
+ // Automatically place markers for poles that have known coordinates (e.g. from DXF or previous georeferencing)
+ polesList.forEach(p => {
+ if (p.status !== 'db_only' && p.new_lon !== null && p.new_lat !== null) {
+ placePoleMarker(p.code, p.new_lon, p.new_lat, p.new_x_3812, p.new_y_3812, false);
+ }
+ });
updatePolesCounter();
});
+ // Delegated click handler for "Voir sur la carte" buttons in table
+ document.addEventListener('click', function(e) {
+ const btn = e.target.closest('.show-pole-on-map-btn');
+ if (!btn) return;
+ e.preventDefault();
+ const code = btn.dataset.code;
+
+ tableView.classList.remove('d-block');
+ tableView.classList.add('d-none');
+ mapView.classList.remove('d-none');
+ mapView.classList.add('d-block');
+
+ if (map) {
+ map.resize();
+ const tabEl = document.getElementById('map-poles-tab');
+ if (tabEl) new bootstrap.Tab(tabEl).show();
+
+ if (placedPoles[code]) {
+ map.flyTo({ center: [placedPoles[code].lon, placedPoles[code].lat], zoom: 19 });
+ } else {
+ activatePlacementMode(code);
+ }
+ }
+ });
+
function getSnappedCoords(lon, lat) {
let bestDistance = Infinity;
let bestCoords = [lon, lat];
@@ -1443,10 +1593,34 @@ document.addEventListener('DOMContentLoaded', () => {
badge.textContent = `{% translate "Positionné" %}`;
}
- document.getElementById(`lon_pole_${code}`).value = lon;
- document.getElementById(`lat_pole_${code}`).value = lat;
- document.getElementById(`x_3812_pole_${code}`).value = x_3812 !== undefined && x_3812 !== null ? x_3812 : "";
- document.getElementById(`y_3812_pole_${code}`).value = y_3812 !== undefined && y_3812 !== null ? y_3812 : "";
+ const inputLon = document.getElementById(`lon_pole_${code}`);
+ if (inputLon) inputLon.value = lon;
+ const inputLat = document.getElementById(`lat_pole_${code}`);
+ if (inputLat) inputLat.value = lat;
+ const inputX = document.getElementById(`x_3812_pole_${code}`);
+ if (inputX) inputX.value = (x_3812 !== undefined && x_3812 !== null) ? x_3812 : "";
+ const inputY = document.getElementById(`y_3812_pole_${code}`);
+ if (inputY) inputY.value = (y_3812 !== undefined && y_3812 !== null) ? y_3812 : "";
+
+ // Dynamically update the table row for this pole if position changed
+ const poleMeta = polesList.find(p => p.code === code);
+ if (poleMeta) {
+ const selectAction = document.querySelector(`select[name="action_pole_${code}"]`);
+ if (selectAction) {
+ if (selectAction.querySelector('option[value="modify"]')) {
+ selectAction.value = 'modify';
+ }
+ }
+ const geomCell = document.getElementById(`geom_cell_pole_${code}`);
+ if (geomCell) {
+ let distText = "";
+ if (poleMeta.db_x_3812 !== null && poleMeta.db_y_3812 !== null && x_3812 && y_3812) {
+ const d = Math.sqrt((poleMeta.db_x_3812 - x_3812)**2 + (poleMeta.db_y_3812 - y_3812)**2);
+ distText = ` (${d.toFixed(1)} m)`;
+ }
+ geomCell.innerHTML = ` {% translate "Positionné sur carte" %}${distText} `;
+ }
+ }
redrawCablesOnMap();
updatePolesCounter();
@@ -1462,7 +1636,7 @@ document.addEventListener('DOMContentLoaded', () => {
placePoleMarker(activePlacementPole, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1]);
});
- function placePoleMarker(code, lon, lat, x_3812, y_3812) {
+ function placePoleMarker(code, lon, lat, x_3812, y_3812, autoAdvance = true) {
if (placedPoles[code]) {
placedPoles[code].marker.remove();
}
@@ -1516,11 +1690,13 @@ document.addEventListener('DOMContentLoaded', () => {
listItem.classList.add('bg-light');
}
- const unplaced = polesList.filter(p => p.status !== 'db_only' && !placedPoles[p.code]);
- if (unplaced.length > 0) {
- activatePlacementMode(unplaced[0].code);
- } else {
- deactivatePlacementMode();
+ if (autoAdvance) {
+ const unplaced = polesList.filter(p => p.status !== 'db_only' && !placedPoles[p.code]);
+ if (unplaced.length > 0) {
+ activatePlacementMode(unplaced[0].code);
+ } else {
+ deactivatePlacementMode();
+ }
}
}
diff --git a/loko/assets/views/trafficlights.py b/loko/assets/views/trafficlights.py
index b270d07..276540e 100644
--- a/loko/assets/views/trafficlights.py
+++ b/loko/assets/views/trafficlights.py
@@ -1922,6 +1922,54 @@ def get_base_dxf_name(filename):
return f"{base_cleaned}{ext}".lower()
+def resolve_dxf_file_path(intersection, filename):
+ if not filename:
+ return None
+ filename_clean = os.path.basename(filename)
+
+ # 1. Managed documents
+ try:
+ from documents.models import DocumentAttachment, DocumentVersion
+ from django.contrib.contenttypes.models import ContentType
+ ct = ContentType.objects.get_for_model(intersection)
+ doc_ids = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id).values_list('document_id', flat=True)
+ versions = DocumentVersion.objects.filter(document_id__in=doc_ids, file__icontains=filename_clean)
+ for v in versions:
+ if v.file and os.path.exists(v.file.path):
+ return v.file.path
+ except Exception:
+ pass
+
+ # 2. Database documents (legacy)
+ try:
+ for d in intersection.documents.all():
+ if os.path.basename(d.file.name) == filename_clean and os.path.exists(d.file.path):
+ return d.file.path
+ except Exception:
+ pass
+
+ # 3. Private media dxf_files
+ uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files')
+ p = os.path.join(uploaded_dxf_dir, filename_clean)
+ if os.path.exists(p):
+ return p
+
+ # 4. External read-only files
+ read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files')
+ p = os.path.join(read_only_dxf_dir, filename_clean)
+ if os.path.exists(p):
+ return p
+
+ # 5. Dxf temp
+ dxf_temp_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_temp')
+ p = os.path.join(dxf_temp_dir, filename_clean)
+ if os.path.exists(p):
+ return p
+
+ return None
+
+
+
@check_thematic_access('trafficlights', edit_permission_required=True)
def import_dxf(request, intersection_id):
intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id)
@@ -4078,43 +4126,142 @@ def import_cross_plan(request, intersection_id):
deduced = merged_deduced
+ # ── RESOLVE ACTIVE DXF & SNAP POINTS ──
+ active_dxf_file = request.POST.get('selected_dxf') or (dxf_files[0] if dxf_files else "")
+ active_dxf_poles = []
+ dxf_poles_by_label = {}
+ if active_dxf_file:
+ dxf_path = resolve_dxf_file_path(intersection, active_dxf_file)
+ if dxf_path:
+ try:
+ active_dxf_poles = parse_dxf_poles(dxf_path)
+ for dp_dxf in active_dxf_poles:
+ lbl = (dp_dxf.get('label') or "").strip().upper()
+ if lbl:
+ dxf_poles_by_label[lbl] = dp_dxf
+ except Exception as e:
+ logger.warning("Could not parse DXF poles from %s: %s", dxf_path, e)
+
# ── 1. POLES COMPARISON ──
existing_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')}
compared_poles = []
+ from pyproj import Transformer
+ transformer_3812_to_4326 = Transformer.from_crs(3812, 4326, always_xy=True)
+ transformer_4326_to_3812 = Transformer.from_crs(4326, 3812, always_xy=True)
+
for dp in deduced["poles"]:
code = dp["code"]
+ short_code = dp["short_code"]
model_name = dp["model"]
db_model = TrafficLightPoleModel.objects.filter(name_fr__iexact=model_name).first() or TrafficLightPoleModel.objects.first()
+ dxf_match = dxf_poles_by_label.get(short_code.strip().upper())
+ dxf_lon = dxf_match['lon'] if dxf_match else None
+ dxf_lat = dxf_match['lat'] if dxf_match else None
+ dxf_x_3812 = dxf_match['x_3812'] if dxf_match else None
+ dxf_y_3812 = dxf_match['y_3812'] if dxf_match else None
+
if code in existing_poles:
ep = existing_poles[code]
- # Since cross plans do not specify pole physical models (height, color), keep existing DB model
- status = 'identical'
+ ep_lon = ep.lon
+ ep_lat = ep.lat
+ ep_x_3812 = ep.geom.x if ep.geom else None
+ ep_y_3812 = ep.geom.y if ep.geom else None
+
+ if ep.geom and (ep_lon is None or ep_lat is None):
+ try:
+ ep_lon, ep_lat = transformer_3812_to_4326.transform(ep_x_3812, ep_y_3812)
+ except Exception:
+ pass
+ elif ep.geom is None and ep_lon is not None and ep_lat is not None:
+ try:
+ ep_x_3812, ep_y_3812 = transformer_4326_to_3812.transform(ep_lon, ep_lat)
+ except Exception:
+ pass
+
+ details_diff = []
+ geom_modified = False
+ distance_diff = None
+
+ if dxf_match:
+ if ep_x_3812 is not None and ep_y_3812 is not None:
+ import math
+ dist = math.sqrt((ep_x_3812 - dxf_x_3812)**2 + (ep_y_3812 - dxf_y_3812)**2)
+ if dist >= 0.05:
+ geom_modified = True
+ distance_diff = round(dist, 1)
+ details_diff.append(_("Position modifiée (DXF) : décalage de %(dist).1f m") % {'dist': dist})
+ else:
+ geom_modified = True
+ details_diff.append(_("Nouvelle position détectée (DXF)"))
+
+ is_model_diff = (ep.model_id != db_model.id) if (ep.model and db_model) else False
+ if is_model_diff:
+ details_diff.append(_("Modèle : %(db_m)s → %(new_m)s") % {'db_m': ep.model.name_fr if ep.model else 'Aucun', 'new_m': model_name})
+
+ status = 'modified' if (geom_modified or is_model_diff) else 'identical'
compared_poles.append({
'code': code,
- 'short_code': dp['short_code'],
- 'name': ep.name_fr or dp.get('name', dp['short_code']),
+ 'short_code': short_code,
+ 'name': ep.name_fr or dp.get('name', short_code),
'status': status,
- 'deduced_model_name': ep.model.name_fr if ep.model else model_name,
+ 'deduced_model_name': model_name,
'db_model_name': ep.model.name_fr if ep.model else '',
'db_id': ep.id,
- 'details_diff': []
+ 'details_diff': details_diff,
+ 'geom_modified': geom_modified,
+ 'distance_diff': distance_diff,
+ 'db_lon': ep_lon,
+ 'db_lat': ep_lat,
+ 'db_x_3812': ep_x_3812,
+ 'db_y_3812': ep_y_3812,
+ 'new_lon': dxf_lon if dxf_match else ep_lon,
+ 'new_lat': dxf_lat if dxf_match else ep_lat,
+ 'new_x_3812': dxf_x_3812 if dxf_match else ep_x_3812,
+ 'new_y_3812': dxf_y_3812 if dxf_match else ep_y_3812,
})
# remove from processed
existing_poles.pop(code)
else:
+ details_diff = []
+ if dxf_match:
+ details_diff.append(_("Positionné via DXF"))
compared_poles.append({
'code': code,
- 'short_code': dp['short_code'],
- 'name': dp.get('name', dp['short_code']),
+ 'short_code': short_code,
+ 'name': dp.get('name', short_code),
'status': 'new',
'deduced_model_name': model_name,
'db_model_name': '',
'db_id': None,
- 'details_diff': []
+ 'details_diff': details_diff,
+ 'geom_modified': False,
+ 'distance_diff': None,
+ 'db_lon': None,
+ 'db_lat': None,
+ 'db_x_3812': None,
+ 'db_y_3812': None,
+ 'new_lon': dxf_lon,
+ 'new_lat': dxf_lat,
+ 'new_x_3812': dxf_x_3812,
+ 'new_y_3812': dxf_y_3812,
})
# Add remaining existing active poles (orphans)
for code, ep in existing_poles.items():
+ ep_lon = ep.lon
+ ep_lat = ep.lat
+ ep_x_3812 = ep.geom.x if ep.geom else None
+ ep_y_3812 = ep.geom.y if ep.geom else None
+ if ep.geom and (ep_lon is None or ep_lat is None):
+ try:
+ ep_lon, ep_lat = transformer_3812_to_4326.transform(ep_x_3812, ep_y_3812)
+ except Exception:
+ pass
+ elif ep.geom is None and ep_lon is not None and ep_lat is not None:
+ try:
+ ep_x_3812, ep_y_3812 = transformer_4326_to_3812.transform(ep_lon, ep_lat)
+ except Exception:
+ pass
compared_poles.append({
'code': code,
'short_code': code.split('_')[-1] if '_' in code else code,
@@ -4123,9 +4270,20 @@ 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 (présent en DB)")]
+ 'details_diff': [_("Absent du fichier Excel (présent en DB)")],
+ 'geom_modified': False,
+ 'distance_diff': None,
+ 'db_lon': ep_lon,
+ 'db_lat': ep_lat,
+ 'db_x_3812': ep_x_3812,
+ 'db_y_3812': ep_y_3812,
+ 'new_lon': ep_lon,
+ 'new_lat': ep_lat,
+ 'new_x_3812': ep_x_3812,
+ 'new_y_3812': ep_y_3812,
})
+
# ── 2. CABLES COMPARISON ──
existing_cables = {c.code: c for c in TrafficLightCable.objects.filter(intersection=intersection, status='active')}
compared_cables = []
@@ -4138,6 +4296,7 @@ def import_cross_plan(request, intersection_id):
ec = existing_cables[code]
is_diff = ec.model_id != db_model.id
status = 'modified' if is_diff else 'identical'
+ has_geom = ec.geom is not None
compared_cables.append({
'code': code,
'name': ec.name_fr or dc['name'],
@@ -4146,7 +4305,9 @@ def import_cross_plan(request, intersection_id):
'db_model_name': ec.model.name_fr if ec.model else '',
'db_id': ec.id,
'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", [])
+ 'connected_poles': dc.get("connected_poles", []),
+ 'has_db_geom': has_geom,
+ 'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
})
existing_cables.pop(code)
else:
@@ -4158,9 +4319,12 @@ def import_cross_plan(request, intersection_id):
'db_model_name': '',
'db_id': None,
'details_diff': [],
- 'connected_poles': dc.get("connected_poles", [])
+ 'connected_poles': dc.get("connected_poles", []),
+ 'has_db_geom': False,
+ 'db_geom_length': None,
})
for code, ec in existing_cables.items():
+ has_geom = ec.geom is not None
compared_cables.append({
'code': code,
'name': ec.name_fr or code,
@@ -4168,7 +4332,9 @@ 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 (présent en DB)")]
+ 'details_diff': [_("Absent du fichier Excel (présent en DB)")],
+ 'has_db_geom': has_geom,
+ 'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
})
# ── 3. LANTERNS COMPARISON ──
@@ -4526,8 +4692,8 @@ def import_cross_plan(request, intersection_id):
'cable_models': cable_models,
'step': 'preview',
'thematic_code': 'trafficlights',
- 'dxf_file': "",
- 'dxf_poles': [],
+ 'dxf_file': active_dxf_file,
+ 'dxf_poles': active_dxf_poles,
'existing_poles_geojson': existing_poles_geojson,
'dxf_files': dxf_files,
'georeferenced_documents_json': georeferenced_documents_json,
@@ -4621,6 +4787,40 @@ def integrate_cross_plan(request, intersection_id):
)
return m
+ from django.contrib.contenttypes.models import ContentType
+ from documents.models import DocumentAttachment, ManagedDocument
+
+ dxf_files = []
+ ct = ContentType.objects.get_for_model(intersection)
+ attachments = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id)
+ managed_dxf_docs = ManagedDocument.objects.filter(
+ id__in=attachments.values_list('document_id', flat=True),
+ is_archived=False
+ ).select_related('current_version')
+ for doc in managed_dxf_docs:
+ if doc.current_version and doc.current_version.file:
+ fn = os.path.basename(doc.current_version.file.name)
+ if fn.lower().endswith('.dxf') and fn not in dxf_files:
+ dxf_files.append(fn)
+ for doc in intersection.documents.all():
+ if doc.file.name.lower().endswith('.dxf'):
+ fn = os.path.basename(doc.file.name)
+ if fn not in dxf_files:
+ dxf_files.append(fn)
+
+ active_dxf_file = request.POST.get('selected_dxf') or (dxf_files[0] if dxf_files else "")
+ dxf_poles_by_label = {}
+ if active_dxf_file:
+ dxf_path = resolve_dxf_file_path(intersection, active_dxf_file)
+ if dxf_path:
+ try:
+ for dp_dxf in parse_dxf_poles(dxf_path):
+ lbl = (dp_dxf.get('label') or "").strip().upper()
+ if lbl:
+ dxf_poles_by_label[lbl] = dp_dxf
+ except Exception:
+ pass
+
def update_pole_geometry(pole, code):
def clean_float(val):
if not val:
@@ -4634,10 +4834,23 @@ def integrate_cross_plan(request, intersection_id):
lat = clean_float(request.POST.get(f"lat_pole_{code}"))
x_3812 = clean_float(request.POST.get(f"x_3812_pole_{code}"))
y_3812 = clean_float(request.POST.get(f"y_3812_pole_{code}"))
-
+
+ if x_3812 is None and lon is None:
+ short_code = code.split('_')[-1] if '_' in code else code
+ dxf_match = dxf_poles_by_label.get(short_code.strip().upper())
+ if dxf_match:
+ lon = dxf_match.get('lon')
+ lat = dxf_match.get('lat')
+ x_3812 = dxf_match.get('x_3812')
+ y_3812 = dxf_match.get('y_3812')
+
if x_3812 is not None and y_3812 is not None and x_3812 != 0.0 and y_3812 != 0.0:
from django.contrib.gis.geos import Point as DjangoPoint
+ from pyproj import Transformer
pole.geom = DjangoPoint(x_3812, y_3812, srid=3812)
+ if lon is None or lat is None:
+ transformer = Transformer.from_crs(3812, 4326, always_xy=True)
+ lon, lat = transformer.transform(x_3812, y_3812)
pole.lon = lon
pole.lat = lat
elif lon is not None and lat is not None:
@@ -4762,7 +4975,193 @@ def integrate_cross_plan(request, intersection_id):
messages.success(request, "Tous les anciens assets ont été archivés et les nouveaux ont été créés sur base du plan de croix.")
- # B. SELECTIVE OR INDIVIDUAL INTEGRATION
+ # B. GLOBAL UPDATE ALL (Preserve existing + Update in-place + Create new)
+ elif global_action == 'update_all':
+ # --- 1. Poles ---
+ created_poles = {}
+ for dp in deduced["poles"]:
+ code = dp["code"]
+ short_code = dp.get('short_code', code.split('_')[-1])
+ existing = TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').first()
+ if not existing:
+ db_id = request.POST.get(f"db_id_pole_{code}")
+ if db_id:
+ existing = TrafficLightPole.objects.filter(pk=db_id, status='active').first()
+ if not existing:
+ existing = TrafficLightPole.objects.filter(intersection=intersection, code__endswith=f"_{short_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:
+ db_model = resolve_pole_model(dp["model"])
+
+ custom_name = request.POST.get(f"name_pole_{code}", dp.get('name', short_code))
+
+ if existing:
+ existing.code = code
+ existing.name_fr = custom_name
+ existing.name_nl = custom_name
+ if db_model:
+ existing.model = db_model
+ existing.updated_by = request.user
+ update_pole_geometry(existing, code)
+ existing.save()
+ created_poles[code] = existing
+ else:
+ new_pole = TrafficLightPole(
+ intersection=intersection,
+ code=code,
+ name_fr=custom_name,
+ name_nl=custom_name,
+ model=db_model,
+ status='active',
+ validation_status='validated',
+ updated_by=request.user
+ )
+ update_pole_geometry(new_pole, code)
+ new_pole.save()
+ created_poles[code] = new_pole
+
+ # Map active poles for lanterns and detectors parenting
+ active_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')}
+ for c, p in created_poles.items():
+ active_poles[c] = p
+
+ # --- 2. Cables ---
+ for dc in deduced["cables"]:
+ code = dc["code"]
+ existing = TrafficLightCable.objects.filter(intersection=intersection, code=code, status='active').first()
+ if not existing:
+ db_id = request.POST.get(f"db_id_cable_{code}")
+ if db_id:
+ existing = TrafficLightCable.objects.filter(pk=db_id, status='active').first()
+
+ selected_model_id = request.POST.get(f"model_cable_{code}")
+ db_model = None
+ if selected_model_id:
+ db_model = TrafficLightCableModel.objects.filter(pk=selected_model_id).first()
+ if not db_model:
+ db_model = resolve_cable_model(dc["model_name"])
+
+ custom_name = request.POST.get(f"name_cable_{code}", dc["name"])
+ if existing:
+ existing.code = code
+ existing.name_fr = custom_name
+ existing.name_nl = custom_name
+ if db_model:
+ existing.model = db_model
+ existing.updated_by = request.user
+ update_cable_geometry(existing, code)
+ existing.save()
+ else:
+ new_cable = TrafficLightCable(
+ intersection=intersection,
+ code=code,
+ name_fr=custom_name,
+ name_nl=custom_name,
+ model=db_model,
+ status='active',
+ validation_status='validated',
+ updated_by=request.user
+ )
+ update_cable_geometry(new_cable, code)
+ new_cable.save()
+
+ # --- 3. Lanterns ---
+ for dl in deduced["lanterns"]:
+ code = dl["code"]
+ pole = active_poles.get(dl["pole_code"])
+ existing = TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code, status='active').first()
+ if not existing:
+ db_id = request.POST.get(f"db_id_lantern_{code}")
+ if db_id:
+ existing = TrafficLightLantern.objects.filter(pk=db_id, status='active').first()
+
+ selected_model_id = request.POST.get(f"model_lantern_{code}")
+ db_model = None
+ if selected_model_id:
+ db_model = TrafficLightLanternModel.objects.filter(pk=selected_model_id).first()
+ if not db_model:
+ db_model = resolve_lantern_model(dl["model_name"])
+
+ custom_name = request.POST.get(f"name_lantern_{code}", dl["name"])
+ if existing:
+ existing.code = code
+ if pole:
+ existing.pole = pole
+ if db_model:
+ existing.model = db_model
+ existing.name_fr = custom_name
+ existing.name_nl = custom_name
+ existing.updated_by = request.user
+ existing.save()
+ elif pole:
+ TrafficLightLantern.objects.create(
+ pole=pole,
+ code=code,
+ name_fr=custom_name,
+ name_nl=custom_name,
+ model=db_model,
+ status='active',
+ validation_status='validated',
+ updated_by=request.user
+ )
+
+ # --- 4. Detectors ---
+ for dd in deduced["detectors"]:
+ code = dd["code"]
+ pole = active_poles.get(dd["pole_code"])
+ existing = TrafficLightDetector.objects.filter(intersection=intersection, code=code, status='active').first()
+ if not existing:
+ db_id = request.POST.get(f"db_id_detector_{code}")
+ if db_id:
+ existing = TrafficLightDetector.objects.filter(pk=db_id, status='active').first()
+ if not existing:
+ parts = code.split('_', 2)
+ if len(parts) == 3:
+ old_key = f"{parts[0]}-{parts[2]}"
+ existing = TrafficLightDetector.objects.filter(intersection=intersection, code=old_key, status='active').first()
+
+ selected_model_id = request.POST.get(f"model_detector_{code}")
+ db_model = None
+ if selected_model_id:
+ db_model = TrafficLightDetectorModel.objects.filter(pk=selected_model_id).first()
+ if not db_model:
+ db_model = resolve_detector_model(dd["model_name"])
+
+ custom_name = request.POST.get(f"name_detector_{code}", dd["name"])
+ if existing:
+ existing.code = code
+ if pole:
+ existing.pole = pole
+ if db_model:
+ existing.model = db_model
+ existing.name = custom_name
+ existing.name_fr = custom_name
+ existing.name_nl = custom_name
+ existing.phases = dd.get("phases", "")
+ existing.updated_by = request.user
+ existing.save()
+ else:
+ TrafficLightDetector.objects.create(
+ intersection=intersection,
+ pole=pole,
+ code=code,
+ name=custom_name,
+ name_fr=custom_name,
+ name_nl=custom_name,
+ model=db_model,
+ phases=dd.get("phases", ""),
+ status='active',
+ validation_status='validated',
+ updated_by=request.user
+ )
+
+ messages.success(request, "Les assets existants ont été mis à jour et les nouveaux ont été créés sans archiver l'existant.")
+
+ # C. SELECTIVE OR INDIVIDUAL INTEGRATION
else:
# We iterate over each asset category, checking the user choices from the form post parameters