feat: implement dynamic filtering for contracts and contract posts in the intervention admin interface
This commit is contained in:
parent
5bfde04f1a
commit
0d35e71cfc
5 changed files with 401 additions and 155 deletions
|
|
@ -784,7 +784,17 @@ class OperationTemplateContractPostInline(admin.TabularInline):
|
||||||
fields = ('contract', 'contract_post', 'default_quantity', 'comment')
|
fields = ('contract', 'contract_post', 'default_quantity', 'comment')
|
||||||
verbose_name = "Poste de contrat par défaut"
|
verbose_name = "Poste de contrat par défaut"
|
||||||
verbose_name_plural = "Postes de contrats par défaut"
|
verbose_name_plural = "Postes de contrats par défaut"
|
||||||
autocomplete_fields = ('contract', 'contract_post')
|
|
||||||
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
|
if db_field.name == "contract":
|
||||||
|
template_id = request.resolver_match.kwargs.get('object_id') if request.resolver_match else None
|
||||||
|
if template_id:
|
||||||
|
template = OperationTemplate.objects.filter(pk=template_id).first()
|
||||||
|
if template and template.thematic_id:
|
||||||
|
kwargs["queryset"] = Contract.objects.filter(
|
||||||
|
thematics=template.thematic
|
||||||
|
).distinct().select_related('company').order_by('contract_number')
|
||||||
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(OperationTemplate)
|
@admin.register(OperationTemplate)
|
||||||
|
|
@ -833,8 +843,21 @@ class ContractOperationTemplatePostAdmin(admin.ModelAdmin):
|
||||||
'operation_template__name',
|
'operation_template__name',
|
||||||
'contract_post__code',
|
'contract_post__code',
|
||||||
'contract_post__description_fr',
|
'contract_post__description_fr',
|
||||||
|
'contract_post__description_nl',
|
||||||
)
|
)
|
||||||
autocomplete_fields = ('contract', 'operation_template', 'contract_post')
|
autocomplete_fields = ('operation_template',)
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ('interventions/admin_operation_template_dynamic.js',)
|
||||||
|
|
||||||
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
|
if db_field.name == "contract_post":
|
||||||
|
obj_id = request.resolver_match.kwargs.get('object_id') if request.resolver_match else None
|
||||||
|
if obj_id:
|
||||||
|
obj = ContractOperationTemplatePost.objects.filter(pk=obj_id).first()
|
||||||
|
if obj and obj.contract_id:
|
||||||
|
kwargs["queryset"] = ContractPost.objects.filter(contract_id=obj.contract_id).order_by('order_number')
|
||||||
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(OperationTemplateItem)
|
@admin.register(OperationTemplateItem)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Dynamic filtering for OperationTemplate admin form
|
* Dynamic filtering for OperationTemplate and ContractOperationTemplatePost admin forms:
|
||||||
* Updates categories based on thematic selection
|
* - Updates categories based on thematic selection
|
||||||
* Updates asset models based on categories selection
|
* - Updates asset models based on categories selection
|
||||||
|
* - Updates teams based on thematic selection
|
||||||
|
* - Updates inline contracts based on thematic selection
|
||||||
|
* - Updates contract posts dynamically based on selected contract (inlines and standalone form)
|
||||||
*/
|
*/
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -16,172 +19,295 @@
|
||||||
var $ = django.jQuery;
|
var $ = django.jQuery;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var $thematic = $('#id_thematic');
|
var $thematic = $('#id_thematic');
|
||||||
var $team = $('#id_team');
|
var $team = $('#id_team');
|
||||||
var $categoriesFrom = $('#id_categories_from');
|
var $categoriesFrom = $('#id_categories_from');
|
||||||
var $categoriesTo = $('#id_categories_to');
|
var $categoriesTo = $('#id_categories_to');
|
||||||
var $assetModelsFrom = $('#id_asset_models_from');
|
var $assetModelsFrom = $('#id_asset_models_from');
|
||||||
var $assetModelsTo = $('#id_asset_models_to');
|
var $assetModelsTo = $('#id_asset_models_to');
|
||||||
|
|
||||||
if (!$thematic.length) return;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update teams based on selected thematic
|
|
||||||
*/
|
|
||||||
function updateTeams() {
|
|
||||||
if (!$team.length) return;
|
|
||||||
var thematicId = $thematic.val();
|
|
||||||
var currentTeamVal = $team.val();
|
|
||||||
|
|
||||||
$.ajax({
|
/**
|
||||||
url: '/interventions/ajax/get-teams-by-thematic/',
|
* Update teams based on selected thematic
|
||||||
data: { thematic_id: thematicId || '' },
|
*/
|
||||||
success: function(data) {
|
function updateTeams() {
|
||||||
$team.empty();
|
if (!$team.length || !$thematic.length) return;
|
||||||
$team.append($('<option></option>').val('').text('---------'));
|
var thematicId = $thematic.val();
|
||||||
var hasCurrentVal = false;
|
var currentTeamVal = $team.val();
|
||||||
data.teams.forEach(function(team) {
|
|
||||||
var option = $('<option></option>')
|
$.ajax({
|
||||||
.val(team.id)
|
url: '/interventions/ajax/get-teams-by-thematic/',
|
||||||
.text(team.name);
|
data: { thematic_id: thematicId || '' },
|
||||||
if (String(team.id) === String(currentTeamVal)) {
|
success: function(data) {
|
||||||
option.prop('selected', true);
|
$team.empty();
|
||||||
hasCurrentVal = true;
|
$team.append($('<option></option>').val('').text('---------'));
|
||||||
|
var hasCurrentVal = false;
|
||||||
|
data.teams.forEach(function(team) {
|
||||||
|
var option = $('<option></option>')
|
||||||
|
.val(team.id)
|
||||||
|
.text(team.name);
|
||||||
|
if (String(team.id) === String(currentTeamVal)) {
|
||||||
|
option.prop('selected', true);
|
||||||
|
hasCurrentVal = true;
|
||||||
|
}
|
||||||
|
$team.append(option);
|
||||||
|
});
|
||||||
|
if (!hasCurrentVal && currentTeamVal) {
|
||||||
|
$team.val('');
|
||||||
}
|
}
|
||||||
$team.append(option);
|
},
|
||||||
});
|
error: function() {
|
||||||
if (!hasCurrentVal && currentTeamVal) {
|
console.error('Error fetching teams');
|
||||||
$team.val('');
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
error: function() {
|
|
||||||
console.error('Error fetching teams');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update categories based on selected thematic
|
|
||||||
*/
|
|
||||||
function updateCategories() {
|
|
||||||
var thematicId = $thematic.val();
|
|
||||||
|
|
||||||
if (!thematicId) {
|
|
||||||
// Clear categories if no thematic selected
|
|
||||||
$categoriesFrom.empty();
|
|
||||||
$categoriesTo.empty();
|
|
||||||
updateAssetModels();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current selections
|
/**
|
||||||
var selectedCategories = $categoriesTo.find('option').map(function() {
|
* Update categories based on selected thematic
|
||||||
return $(this).val();
|
*/
|
||||||
}).get();
|
function updateCategories() {
|
||||||
|
if (!$thematic.length || !$categoriesFrom.length) return;
|
||||||
// Fetch filtered categories
|
var thematicId = $thematic.val();
|
||||||
$.ajax({
|
|
||||||
url: '/interventions/ajax/get-categories-by-thematic/',
|
if (!thematicId) {
|
||||||
data: { thematic_id: thematicId },
|
// Clear categories if no thematic selected
|
||||||
success: function(data) {
|
|
||||||
// Clear both lists
|
|
||||||
$categoriesFrom.empty();
|
$categoriesFrom.empty();
|
||||||
$categoriesTo.empty();
|
$categoriesTo.empty();
|
||||||
|
|
||||||
// Populate categories
|
|
||||||
data.categories.forEach(function(cat) {
|
|
||||||
var option = $('<option></option>')
|
|
||||||
.val(cat.id)
|
|
||||||
.text(cat.name);
|
|
||||||
|
|
||||||
// If previously selected, add to "chosen" list
|
|
||||||
if (selectedCategories.indexOf(String(cat.id)) !== -1) {
|
|
||||||
$categoriesTo.append(option);
|
|
||||||
} else {
|
|
||||||
$categoriesFrom.append(option);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update asset models after categories change
|
|
||||||
updateAssetModels();
|
updateAssetModels();
|
||||||
},
|
return;
|
||||||
error: function() {
|
|
||||||
console.error('Error fetching categories');
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
// Get current selections
|
||||||
|
var selectedCategories = $categoriesTo.find('option').map(function() {
|
||||||
/**
|
return $(this).val();
|
||||||
* Update asset models based on selected categories (or thematic if no categories)
|
}).get();
|
||||||
*/
|
|
||||||
function updateAssetModels() {
|
// Fetch filtered categories
|
||||||
var thematicId = $thematic.val();
|
$.ajax({
|
||||||
var categoryIds = $categoriesTo.find('option').map(function() {
|
url: '/interventions/ajax/get-categories-by-thematic/',
|
||||||
return $(this).val();
|
data: { thematic_id: thematicId },
|
||||||
}).get();
|
success: function(data) {
|
||||||
|
// Clear both lists
|
||||||
if (!thematicId) {
|
$categoriesFrom.empty();
|
||||||
// Clear asset models if no thematic selected
|
$categoriesTo.empty();
|
||||||
$assetModelsFrom.empty();
|
|
||||||
$assetModelsTo.empty();
|
// Populate categories
|
||||||
return;
|
data.categories.forEach(function(cat) {
|
||||||
|
var option = $('<option></option>')
|
||||||
|
.val(cat.id)
|
||||||
|
.text(cat.name);
|
||||||
|
|
||||||
|
// If previously selected, add to "chosen" list
|
||||||
|
if (selectedCategories.indexOf(String(cat.id)) !== -1) {
|
||||||
|
$categoriesTo.append(option);
|
||||||
|
} else {
|
||||||
|
$categoriesFrom.append(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update asset models after categories change
|
||||||
|
updateAssetModels();
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
console.error('Error fetching categories');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current selections
|
/**
|
||||||
var selectedModels = $assetModelsTo.find('option').map(function() {
|
* Update asset models based on selected categories (or thematic if no categories)
|
||||||
return $(this).val();
|
*/
|
||||||
}).get();
|
function updateAssetModels() {
|
||||||
|
if (!$thematic.length || !$assetModelsFrom.length) return;
|
||||||
// Fetch filtered asset models
|
var thematicId = $thematic.val();
|
||||||
$.ajax({
|
var categoryIds = $categoriesTo.find('option').map(function() {
|
||||||
url: '/interventions/ajax/get-asset-models/',
|
return $(this).val();
|
||||||
data: {
|
}).get();
|
||||||
thematic_id: thematicId,
|
|
||||||
category_ids: categoryIds.join(',')
|
if (!thematicId) {
|
||||||
},
|
// Clear asset models if no thematic selected
|
||||||
success: function(data) {
|
|
||||||
// Clear both lists
|
|
||||||
$assetModelsFrom.empty();
|
$assetModelsFrom.empty();
|
||||||
$assetModelsTo.empty();
|
$assetModelsTo.empty();
|
||||||
|
return;
|
||||||
// Populate asset models
|
|
||||||
data.models.forEach(function(model) {
|
|
||||||
var option = $('<option></option>')
|
|
||||||
.val(model.value)
|
|
||||||
.text(model.display);
|
|
||||||
|
|
||||||
// If previously selected, add to "chosen" list
|
|
||||||
if (selectedModels.indexOf(model.value) !== -1) {
|
|
||||||
$assetModelsTo.append(option);
|
|
||||||
} else {
|
|
||||||
$assetModelsFrom.append(option);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
console.error('Error fetching asset models');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current selections
|
||||||
|
var selectedModels = $assetModelsTo.find('option').map(function() {
|
||||||
|
return $(this).val();
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
// Fetch filtered asset models
|
||||||
|
$.ajax({
|
||||||
|
url: '/interventions/ajax/get-asset-models/',
|
||||||
|
data: {
|
||||||
|
thematic_id: thematicId,
|
||||||
|
category_ids: categoryIds.join(',')
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
// Clear both lists
|
||||||
|
$assetModelsFrom.empty();
|
||||||
|
$assetModelsTo.empty();
|
||||||
|
|
||||||
|
// Populate asset models
|
||||||
|
data.models.forEach(function(model) {
|
||||||
|
var option = $('<option></option>')
|
||||||
|
.val(model.value)
|
||||||
|
.text(model.display);
|
||||||
|
|
||||||
|
// If previously selected, add to "chosen" list
|
||||||
|
if (selectedModels.indexOf(model.value) !== -1) {
|
||||||
|
$assetModelsTo.append(option);
|
||||||
|
} else {
|
||||||
|
$assetModelsFrom.append(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
console.error('Error fetching asset models');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update contract options in inlines based on selected thematic
|
||||||
|
*/
|
||||||
|
function updateContractsInlines() {
|
||||||
|
if (!$thematic.length) return;
|
||||||
|
var thematicId = $thematic.val();
|
||||||
|
$.ajax({
|
||||||
|
url: '/interventions/ajax/get-contracts-by-thematic/',
|
||||||
|
data: { thematic_id: thematicId || '' },
|
||||||
|
success: function(data) {
|
||||||
|
$('select[name$="-contract"]').each(function() {
|
||||||
|
var $cSelect = $(this);
|
||||||
|
var currentVal = $cSelect.val();
|
||||||
|
$cSelect.empty();
|
||||||
|
$cSelect.append($('<option></option>').val('').text('---------'));
|
||||||
|
var hasCurrent = false;
|
||||||
|
data.contracts.forEach(function(c) {
|
||||||
|
var opt = $('<option></option>').val(c.id).text(c.label);
|
||||||
|
if (String(c.id) === String(currentVal)) {
|
||||||
|
opt.prop('selected', true);
|
||||||
|
hasCurrent = true;
|
||||||
|
}
|
||||||
|
$cSelect.append(opt);
|
||||||
|
});
|
||||||
|
if (!hasCurrent && currentVal) {
|
||||||
|
$cSelect.val('');
|
||||||
|
$cSelect.trigger('change.dynamicPosts');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update posts dropdown based on selected contract
|
||||||
|
*/
|
||||||
|
function updatePostsForContract($contractSelect, $postSelect, keepSelectedVal) {
|
||||||
|
var contractId = $contractSelect.val();
|
||||||
|
var currentPostVal = keepSelectedVal !== undefined ? keepSelectedVal : $postSelect.val();
|
||||||
|
|
||||||
|
if (!contractId) {
|
||||||
|
$postSelect.empty();
|
||||||
|
$postSelect.append($('<option></option>').val('').text('---------'));
|
||||||
|
$postSelect.val('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/interventions/ajax/get-posts-by-contract/',
|
||||||
|
data: { contract_id: contractId },
|
||||||
|
success: function(data) {
|
||||||
|
$postSelect.empty();
|
||||||
|
$postSelect.append($('<option></option>').val('').text('---------'));
|
||||||
|
var matched = false;
|
||||||
|
if (data.posts && data.posts.length > 0) {
|
||||||
|
data.posts.forEach(function(post) {
|
||||||
|
var option = $('<option></option>')
|
||||||
|
.val(post.id)
|
||||||
|
.text(post.label);
|
||||||
|
if (String(post.id) === String(currentPostVal)) {
|
||||||
|
option.prop('selected', true);
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
$postSelect.append(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (matched) {
|
||||||
|
$postSelect.val(currentPostVal);
|
||||||
|
} else if (currentPostVal) {
|
||||||
|
$postSelect.val('');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
console.error('Error fetching contract posts');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindContractPostRow($row) {
|
||||||
|
var $contract = $row.find('select[name$="-contract"]');
|
||||||
|
var $post = $row.find('select[name$="-contract_post"]');
|
||||||
|
if (!$contract.length || !$post.length) return;
|
||||||
|
|
||||||
|
$contract.off('change.dynamicPosts').on('change.dynamicPosts', function() {
|
||||||
|
updatePostsForContract($contract, $post);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load for existing row: if contract is selected, populate options
|
||||||
|
if ($contract.val()) {
|
||||||
|
var initialPostVal = $post.val();
|
||||||
|
updatePostsForContract($contract, $post, initialPostVal);
|
||||||
|
} else {
|
||||||
|
$post.empty().append($('<option></option>').val('').text('---------'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind all inline rows
|
||||||
|
$('tr.form-row, tr[id^="contract_posts-"], .dynamic-contract_posts, tbody tr').each(function() {
|
||||||
|
bindContractPostRow($(this));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
// Listen for new inline rows added dynamically in Django Admin
|
||||||
// Listen to thematic changes
|
$(document).on('formset:added', function(event, $row, formsetName) {
|
||||||
$thematic.on('change', function() {
|
bindContractPostRow($row);
|
||||||
updateCategories();
|
|
||||||
updateTeams();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to categories changes using MutationObserver
|
|
||||||
// (filter_horizontal uses JavaScript to move options, not change events)
|
|
||||||
if ($categoriesTo.length) {
|
|
||||||
var observer = new MutationObserver(function(mutations) {
|
|
||||||
updateAssetModels();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Standalone form (ContractOperationTemplatePostAdmin)
|
||||||
|
var $standaloneContract = $('#id_contract');
|
||||||
|
var $standalonePost = $('#id_contract_post');
|
||||||
|
if ($standaloneContract.length && $standalonePost.length) {
|
||||||
|
$standaloneContract.on('change', function() {
|
||||||
|
updatePostsForContract($standaloneContract, $standalonePost);
|
||||||
|
});
|
||||||
|
if ($standaloneContract.val()) {
|
||||||
|
var initialPostVal = $standalonePost.val();
|
||||||
|
updatePostsForContract($standaloneContract, $standalonePost, initialPostVal);
|
||||||
|
} else {
|
||||||
|
$standalonePost.empty().append($('<option></option>').val('').text('---------'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
observer.observe($categoriesTo[0], {
|
// Listen to thematic changes
|
||||||
childList: true,
|
if ($thematic.length) {
|
||||||
subtree: true
|
$thematic.on('change', function() {
|
||||||
});
|
updateCategories();
|
||||||
}
|
updateTeams();
|
||||||
|
updateContractsInlines();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to categories changes using MutationObserver
|
||||||
|
if ($categoriesTo.length) {
|
||||||
|
var observer = new MutationObserver(function(mutations) {
|
||||||
|
updateAssetModels();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe($categoriesTo[0], {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1207,6 +1207,38 @@ class ContractOperationTemplatePostTests(TestCase):
|
||||||
self.assertIn(f'value="{order.id}"', content)
|
self.assertIn(f'value="{order.id}"', content)
|
||||||
self.assertIn('selected', content)
|
self.assertIn('selected', content)
|
||||||
|
|
||||||
|
def test_ajax_get_posts_by_contract(self):
|
||||||
|
"""Test the AJAX endpoint filtering contract posts by contract."""
|
||||||
|
self.client.login(username='test_user_posts', password='password123')
|
||||||
|
url = reverse('interventions:ajax_get_posts_by_contract')
|
||||||
|
|
||||||
|
# Call with no contract_id
|
||||||
|
res = self.client.get(url)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertEqual(res.json().get('posts'), [])
|
||||||
|
|
||||||
|
# Call with self.contract
|
||||||
|
res = self.client.get(url, {'contract_id': self.contract.id})
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
posts_data = res.json().get('posts', [])
|
||||||
|
post_ids = [p['id'] for p in posts_data]
|
||||||
|
self.assertIn(self.post_1.id, post_ids)
|
||||||
|
self.assertIn(self.post_2.id, post_ids)
|
||||||
|
self.assertNotIn(self.post_other.id, post_ids)
|
||||||
|
|
||||||
|
def test_ajax_get_contracts_by_thematic(self):
|
||||||
|
"""Test the AJAX endpoint filtering contracts by thematic."""
|
||||||
|
self.client.login(username='test_user_posts', password='password123')
|
||||||
|
url = reverse('interventions:ajax_get_contracts_by_thematic')
|
||||||
|
|
||||||
|
res = self.client.get(url, {'thematic_id': self.thematic.id})
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
contracts_data = res.json().get('contracts', [])
|
||||||
|
contract_ids = [c['id'] for c in contracts_data]
|
||||||
|
self.assertIn(self.contract.id, contract_ids)
|
||||||
|
self.assertNotIn(self.contract_other.id, contract_ids)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ urlpatterns = [
|
||||||
path('ajax/get-subcategories/', views.ajax_get_subcategories, name='ajax_get_subcategories'),
|
path('ajax/get-subcategories/', views.ajax_get_subcategories, name='ajax_get_subcategories'),
|
||||||
path('ajax/get-asset-models/', views.ajax_get_asset_models, name='ajax_get_asset_models'),
|
path('ajax/get-asset-models/', views.ajax_get_asset_models, name='ajax_get_asset_models'),
|
||||||
path('ajax/get-replacement-models-by-category/', views.ajax_get_replacement_models_by_category, name='ajax_get_replacement_models_by_category'),
|
path('ajax/get-replacement-models-by-category/', views.ajax_get_replacement_models_by_category, name='ajax_get_replacement_models_by_category'),
|
||||||
|
path('ajax/get-posts-by-contract/', views.ajax_get_posts_by_contract, name='ajax_get_posts_by_contract'),
|
||||||
|
path('ajax/get-contracts-by-thematic/', views.ajax_get_contracts_by_thematic, name='ajax_get_contracts_by_thematic'),
|
||||||
# Template check for assets without matching templates
|
# Template check for assets without matching templates
|
||||||
path('<int:intervention_id>/check-asset-templates/', views.check_asset_templates_for_intervention, name='check_asset_templates'),
|
path('<int:intervention_id>/check-asset-templates/', views.check_asset_templates_for_intervention, name='check_asset_templates'),
|
||||||
path('<int:intervention_id>/create-operations-from-template/', views.create_operations_from_selected_template, name='create_operations_from_template'),
|
path('<int:intervention_id>/create-operations-from-template/', views.create_operations_from_selected_template, name='create_operations_from_template'),
|
||||||
|
|
|
||||||
|
|
@ -1300,3 +1300,66 @@ def ajax_get_replacement_models_by_category(request):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'error': str(e)}, status=400)
|
return JsonResponse({'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def ajax_get_posts_by_contract(request):
|
||||||
|
"""AJAX endpoint to get contract posts for a given contract.
|
||||||
|
Used by Django Admin to dynamically filter contract posts based on selected contract.
|
||||||
|
"""
|
||||||
|
contract_id = request.GET.get('contract_id')
|
||||||
|
if not contract_id:
|
||||||
|
return JsonResponse({'posts': []})
|
||||||
|
|
||||||
|
try:
|
||||||
|
from contracts.models import ContractPost
|
||||||
|
posts = ContractPost.objects.filter(contract_id=contract_id).order_by('order_number')
|
||||||
|
lang = get_language()
|
||||||
|
posts_data = []
|
||||||
|
for post in posts:
|
||||||
|
desc = post.description_fr
|
||||||
|
if lang and lang.startswith('nl') and post.description_nl:
|
||||||
|
desc = post.description_nl
|
||||||
|
label_parts = []
|
||||||
|
if post.order_number:
|
||||||
|
label_parts.append(str(post.order_number))
|
||||||
|
if post.code:
|
||||||
|
label_parts.append(str(post.code))
|
||||||
|
if desc:
|
||||||
|
label_parts.append(desc)
|
||||||
|
label = " - ".join(label_parts) if label_parts else f"Poste #{post.id}"
|
||||||
|
posts_data.append({
|
||||||
|
'id': post.id,
|
||||||
|
'code': post.code,
|
||||||
|
'order_number': post.order_number,
|
||||||
|
'description': desc,
|
||||||
|
'label': label,
|
||||||
|
})
|
||||||
|
return JsonResponse({'posts': posts_data})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def ajax_get_contracts_by_thematic(request):
|
||||||
|
"""AJAX endpoint to get contracts for a given thematic.
|
||||||
|
Used by Django Admin to dynamically filter contracts based on selected thematic.
|
||||||
|
"""
|
||||||
|
thematic_id = request.GET.get('thematic_id')
|
||||||
|
try:
|
||||||
|
from contracts.models import Contract
|
||||||
|
if thematic_id:
|
||||||
|
contracts = Contract.objects.filter(thematics__id=thematic_id).distinct().select_related('company').order_by('contract_number')
|
||||||
|
else:
|
||||||
|
contracts = Contract.objects.all().select_related('company').order_by('contract_number')
|
||||||
|
contracts_data = [
|
||||||
|
{
|
||||||
|
'id': c.id,
|
||||||
|
'contract_number': c.contract_number,
|
||||||
|
'description': c.description or '',
|
||||||
|
'label': f"{c.contract_number} ({c.company.name})" if c.company else c.contract_number,
|
||||||
|
}
|
||||||
|
for c in contracts
|
||||||
|
]
|
||||||
|
return JsonResponse({'contracts': contracts_data})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue