diff --git a/loko/assets/static/assets/asset_add_modal.js b/loko/assets/static/assets/asset_add_modal.js
index 8f80fbb..b6f88c8 100644
--- a/loko/assets/static/assets/asset_add_modal.js
+++ b/loko/assets/static/assets/asset_add_modal.js
@@ -376,12 +376,18 @@
},
body: JSON.stringify(data)
})
- .then(response => response.json())
- .then(result => {
+ .then(async response => {
+ let result = null;
+ try {
+ result = await response.json();
+ } catch (e) {
+ result = null;
+ }
+
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
- if (result.success) {
+ if (response.ok && result && result.success) {
alertContainer.className = 'alert alert-success';
alertContainer.innerHTML = '' + result.message;
alertContainer.classList.remove('d-none');
@@ -396,18 +402,20 @@
}, 1000);
}
} else {
+ const errorMsg = (result && (result.error || result.message))
+ || (response.status ? `${gettext('Erreur serveur')} (${response.status})` : gettext('Erreur lors de la création'));
alertContainer.className = 'alert alert-danger';
- alertContainer.innerHTML = '' + (result.error || gettext('Erreur lors de la création'));
+ alertContainer.innerHTML = '' + errorMsg;
alertContainer.classList.remove('d-none');
}
})
.catch(error => {
- console.error('Error:', error);
+ console.error('Error creating asset:', error);
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
alertContainer.className = 'alert alert-danger';
- alertContainer.innerHTML = '' + gettext('Erreur de connexion');
+ alertContainer.innerHTML = '' + (error.message || gettext('Erreur de connexion'));
alertContainer.classList.remove('d-none');
});
}
@@ -426,9 +434,15 @@
asset_id: assetId
})
})
- .then(response => response.json())
- .then(data => {
- if (data.success && data.has_templates && data.available_templates && data.available_templates.length > 0) {
+ .then(async response => {
+ let data = null;
+ try {
+ data = await response.json();
+ } catch (e) {
+ data = null;
+ }
+
+ if (response.ok && data && data.success && data.has_templates && data.available_templates && data.available_templates.length > 0) {
// Afficher le modal de sélection de template
showTemplateSelectionModal(
interventionId,
@@ -440,7 +454,7 @@
data.message
);
} else {
- // Pas de templates disponibles, rediriger normalement
+ // Pas de templates disponibles ou non applicable, rediriger normalement
setTimeout(() => {
performRedirect(modalId, interventionId, {intervention_linked: true});
}, 1000);
@@ -584,9 +598,15 @@
template_ids: selectedTemplateIds
})
})
- .then(response => response.json())
- .then(data => {
- if (data.success) {
+ .then(async response => {
+ let data = null;
+ try {
+ data = await response.json();
+ } catch (e) {
+ data = null;
+ }
+
+ if (response.ok && data && data.success) {
templateAlert.className = 'alert alert-success';
templateAlert.innerHTML = '' + data.message;
templateAlert.classList.remove('d-none');
@@ -600,17 +620,19 @@
} else {
createBtn.disabled = false;
createBtn.innerHTML = originalBtnText;
+ const errorMsg = (data && (data.error || data.message))
+ || (response.status ? `${gettext('Erreur serveur')} (${response.status})` : gettext('Erreur lors de la création'));
templateAlert.className = 'alert alert-danger';
- templateAlert.innerHTML = '' + (data.error || gettext('Erreur lors de la création'));
+ templateAlert.innerHTML = '' + errorMsg;
templateAlert.classList.remove('d-none');
}
})
.catch(error => {
- console.error('Error:', error);
+ console.error('Error creating operations from template:', error);
createBtn.disabled = false;
createBtn.innerHTML = originalBtnText;
templateAlert.className = 'alert alert-danger';
- templateAlert.innerHTML = '' + gettext('Erreur de connexion');
+ templateAlert.innerHTML = '' + (error.message || gettext('Erreur de connexion'));
templateAlert.classList.remove('d-none');
});
});
diff --git a/loko/assets/static/assets/unified_index.js b/loko/assets/static/assets/unified_index.js
index 1686c66..097ff21 100644
--- a/loko/assets/static/assets/unified_index.js
+++ b/loko/assets/static/assets/unified_index.js
@@ -972,8 +972,14 @@ $(document).ready(function () {
},
body: JSON.stringify(payload),
});
- const result = await response.json();
- if (result.success) {
+ let result = null;
+ try {
+ result = await response.json();
+ } catch (e) {
+ result = null;
+ }
+
+ if (response.ok && result && result.success) {
setModalAlert('unifiedAddAssetAlert', `${escapeHtml(result.message || gettext('Asset créé.'))}`, 'success');
setTimeout(() => {
bootstrap.Modal.getInstance(document.getElementById('unifiedAddAssetModal'))?.hide();
@@ -985,11 +991,13 @@ $(document).ready(function () {
else window.location.reload();
}, 800);
} else {
- setModalAlert('unifiedAddAssetAlert', `${escapeHtml(result.error || gettext('Erreur lors de la création.'))}`, 'danger');
+ const errorMsg = (result && (result.error || result.message))
+ || (response.status ? `${gettext('Erreur serveur')} (${response.status})` : gettext('Erreur lors de la création.'));
+ setModalAlert('unifiedAddAssetAlert', `${escapeHtml(errorMsg)}`, 'danger');
}
} catch (error) {
console.error('Error creating asset:', error);
- setModalAlert('unifiedAddAssetAlert', `${escapeHtml(gettext('Erreur de connexion.'))}`, 'danger');
+ setModalAlert('unifiedAddAssetAlert', `${escapeHtml(error.message || gettext('Erreur de connexion.'))}`, 'danger');
} finally {
if (submitBtn) {
submitBtn.disabled = false;
diff --git a/loko/assets/tests.py b/loko/assets/tests.py
index a152f54..66507eb 100644
--- a/loko/assets/tests.py
+++ b/loko/assets/tests.py
@@ -432,8 +432,8 @@ class StructuresLocationDetailAddAssetModalTest(TestCase):
self.assertContains(response, "Balisage Test")
self.assertContains(response, f'name="location_id" value="{self.location.id}"')
self.assertIn("suggested_code", response.context)
- self.assertEqual(response.context["suggested_code"], "OA_TEST_01-000001")
- self.assertContains(response, 'value="OA_TEST_01-000001"')
+ self.assertEqual(response.context["suggested_code"], "LOC_TEST_01-000001")
+ self.assertContains(response, 'value="LOC_TEST_01-000001"')
def test_incremental_code_generation_with_existing_assets(self):
from assets.models.structures import StructureGeoAsset, StructureAssetModel
@@ -441,14 +441,14 @@ class StructuresLocationDetailAddAssetModalTest(TestCase):
model = StructureAssetModel.objects.create(code="MOD_TEST", name_fr="Modèle Test", category=self.category)
StructureGeoAsset.objects.create(
- code="WTB30-000023",
+ code="LOC_TEST_01-000023",
name_fr="Capteur 23",
structure=self.structure,
location=self.location,
model=model
)
StructureGeoAsset.objects.create(
- code="WTB30-000024",
+ code="LOC_TEST_01-000024",
name_fr="Capteur 24",
structure=self.structure,
location=self.location,
@@ -456,10 +456,10 @@ class StructuresLocationDetailAddAssetModalTest(TestCase):
)
next_code = get_next_structure_asset_code(location=self.location)
- self.assertEqual(next_code, "WTB30-000025")
+ self.assertEqual(next_code, "LOC_TEST_01-000025")
suggested = get_suggested_asset_code("structures", location_id=self.location.id)
- self.assertEqual(suggested, "WTB30-000025")
+ self.assertEqual(suggested, "LOC_TEST_01-000025")
# Test API endpoint
self.client.force_login(self.user)
@@ -470,13 +470,13 @@ class StructuresLocationDetailAddAssetModalTest(TestCase):
self.assertEqual(api_res.status_code, 200)
data = api_res.json()
self.assertTrue(data.get("success"))
- self.assertEqual(data.get("suggested_code"), "WTB30-000025")
+ self.assertEqual(data.get("suggested_code"), "LOC_TEST_01-000025")
# Test page rendering has updated suggested code
page_res = self.client.get(reverse("assets:structures_locations_detail", args=[self.location.id]))
self.assertEqual(page_res.status_code, 200)
- self.assertEqual(page_res.context["suggested_code"], "WTB30-000025")
- self.assertContains(page_res, 'value="WTB30-000025"')
+ self.assertEqual(page_res.context["suggested_code"], "LOC_TEST_01-000025")
+ self.assertContains(page_res, 'value="LOC_TEST_01-000025"')
# Test intervention code suggestion (mobile / desktop intervention)
from interventions.models import Intervention, InterventionLocation
@@ -495,14 +495,120 @@ class StructuresLocationDetailAddAssetModalTest(TestCase):
)
itv_suggested = get_suggested_asset_code("structures", intervention_id=itv.id)
- self.assertEqual(itv_suggested, "WTB30-000025")
+ self.assertEqual(itv_suggested, "LOC_TEST_01-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")
+ self.assertEqual(itv_api_res.json().get("suggested_code"), "LOC_TEST_01-000025")
+
+ def test_code_suggestion_specific_location_avoids_other_sublocation_dominant_prefix(self):
+ """
+ Verify that when generating a code for location WTB23B, it generates WTB23B-000001 (or next WTB23B)
+ even if another location WTB23J under the same structure has many existing assets.
+ """
+ from assets.models.structures import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel
+ from assets.utils.asset_code_generator import get_next_structure_asset_code, get_suggested_asset_code
+ from interventions.models import Intervention, InterventionLocation
+ from django.contrib.contenttypes.models import ContentType
+
+ struct_belliard = Structure.objects.create(code="WTB23", name_fr="Tunnel Belliard")
+ loc_b = StructureLocation.objects.create(code="WTB23B", name_fr="Belliard B", structure=struct_belliard)
+ loc_j = StructureLocation.objects.create(code="WTB23J", name_fr="Belliard J", structure=struct_belliard)
+
+ model = StructureAssetModel.objects.create(code="MOD_BELL", name_fr="Modèle Belliard", category=self.category)
+
+ # Create 10 assets for WTB23J
+ for i in range(1, 11):
+ StructureGeoAsset.objects.create(
+ code=f"WTB23J-{i:06d}",
+ name_fr=f"Asset J {i}",
+ structure=struct_belliard,
+ location=loc_j,
+ model=model
+ )
+
+ # For location WTB23B with 0 existing assets, it should suggest WTB23B-000001, NOT WTB23J-000011!
+ suggested_b = get_next_structure_asset_code(location=loc_b)
+ self.assertEqual(suggested_b, "WTB23B-000001")
+
+ # For an intervention linked to WTB23B
+ itv = Intervention.objects.create(code="ITV_WTB23B", title="Cyclique Belliard", thematic=self.thematic)
+ loc_ct = ContentType.objects.get_for_model(loc_b)
+ InterventionLocation.objects.create(intervention=itv, content_type=loc_ct, object_id=loc_b.id)
+
+ itv_code = get_suggested_asset_code("structures", intervention_id=itv.id)
+ self.assertEqual(itv_code, "WTB23B-000001")
+
+ # If WTB23B already has an asset WTB23B-000001, it should suggest WTB23B-000002
+ StructureGeoAsset.objects.create(
+ code="WTB23B-000001",
+ name_fr="Asset B 1",
+ structure=struct_belliard,
+ location=loc_b,
+ model=model
+ )
+ itv_code_2 = get_suggested_asset_code("structures", intervention_id=itv.id)
+ self.assertEqual(itv_code_2, "WTB23B-000002")
+
+ def test_create_asset_api_success_with_model_and_intervention(self):
+ """
+ Verify create_asset_api creates a StructureGeoAsset with model, resolves location and links to intervention.
+ """
+ from assets.models.structures import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel
+ from interventions.models import Intervention, InterventionLocation, InterventionAsset
+ from django.contrib.contenttypes.models import ContentType
+
+ struct = Structure.objects.create(code="WTB23_TEST", name_fr="Tunnel Belliard Test")
+ loc = StructureLocation.objects.create(code="WTB23B_TEST", name_fr="Belliard B Test", structure=struct)
+ model = StructureAssetModel.objects.create(code="MOD_API_TEST", name_fr="Modèle API", category=self.category)
+
+ # Allow model for category
+ ct = ContentType.objects.get_for_model(StructureGeoAsset)
+ self.category.allowed_models.add(ct)
+
+ itv = Intervention.objects.create(
+ code="ITV_CREATE_ASSET",
+ title="Intervention Création",
+ thematic=self.thematic,
+ maintain_type="preventive"
+ )
+ loc_ct = ContentType.objects.get_for_model(loc)
+ InterventionLocation.objects.create(intervention=itv, content_type=loc_ct, object_id=loc.id)
+
+ self.client.force_login(self.user)
+ payload = {
+ "thematic_code": "structures",
+ "category_id": self.category.id,
+ "model_id": model.id,
+ "code": "WTB23B_TEST-000001",
+ "name_fr": "Nouvel Équipement Test",
+ "intervention_id": itv.id
+ }
+
+ res = self.client.post(
+ reverse("assets:create_asset_api"),
+ data=json.dumps(payload),
+ content_type="application/json"
+ )
+ self.assertEqual(res.status_code, 200, f"Error: {res.content}")
+ data = res.json()
+ self.assertTrue(data.get("success"))
+ self.assertTrue(data.get("intervention_linked"))
+ self.assertTrue(data.get("needs_template_selection"))
+
+ # Verify asset created in DB
+ created_asset = StructureGeoAsset.objects.get(code="WTB23B_TEST-000001")
+ self.assertEqual(created_asset.model, model)
+ self.assertEqual(created_asset.category, self.category)
+ self.assertEqual(created_asset.location, loc)
+ self.assertEqual(created_asset.structure, struct)
+ self.assertEqual(created_asset.name_fr, "Nouvel Équipement Test")
+
+ # Verify linked to intervention
+ self.assertTrue(InterventionAsset.objects.filter(intervention=itv, object_id=created_asset.id).exists())
diff --git a/loko/assets/utils/asset_code_generator.py b/loko/assets/utils/asset_code_generator.py
index 83f29f3..082fd78 100644
--- a/loko/assets/utils/asset_code_generator.py
+++ b/loko/assets/utils/asset_code_generator.py
@@ -19,6 +19,8 @@ def get_next_structure_asset_code(
"""
from assets.models.structures import Structure, StructureLocation, StructureGeoAsset
+ from django.db.models import Q
+
if intervention_id and not intervention:
try:
from interventions.models import Intervention
@@ -30,7 +32,16 @@ def get_next_structure_asset_code(
# 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'):
+ if isinstance(loc_obj, StructureLocation):
+ location = loc_obj
+ structure = loc_obj.structure
+ break
+ elif isinstance(loc_obj, Structure):
+ structure = loc_obj
+ if loc_obj.main_location:
+ location = loc_obj.main_location
+ break
+ elif hasattr(loc_obj, 'structure'):
location = loc_obj
structure = getattr(loc_obj, 'structure', None)
break
@@ -39,10 +50,13 @@ def get_next_structure_asset_code(
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
+ if asset_obj:
+ if hasattr(asset_obj, 'location') and asset_obj.location:
+ location = asset_obj.location
+ if hasattr(asset_obj, 'structure') and asset_obj.structure:
+ structure = asset_obj.structure
+ if location or structure:
+ break
# 3. Fallback: match by location_code / address if structure exists
if not structure and not location and intervention.location_code:
@@ -54,6 +68,8 @@ def get_next_structure_asset_code(
struct_match = Structure.objects.filter(code__iexact=intervention.location_code).first()
if struct_match:
structure = struct_match
+ if struct_match.main_location:
+ location = struct_match.main_location
if location_id and not location:
try:
@@ -73,22 +89,41 @@ def get_next_structure_asset_code(
if not structure and not location:
return ""
- # Fetch existing asset codes for this structure or location
- if structure:
- codes = list(StructureGeoAsset.objects.filter(structure=structure).values_list('code', flat=True))
+ # Target prefix is the location code if location is present, otherwise structure code
+ target_prefix_base = location.code if location and location.code else (structure.code if structure and structure.code else "")
+ if not target_prefix_base:
+ return ""
+
+ # Look for existing asset codes
+ # Priority: codes matching this specific location / prefix
+ if location:
+ codes = list(
+ StructureGeoAsset.objects.filter(
+ Q(location=location) | Q(code__istartswith=location.code)
+ ).values_list('code', flat=True)
+ )
+ elif structure:
+ codes = list(
+ StructureGeoAsset.objects.filter(
+ Q(structure=structure) | Q(code__istartswith=structure.code)
+ ).values_list('code', flat=True)
+ )
else:
- codes = list(StructureGeoAsset.objects.filter(location=location).values_list('code', flat=True))
+ codes = []
pattern = re.compile(r'^(.*?)(\d+)$')
matches = []
for c in codes:
if not c:
continue
- m = pattern.match(c.strip())
+ c_str = c.strip()
+ m = pattern.match(c_str)
if m:
prefix = m.group(1)
num_str = m.group(2)
- matches.append((prefix, int(num_str), len(num_str)))
+ # Only consider codes that start with our target prefix (case-insensitive)
+ if prefix.upper().startswith(target_prefix_base.upper()):
+ matches.append((prefix, int(num_str), len(num_str)))
if matches:
prefix_counts = Counter(m[0] for m in matches)
@@ -104,12 +139,27 @@ def get_next_structure_asset_code(
suggested_code = f"{dominant_prefix}{str(next_num).zfill(digit_len)}"
return suggested_code
else:
- struct_code = structure.code if structure else (location.code if location else "STR")
+ # Default code formatting: determine digit length and separator from existing structure assets if any
+ digit_len = 6
+ separator = "-"
+ if structure:
+ struct_codes = list(StructureGeoAsset.objects.filter(structure=structure).values_list('code', flat=True))
+ struct_matches = [pattern.match(sc.strip()) for sc in struct_codes if sc and pattern.match(sc.strip())]
+ if struct_matches:
+ digit_len = max(len(sm.group(2)) for sm in struct_matches)
+ sample_prefix = struct_matches[0].group(1)
+ if sample_prefix.endswith('-'):
+ separator = "-"
+ elif sample_prefix.endswith('_'):
+ separator = "_"
+ elif not sample_prefix[-1].isalnum():
+ separator = sample_prefix[-1]
+
next_num = 1
- suggested_code = f"{struct_code}-{str(next_num).zfill(6)}"
+ suggested_code = f"{target_prefix_base}{separator}{str(next_num).zfill(digit_len)}"
while StructureGeoAsset.objects.filter(code=suggested_code).exists():
next_num += 1
- suggested_code = f"{struct_code}-{str(next_num).zfill(6)}"
+ suggested_code = f"{target_prefix_base}{separator}{str(next_num).zfill(digit_len)}"
return suggested_code
diff --git a/loko/assets/views/asset_editing.py b/loko/assets/views/asset_editing.py
index 000e3ef..eaf7632 100644
--- a/loko/assets/views/asset_editing.py
+++ b/loko/assets/views/asset_editing.py
@@ -29,6 +29,15 @@ from interventions.models import Intervention, InterventionAsset
from assets.views.locations import ASSETS_LOCATION_REGISTRY
+def _has_model_field(model_or_instance, field_name):
+ """Safely check if a model class or instance has a specific field in its _meta."""
+ try:
+ model_or_instance._meta.get_field(field_name)
+ return True
+ except Exception:
+ return False
+
+
# -----------------------------------------------------------------------
# Asset Document API
# -----------------------------------------------------------------------
@@ -640,7 +649,7 @@ def replace_asset(request, asset_model, asset_id):
setattr(new_asset, field_name, value)
# Handle new model if specified
- if new_model_id and hasattr(new_asset, 'model'):
+ if new_model_id and _has_model_field(new_asset, 'model'):
try:
model_field = model_class._meta.get_field('model')
related_model = model_field.related_model
@@ -1408,13 +1417,22 @@ def _get_asset_classes_for_category(thematic_code, category):
ct = allowed_cts.first()
asset_class = ct.model_class()
if asset_class:
- import importlib
- model_class_name = f"{asset_class.__name__}Model"
- try:
- module = importlib.import_module(asset_class.__module__)
- asset_model_class = getattr(module, model_class_name, None)
- except Exception:
- asset_model_class = None
+ asset_model_class = None
+ if hasattr(asset_class, '_meta'):
+ try:
+ model_field = asset_class._meta.get_field('model')
+ if hasattr(model_field, 'related_model'):
+ asset_model_class = model_field.related_model
+ except Exception:
+ asset_model_class = None
+ if not asset_model_class:
+ import importlib
+ model_class_name = f"{asset_class.__name__}Model"
+ try:
+ module = importlib.import_module(asset_class.__module__)
+ asset_model_class = getattr(module, model_class_name, None)
+ except Exception:
+ asset_model_class = None
return asset_class, asset_model_class
category_code = category.code if category and category.code else None
@@ -1451,6 +1469,9 @@ def _import_asset_classes(thematic_code, class_names):
'artworks': 'assets.models.artworks',
'sign': 'assets.models.sign',
'roads': 'assets.models.roads',
+ 'cleanliness': 'assets.models.cleanliness',
+ 'buildings': 'assets.models.buildings',
+ 'publicbuildings': 'assets.models.buildings',
}
module_name = thematic_module_map.get(thematic_code)
@@ -1462,6 +1483,13 @@ def _import_asset_classes(thematic_code, class_names):
module = importlib.import_module(module_name)
asset_class = getattr(module, asset_class_name, None)
model_class = getattr(module, model_class_name, None) if model_class_name else None
+ if asset_class and not model_class and hasattr(asset_class, '_meta'):
+ try:
+ model_field = asset_class._meta.get_field('model')
+ if hasattr(model_field, 'related_model'):
+ model_class = model_field.related_model
+ except Exception:
+ pass
return asset_class, model_class
except (ImportError, AttributeError):
return None, None
@@ -1469,8 +1497,6 @@ def _import_asset_classes(thematic_code, class_names):
def _get_asset_model_class_for_thematic(thematic_code):
"""Get the appropriate asset model class for a thematic."""
- from django.apps import apps
-
# Direct imports for known thematics
if thematic_code == 'structures':
from assets.models.structures import StructureGeoAsset, StructureAssetModel
@@ -1490,23 +1516,57 @@ def _get_asset_model_class_for_thematic(thematic_code):
elif thematic_code == 'its':
from assets.models.its import ITSGeoAsset, ITSAssetModel
return ITSGeoAsset, ITSAssetModel
+ elif thematic_code == 'artworks':
+ from assets.models.artworks import ArtworkFountain
+ return ArtworkFountain, None
+ elif thematic_code == 'sign':
+ from assets.models.sign import SignPole, SignPoleModel
+ return SignPole, SignPoleModel
+ elif thematic_code == 'roads':
+ from assets.models.roads import Roadway, RoadwayModel
+ return Roadway, RoadwayModel
+ elif thematic_code in ('publicbuildings', 'buildings'):
+ from assets.models.buildings import BuildingAsset
+ return BuildingAsset, None
+ elif thematic_code == 'cleanliness':
+ from assets.models.cleanliness import CleanLitterBin, CleanLitterBinModel
+ return CleanLitterBin, CleanLitterBinModel
return None, None
def _get_asset_model_instance(thematic_code, model_id, category=None):
"""Get the asset model instance for a given thematic, model ID, and optionally category."""
+ if not model_id:
+ return None
+
# Try category-specific lookup first
+ asset_model_class = None
if category:
_, asset_model_class = _get_asset_classes_for_category(thematic_code, category)
- else:
+ if not asset_model_class:
_, asset_model_class = _get_asset_model_class_for_thematic(thematic_code)
if asset_model_class and model_id:
try:
return asset_model_class.objects.get(pk=model_id)
except asset_model_class.DoesNotExist:
- return None
+ pass
+
+ # Universal fallback: search across all AbstractAssetModel subclasses
+ from django.apps import apps
+ from assets.models.core import AbstractAssetModel
+ for model in apps.get_models():
+ if model._meta.abstract:
+ continue
+ try:
+ if issubclass(model, AbstractAssetModel):
+ instance = model.objects.filter(pk=model_id).first()
+ if instance:
+ return instance
+ except (TypeError, Exception):
+ continue
+
return None
@@ -1590,7 +1650,22 @@ def create_asset_api(request):
}, status=404)
allowed_actions = get_allowed_actions_for_thematic(request.user, thematic)
- if 'create' not in allowed_actions:
+ can_create = 'create' in allowed_actions
+
+ # If creating from an intervention, check if user has permission to edit assets on this intervention
+ intervention = None
+ if intervention_id:
+ try:
+ intervention = Intervention.objects.get(pk=intervention_id)
+ from interventions.permissions import get_allowed_update_fields, can_edit_intervention
+ if can_edit_intervention(request.user, intervention):
+ intv_allowed_fields = get_allowed_update_fields(request.user, intervention)
+ if 'assets' in intv_allowed_fields:
+ can_create = True
+ except Intervention.DoesNotExist:
+ intervention = None
+
+ if not can_create:
return JsonResponse({
'success': False,
'error': _("Vous n'avez pas la permission de créer des équipements")
@@ -1639,23 +1714,55 @@ def create_asset_api(request):
'error': _("Modèle requis")
}, status=400)
- # If creating from an intervention without explicit location, get the first location from intervention
- intervention = None
- if intervention_id:
- try:
- intervention = Intervention.objects.get(pk=intervention_id)
+ # If creating from an intervention without explicit location, resolve location/structure from intervention
+ if intervention and not location_id and not structure_id:
+ for loc_link in intervention.locations.all():
+ loc_obj = loc_link.location
+ if loc_obj:
+ if thematic_code == 'structures':
+ from assets.models.structures import StructureLocation, Structure
+ if isinstance(loc_obj, StructureLocation):
+ location_id = loc_obj.id
+ structure_id = getattr(loc_obj, 'structure_id', None)
+ break
+ elif isinstance(loc_obj, Structure):
+ structure_id = loc_obj.id
+ if loc_obj.main_location:
+ location_id = loc_obj.main_location.id
+ break
+ elif hasattr(loc_obj, 'structure'):
+ location_id = loc_obj.id
+ structure_id = getattr(loc_obj, 'structure_id', None)
+ break
+ elif thematic_code == 'trafficlights':
+ from assets.models.trafficlights import TrafficLightIntersection
+ if isinstance(loc_obj, TrafficLightIntersection) or hasattr(loc_obj, 'pk'):
+ location_id = loc_obj.id
+ break
+ else:
+ if hasattr(loc_obj, 'pk'):
+ location_id = loc_obj.id
+ break
- # If no location_id specified, try to get first location from intervention
- if not location_id and not structure_id:
- from interventions.models import InterventionLocation
- first_location_link = intervention.locations.first()
-
- if first_location_link:
- # Set location_id or structure_id based on what we find
- location_id = first_location_link.object_id
- # We'll use the location's content type to determine what to set
- except Intervention.DoesNotExist:
- intervention = None
+ # Fallback: match by location_code on intervention
+ if not location_id and not structure_id and intervention.location_code:
+ if thematic_code == 'structures':
+ from assets.models.structures import StructureLocation, Structure
+ loc_match = StructureLocation.objects.filter(code__iexact=intervention.location_code).first()
+ if loc_match:
+ location_id = loc_match.id
+ structure_id = getattr(loc_match, 'structure_id', None)
+ else:
+ struct_match = Structure.objects.filter(code__iexact=intervention.location_code).first()
+ if struct_match:
+ structure_id = struct_match.id
+ if struct_match.main_location:
+ location_id = struct_match.main_location.id
+ elif thematic_code == 'trafficlights':
+ from assets.models.trafficlights import TrafficLightIntersection
+ inter_match = TrafficLightIntersection.objects.filter(code__iexact=intervention.location_code).first()
+ if inter_match:
+ location_id = inter_match.id
with transaction.atomic():
# Create the new asset
@@ -1663,16 +1770,16 @@ def create_asset_api(request):
new_asset.code = code
# Only set name fields if the model has them
- if hasattr(new_asset, 'name_fr'):
+ if _has_model_field(new_asset, 'name_fr'):
new_asset.name_fr = name_fr if name_fr else None
- if hasattr(new_asset, 'name_nl'):
+ if _has_model_field(new_asset, 'name_nl'):
new_asset.name_nl = name_nl if name_nl else None
# Only set category if the model has it
- if hasattr(new_asset, 'category'):
+ if _has_model_field(new_asset, 'category'):
new_asset.category = category
- if hasattr(new_asset, 'model'):
+ if _has_model_field(new_asset, 'model'):
new_asset.model = asset_model_instance
new_asset.status = 'active'
new_asset.validation_status = 'to_be_validated'
@@ -1691,7 +1798,7 @@ def create_asset_api(request):
pass
# Handle structure (for structures)
- if structure_id:
+ if structure_id and not getattr(new_asset, 'structure', None):
from assets.models.structures import Structure
try:
structure = Structure.objects.get(pk=structure_id)
@@ -1700,7 +1807,7 @@ def create_asset_api(request):
pass
# Handle intersection (for trafficlights)
- if thematic_code == 'trafficlights':
+ elif thematic_code == 'trafficlights':
intersection_id = data.get('intersection_id') or location_id
if not intersection_id:
@@ -1725,20 +1832,33 @@ def create_asset_api(request):
from assets.models.trafficlights import TrafficLightPole
try:
pole = TrafficLightPole.objects.get(pk=pole_id)
- if hasattr(new_asset, 'pole'):
+ if _has_model_field(new_asset, 'pole'):
new_asset.pole = pole
except TrafficLightPole.DoesNotExist:
pass
+ # Handle generic location for other thematics
+ elif location_id:
+ for field in asset_class._meta.get_fields():
+ if hasattr(field, 'related_model') and field.related_model and not field.auto_created:
+ try:
+ loc_instance = field.related_model.objects.get(pk=location_id)
+ setattr(new_asset, field.name, loc_instance)
+ break
+ except Exception:
+ continue
+
# Set additional data
extra_data = data.get('data', {})
if isinstance(extra_data, dict):
for field_name, value in extra_data.items():
- if hasattr(new_asset, field_name) and field_name not in ('id', 'code'):
+ if _has_model_field(new_asset, field_name) and field_name not in ('id', 'code'):
setattr(new_asset, field_name, value)
# Assign position from location / hierarchy
location = getattr(new_asset, 'location', None) or getattr(new_asset, 'intersection', None)
+ if not location and getattr(new_asset, 'structure', None):
+ location = getattr(new_asset.structure, 'main_location', None)
_assign_default_position(new_asset, location, thematic_code)
new_asset.save()