feat: add location management, asset creation modal, and intervention proximity tracking to details view

This commit is contained in:
Karl Determe 2026-08-19 07:17:15 +02:00
parent 1bdb771f3b
commit ca6d09c6b1
6 changed files with 88 additions and 9 deletions

View file

@ -45,13 +45,15 @@
const thematicCode = form.dataset.thematic || form.querySelector('[name="thematic_code"]')?.value || ''; const thematicCode = form.dataset.thematic || form.querySelector('[name="thematic_code"]')?.value || '';
const locationId = form.querySelector('[name="location_id"]')?.value || ''; const locationId = form.querySelector('[name="location_id"]')?.value || '';
const structureId = form.querySelector('[name="structure_id"]')?.value || ''; const structureId = form.querySelector('[name="structure_id"]')?.value || '';
const interventionId = form.dataset.interventionId || form.querySelector('[name="intervention_id"]')?.value || '';
if (!thematicCode) return; if (!thematicCode) return;
const params = new URLSearchParams({ const params = new URLSearchParams({
thematic_code: thematicCode, thematic_code: thematicCode,
location_id: locationId, location_id: locationId,
structure_id: structureId structure_id: structureId,
intervention_id: interventionId
}); });
fetch(`/assets/api/suggested-code/?${params.toString()}`) fetch(`/assets/api/suggested-code/?${params.toString()}`)

View file

@ -477,6 +477,33 @@ class StructuresLocationDetailAddAssetModalTest(TestCase):
self.assertEqual(page_res.context["suggested_code"], "WTB30-000025") self.assertEqual(page_res.context["suggested_code"], "WTB30-000025")
self.assertContains(page_res, 'value="WTB30-000025"') self.assertContains(page_res, 'value="WTB30-000025"')
# Test intervention code suggestion (mobile / desktop intervention)
from interventions.models import Intervention, InterventionLocation
from django.contrib.contenttypes.models import ContentType
itv = Intervention.objects.create(
code="ITV_TEST_99",
title="Intervention Test Structures",
thematic=self.thematic
)
loc_ct = ContentType.objects.get_for_model(self.location)
InterventionLocation.objects.create(
intervention=itv,
content_type=loc_ct,
object_id=self.location.id
)
itv_suggested = get_suggested_asset_code("structures", intervention_id=itv.id)
self.assertEqual(itv_suggested, "WTB30-000025")
itv_api_res = self.client.get(reverse("assets:get_suggested_asset_code_api"), {
"thematic_code": "structures",
"intervention_id": itv.id
})
self.assertEqual(itv_api_res.status_code, 200)
self.assertEqual(itv_api_res.json().get("suggested_code"), "WTB30-000025")

View file

@ -7,16 +7,54 @@ def get_next_structure_asset_code(
structure=None, structure=None,
location=None, location=None,
structure_id: Optional[int] = None, structure_id: Optional[int] = None,
location_id: Optional[int] = None location_id: Optional[int] = None,
intervention=None,
intervention_id: Optional[int] = None
) -> str: ) -> str:
""" """
Computes the next incremental asset code for a structure / tunnel location. Computes the next incremental asset code for a structure / tunnel location / intervention.
Looks up existing asset codes (e.g. WTB30-000023, WTB30-000412), determines Looks up existing asset codes (e.g. WTB30-000023, WTB30-000412), determines
the dominant prefix and maximum integer number, and returns the next code the dominant prefix and maximum integer number, and returns the next code
(e.g. WTB30-000413), ensuring it does not collide with existing codes. (e.g. WTB30-000413), ensuring it does not collide with existing codes.
""" """
from assets.models.structures import Structure, StructureLocation, StructureGeoAsset from assets.models.structures import Structure, StructureLocation, StructureGeoAsset
if intervention_id and not intervention:
try:
from interventions.models import Intervention
intervention = Intervention.objects.get(pk=intervention_id)
except Exception:
intervention = None
if intervention and not location and not structure:
# 1. Look for linked structure locations
for loc_link in intervention.locations.all():
loc_obj = loc_link.location
if isinstance(loc_obj, StructureLocation) or hasattr(loc_obj, 'structure'):
location = loc_obj
structure = getattr(loc_obj, 'structure', None)
break
# 2. Look for linked structure assets
if not structure and not location:
for a_link in intervention.interventionasset_set.all():
asset_obj = a_link.asset
if asset_obj and hasattr(asset_obj, 'structure') and asset_obj.structure:
structure = asset_obj.structure
location = getattr(asset_obj, 'location', None)
break
# 3. Fallback: match by location_code / address if structure exists
if not structure and not location and intervention.location_code:
loc_match = StructureLocation.objects.filter(code__iexact=intervention.location_code).first()
if loc_match:
location = loc_match
structure = loc_match.structure
else:
struct_match = Structure.objects.filter(code__iexact=intervention.location_code).first()
if struct_match:
structure = struct_match
if location_id and not location: if location_id and not location:
try: try:
location = StructureLocation.objects.select_related('structure').get(pk=location_id) location = StructureLocation.objects.select_related('structure').get(pk=location_id)
@ -79,17 +117,21 @@ def get_suggested_asset_code(
thematic_code: str, thematic_code: str,
location_id: Optional[int] = None, location_id: Optional[int] = None,
structure_id: Optional[int] = None, structure_id: Optional[int] = None,
intervention_id: Optional[int] = None,
location=None, location=None,
structure=None structure=None,
intervention=None
) -> str: ) -> str:
""" """
Returns a suggested asset code based on thematic and context. Returns a suggested asset code based on thematic and context (location, structure, or intervention).
""" """
if thematic_code == 'structures': if thematic_code == 'structures':
return get_next_structure_asset_code( return get_next_structure_asset_code(
structure=structure, structure=structure,
location=location, location=location,
structure_id=structure_id, structure_id=structure_id,
location_id=location_id location_id=location_id,
intervention=intervention,
intervention_id=intervention_id
) )
return "" return ""

View file

@ -1286,12 +1286,14 @@ def get_suggested_asset_code_api(request):
- thematic_code (str) - thematic_code (str)
- location_id (int, optional) - location_id (int, optional)
- structure_id (int, optional) - structure_id (int, optional)
- intervention_id (int, optional)
""" """
from assets.utils.asset_code_generator import get_suggested_asset_code from assets.utils.asset_code_generator import get_suggested_asset_code
thematic_code = request.GET.get('thematic_code') or '' thematic_code = request.GET.get('thematic_code') or ''
location_id = request.GET.get('location_id') location_id = request.GET.get('location_id')
structure_id = request.GET.get('structure_id') structure_id = request.GET.get('structure_id')
intervention_id = request.GET.get('intervention_id')
try: try:
loc_id = int(location_id) if location_id else None loc_id = int(location_id) if location_id else None
@ -1303,10 +1305,16 @@ def get_suggested_asset_code_api(request):
except (ValueError, TypeError): except (ValueError, TypeError):
struct_id = None struct_id = None
try:
itv_id = int(intervention_id) if intervention_id else None
except (ValueError, TypeError):
itv_id = None
suggested_code = get_suggested_asset_code( suggested_code = get_suggested_asset_code(
thematic_code=thematic_code, thematic_code=thematic_code,
location_id=loc_id, location_id=loc_id,
structure_id=struct_id structure_id=struct_id,
intervention_id=itv_id
) )
return JsonResponse({ return JsonResponse({

View file

@ -169,7 +169,7 @@
<label for="addAssetDesktopModalCode" class="form-label"> <label for="addAssetDesktopModalCode" class="form-label">
<i class="bi bi-hash me-1"></i>{% translate "Code" %} <span class="text-danger">*</span> <i class="bi bi-hash me-1"></i>{% translate "Code" %} <span class="text-danger">*</span>
</label> </label>
<input type="text" class="form-control" id="addAssetDesktopModalCode" name="code" required <input type="text" class="form-control asset-code-input" id="addAssetDesktopModalCode" name="code" required
placeholder="{% translate 'Ex: STR-001' %}" maxlength="50"> placeholder="{% translate 'Ex: STR-001' %}" maxlength="50">
<div class="form-text text-muted"> <div class="form-text text-muted">
{% translate "Code unique pour identifier l'équipement." %} {% translate "Code unique pour identifier l'équipement." %}

View file

@ -1517,7 +1517,7 @@
<label for="addAssetMobileModalCode" class="form-label"> <label for="addAssetMobileModalCode" class="form-label">
{% translate "Code" %} <span class="text-danger">*</span> {% translate "Code" %} <span class="text-danger">*</span>
</label> </label>
<input type="text" class="form-control" id="addAssetMobileModalCode" name="code" required <input type="text" class="form-control asset-code-input" id="addAssetMobileModalCode" name="code" required
placeholder="{% translate 'Ex: STR-001' %}" maxlength="50"> placeholder="{% translate 'Ex: STR-001' %}" maxlength="50">
</div> </div>