877 lines
40 KiB
HTML
877 lines
40 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
{% load i18n %}
|
|
{% load stock_custom_filters %}
|
|
|
|
{% block content %}
|
|
<div class="container mt-3">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8 col-md-10 col-sm-12">
|
|
<h3 style="text-align: center;">{% translate "Nouveau Bon de Préparation" %}</h3>
|
|
|
|
<form method="post" enctype="multipart/form-data" id="preparation-order-form">
|
|
{% csrf_token %}
|
|
|
|
<div class="mb-3">
|
|
{{ form.non_field_errors }}
|
|
</div>
|
|
|
|
{% if from_intervention and intervention %}
|
|
<!-- Bloc regroupant l'origine de la demande -->
|
|
<div class="card mb-3 shadow">
|
|
<div class="card-header bg-info text-white">
|
|
<i class="bi bi-info-circle"></i> {% translate "Origine de la demande" %}
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<strong>{% translate "Intervention" %}:</strong>
|
|
<a href="{% url 'interventions:intervention_detail' intervention.id %}" target="_blank">
|
|
#{{ intervention.id }} - {{ intervention.title|default:intervention.description|truncatewords:10 }}
|
|
</a>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>{% translate "Priorité" %}:</strong>
|
|
{% if intervention.priority == '1' %}
|
|
<span class="badge bg-danger">{% translate "Urgent" %}</span>
|
|
{% elif intervention.priority == '2' %}
|
|
<span class="badge bg-warning">{% translate "Elevé" %}</span>
|
|
{% elif intervention.priority == '3' %}
|
|
<span class="badge bg-info">{% translate "Normal" %}</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{% translate "Bas" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-md-6">
|
|
<strong>{% translate "Statut" %}:</strong>
|
|
{{ intervention.get_status_display }}
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>{% translate "Date création" %}:</strong>
|
|
{{ intervention.created_date|date:"d/m/Y H:i" }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Bloc numéro et statut du bon -->
|
|
<div class="card mb-3 shadow">
|
|
<div class="card-header">
|
|
{% translate "Informations du bon" %}
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
{{ form.order_number.label_tag }}
|
|
{{ form.order_number|add_class:"form-control" }}
|
|
{{ form.order_number.errors }}
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
{{ form.status.label_tag }}
|
|
{{ form.status|add_class:"form-control" }}
|
|
{{ form.status.errors }}
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
{{ form.priority.label_tag }}
|
|
{{ form.priority|add_class:"form-select" }}
|
|
{{ form.priority.errors }}
|
|
</div>
|
|
</div>
|
|
{% if not from_intervention %}
|
|
<div class="row">
|
|
<div class="col-12 mb-3">
|
|
{{ form.requester.label_tag }}
|
|
{{ form.requester }}
|
|
<div class="form-text">{% translate "Optionnel — personne ou service à l'origine de la demande." %}</div>
|
|
{{ form.requester.errors }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div class="row mt-2">
|
|
<div class="col-md-6 mb-3 d-flex align-items-center">
|
|
<div class="form-check form-switch mt-3">
|
|
{{ form.is_loan }}
|
|
<label class="form-check-label fw-bold text-primary" for="{{ form.is_loan.auto_id }}">
|
|
{{ form.is_loan.label }}
|
|
</label>
|
|
</div>
|
|
{{ form.is_loan.errors }}
|
|
</div>
|
|
<div class="col-md-6 mb-3" id="estimated_return_date_wrapper" style="display: none;">
|
|
{{ form.estimated_return_date.label_tag }}
|
|
{{ form.estimated_return_date }}
|
|
{{ form.estimated_return_date.errors }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Bloc regroupant les produits à préparer -->
|
|
<div class="card mb-3 shadow">
|
|
<div class="card-header">
|
|
{% translate "Articles en stock" %}
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Zone de recherche et ajout -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<label for="product-search">{% translate "Rechercher un produit" %}</label>
|
|
<input type="text"
|
|
id="product-search"
|
|
class="form-control"
|
|
placeholder="{% translate "Tapez pour rechercher..." %}"
|
|
autocomplete="off">
|
|
<!-- Dropdown pour les résultats -->
|
|
<div id="search-results" class="list-group position-absolute" style="display: none; z-index: 1000; max-height: 300px; overflow-y: auto; width: calc(50% - 30px);"></div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="product-quantity" id="product-quantity-label">{% translate "Quantité" %}</label>
|
|
<input type="number"
|
|
id="product-quantity"
|
|
class="form-control"
|
|
value="1"
|
|
min="1"
|
|
placeholder="1">
|
|
</div>
|
|
<div class="col-md-3 d-flex align-items-end">
|
|
<button type="button" id="add-product-btn" class="btn btn-primary w-100">
|
|
<i class="bi bi-plus-circle"></i> {% translate "Ajouter" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Liste des produits ajoutés -->
|
|
<div id="products-list">
|
|
<h6 class="mb-3">{% translate "Produits sélectionnés" %}:</h6>
|
|
<div id="products-container">
|
|
<p class="text-muted text-center" id="empty-message">
|
|
<i class="bi bi-inbox"></i> {% translate "Aucun produit ajouté" %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Formset caché pour la soumission -->
|
|
<div style="display: none;">
|
|
{{ formset.management_form }}
|
|
<div id="formset-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Bloc "Autres produits" -->
|
|
<div class="card mb-3 shadow">
|
|
<div class="card-header">
|
|
<i class="bi bi-pencil-square"></i> {% translate "Autres produits" %}
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="text-muted small">
|
|
<i class="bi bi-info-circle"></i>
|
|
{% translate "Ajoutez des lignes pour des produits non référencés : décrivez librement, choisissez la catégorie et la quantité." %}
|
|
</p>
|
|
|
|
<!-- Formulaire d'ajout -->
|
|
<div class="row g-2 mb-3 align-items-start">
|
|
<div class="col-md-6">
|
|
<label for="other-description">{% translate "Description" %}</label>
|
|
<textarea
|
|
id="other-description"
|
|
class="form-control"
|
|
placeholder="{% translate 'Description du produit...' %}"
|
|
autocomplete="off"
|
|
rows="2"
|
|
></textarea>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="other-quantity">{% translate "Quantité" %}</label>
|
|
<input type="number"
|
|
id="other-quantity"
|
|
class="form-control"
|
|
value="1"
|
|
min="1">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="other-category">{% translate "Catégorie" %}</label>
|
|
<select id="other-category" class="form-select">
|
|
<option value="">--- {% translate "Catégorie" %} ---</option>
|
|
{% for cat in categories %}
|
|
<option value="{{ cat.id }}" data-code="{{ cat.code }}">{{ cat.name_fr }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2" id="other-signtype-wrapper" style="display:none;">
|
|
<label for="other-signtype">{% translate "Type" %}</label>
|
|
<input type="text"
|
|
id="other-signtype"
|
|
class="form-control"
|
|
placeholder="{% translate 'Code ou nom...' %}"
|
|
autocomplete="off">
|
|
<div id="other-signtype-results" class="list-group position-absolute" style="display:none;z-index:1002;max-height:250px;overflow-y:auto;"></div>
|
|
<input type="hidden" id="other-signtype-id" value="">
|
|
</div>
|
|
</div>
|
|
<div class="row mb-3">
|
|
<div class="col-auto">
|
|
<button type="button" id="add-other-btn" class="btn btn-light">
|
|
<i class="bi bi-plus-circle"></i>{% translate "Ajouter la ligne" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Liste des lignes ajoutées -->
|
|
<div id="other-items-list">
|
|
<p class="text-muted text-center" id="other-empty-message">
|
|
<i class="bi bi-inbox"></i> {% translate "Aucune ligne ajoutée" %}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Champs cachés pour soumission -->
|
|
<div style="display:none;">
|
|
<input type="hidden" id="other-TOTAL_FORMS" name="other-TOTAL_FORMS" value="0">
|
|
<div id="other-formset-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents joints -->
|
|
<div class="card mb-3 shadow">
|
|
<div class="card-header">
|
|
<i class="bi bi-paperclip me-1"></i>{% translate "Documents joints" %}
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="prep-drop-zone" class="drop-zone">
|
|
<i class="bi bi-cloud-upload fs-2 text-muted"></i>
|
|
<p class="mt-2 mb-1 fw-semibold">{% translate "Glissez-déposez vos fichiers ici" %}</p>
|
|
<p class="text-muted small">{% translate "ou" %}</p>
|
|
<label for="prep_documents_input" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-folder2-open me-1"></i>{% translate "Parcourir..." %}
|
|
</label>
|
|
<input type="file" id="prep_documents_input" name="documents" multiple class="d-none"
|
|
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.odt,.ods,.txt">
|
|
</div>
|
|
<div id="prep-file-list" class="mt-2"></div>
|
|
<div class="form-text">{% translate "Formats acceptés : images, PDF, Word, Excel, ODT, ODS, texte. Optionnel." %}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Envoi du formulaire -->
|
|
<div class="d-flex justify-content-between mt-4">
|
|
{% if from_intervention %}
|
|
<a href="{% url 'interventions:intervention_detail' intervention.id %}" class="btn btn-secondary">
|
|
<i class="bi bi-arrow-left"></i> {% translate "Retour à l'intervention" %}
|
|
</a>
|
|
{% else %}
|
|
<a href="{% url 'stock:stock_movements' %}" class="btn btn-secondary">
|
|
<i class="bi bi-arrow-left"></i> {% translate "Retour" %}
|
|
</a>
|
|
{% endif %}
|
|
<button type="submit" class="btn btn-success">
|
|
<i class="bi bi-check-circle"></i> {% translate "Créer le bon de préparation" %}
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock content %}
|
|
|
|
{% block script %}
|
|
<style>
|
|
#search-results .list-group-item {
|
|
cursor: pointer;
|
|
border-left: 3px solid transparent;
|
|
}
|
|
#search-results .list-group-item:hover {
|
|
background-color: #f8f9fa;
|
|
border-left-color: #0d6efd;
|
|
}
|
|
.product-card {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-bottom: 10px;
|
|
background: #f8f9fa;
|
|
}
|
|
.location-badge {
|
|
background-color: #e9ecef;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
margin-right: 5px;
|
|
font-size: 0.85rem;
|
|
}
|
|
.product-info {
|
|
font-size: 0.9rem;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Gestion du prêt de matériel
|
|
const isLoanCheckbox = document.getElementById('{{ form.is_loan.auto_id }}');
|
|
const returnDateWrapper = document.getElementById('estimated_return_date_wrapper');
|
|
const returnDateInput = document.getElementById('{{ form.estimated_return_date.auto_id }}');
|
|
|
|
function toggleReturnDateField() {
|
|
if (isLoanCheckbox && isLoanCheckbox.checked) {
|
|
returnDateWrapper.style.display = 'block';
|
|
if (returnDateInput) returnDateInput.required = true;
|
|
} else {
|
|
returnDateWrapper.style.display = 'none';
|
|
if (returnDateInput) {
|
|
returnDateInput.required = false;
|
|
returnDateInput.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isLoanCheckbox) {
|
|
isLoanCheckbox.addEventListener('change', toggleReturnDateField);
|
|
toggleReturnDateField(); // Etat initial
|
|
}
|
|
|
|
const productSearch = document.getElementById('product-search');
|
|
const searchResults = document.getElementById('search-results');
|
|
const productQuantity = document.getElementById('product-quantity');
|
|
const addProductBtn = document.getElementById('add-product-btn');
|
|
const productsContainer = document.getElementById('products-container');
|
|
const emptyMessage = document.getElementById('empty-message');
|
|
const formsetContainer = document.getElementById('formset-container');
|
|
const productsApiUrl = "{% url 'stock:products_autocomplete_api' %}";
|
|
|
|
// Détecter le préfixe du formset depuis le management form
|
|
const managementForm = formsetContainer.parentElement;
|
|
const totalFormsInput = managementForm.querySelector('[id$="-TOTAL_FORMS"]');
|
|
const formPrefix = totalFormsInput ? totalFormsInput.id.replace('-TOTAL_FORMS', '').replace('id_', '') : 'form';
|
|
|
|
console.log('Formset prefix detected:', formPrefix);
|
|
|
|
let selectedProduct = null;
|
|
let productsAdded = [];
|
|
let searchTimeout = null;
|
|
|
|
// Charger les produits existants du formset (en cas d'erreur de validation)
|
|
function loadExistingProducts() {
|
|
const productIds = [];
|
|
const productQuantities = {};
|
|
|
|
{% for form in formset %}
|
|
{% if form.product.value and form.quantity.value %}
|
|
productIds.push({{ form.product.value }});
|
|
productQuantities[{{ form.product.value }}] = {{ form.quantity.value }};
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
if (productIds.length > 0) {
|
|
// Récupérer les détails de tous les produits en une seule requête
|
|
fetch(`${productsApiUrl}?ids=${productIds.join(',')}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
data.results.forEach(product => {
|
|
productsAdded.push({
|
|
id: product.id,
|
|
text: product.text,
|
|
code: product.code,
|
|
category: product.category,
|
|
total_stock: product.total_stock,
|
|
reserved_quantity: product.reserved_quantity || 0,
|
|
available_stock: product.available_stock || product.total_stock,
|
|
threshold: product.threshold,
|
|
locations: product.locations,
|
|
unit: product.unit || 'pc',
|
|
unit_display: product.unit_display || 'Pièce(s)',
|
|
quantity: productQuantities[product.id]
|
|
});
|
|
});
|
|
renderProductsList();
|
|
})
|
|
.catch(error => console.error('Erreur de chargement des produits:', error));
|
|
}
|
|
}
|
|
|
|
// Charger les produits existants au démarrage
|
|
loadExistingProducts();
|
|
|
|
// Recherche de produits avec debounce
|
|
productSearch.addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
const query = this.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
searchResults.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
searchTimeout = setTimeout(() => {
|
|
fetch(`${productsApiUrl}?q=${encodeURIComponent(query)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
displaySearchResults(data.results);
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur de recherche:', error);
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
// Afficher les résultats de recherche
|
|
function displaySearchResults(results) {
|
|
searchResults.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
searchResults.innerHTML = '<div class="list-group-item">{% translate "Aucun produit trouvé" %}</div>';
|
|
searchResults.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
results.forEach(product => {
|
|
const item = document.createElement('div');
|
|
item.className = 'list-group-item list-group-item-action';
|
|
|
|
const stockBadge = product.total_stock > 0
|
|
? `<span class="badge bg-success ms-2">${product.total_stock} ${product.unit_display || 'en stock'}</span>`
|
|
: `<span class="badge bg-secondary ms-2">Stock vide</span>`;
|
|
|
|
item.innerHTML = `
|
|
<strong>${product.text}</strong>
|
|
<small class="text-muted">(${product.code})</small>
|
|
${stockBadge}
|
|
${product.category ? `<br><small class="text-muted">${product.category}</small>` : ''}
|
|
`;
|
|
|
|
item.addEventListener('click', () => {
|
|
selectProduct(product);
|
|
});
|
|
|
|
searchResults.appendChild(item);
|
|
});
|
|
|
|
searchResults.style.display = 'block';
|
|
}
|
|
|
|
// Sélectionner un produit
|
|
function selectProduct(product) {
|
|
selectedProduct = product;
|
|
productSearch.value = `${product.text} (${product.code})`;
|
|
searchResults.style.display = 'none';
|
|
// Update quantity label with unit
|
|
const qtyLabel = document.getElementById('product-quantity-label');
|
|
if (qtyLabel) {
|
|
qtyLabel.textContent = product.unit_display
|
|
? `{% translate "Quantité" %} (${product.unit_display})`
|
|
: `{% translate "Quantité" %}`;
|
|
}
|
|
productQuantity.focus();
|
|
}
|
|
|
|
// Fermer les résultats si on clique ailleurs
|
|
document.addEventListener('click', function(e) {
|
|
if (!productSearch.contains(e.target) && !searchResults.contains(e.target)) {
|
|
searchResults.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Ajouter un produit à la liste
|
|
addProductBtn.addEventListener('click', function() {
|
|
if (!selectedProduct) {
|
|
alert('{% translate "Veuillez sélectionner un produit" %}');
|
|
return;
|
|
}
|
|
|
|
const quantity = parseInt(productQuantity.value);
|
|
if (!quantity || quantity < 1) {
|
|
alert('{% translate "Veuillez entrer une quantité valide" %}');
|
|
return;
|
|
}
|
|
|
|
// Vérifier si le produit existe déjà
|
|
const existingIndex = productsAdded.findIndex(p => p.id === selectedProduct.id);
|
|
if (existingIndex !== -1) {
|
|
// Mettre à jour la quantité
|
|
productsAdded[existingIndex].quantity += quantity;
|
|
} else {
|
|
// Ajouter le nouveau produit
|
|
productsAdded.push({
|
|
id: selectedProduct.id,
|
|
text: selectedProduct.text,
|
|
code: selectedProduct.code,
|
|
category: selectedProduct.category,
|
|
total_stock: selectedProduct.total_stock,
|
|
reserved_quantity: selectedProduct.reserved_quantity || 0,
|
|
available_stock: selectedProduct.available_stock || selectedProduct.total_stock,
|
|
threshold: selectedProduct.threshold,
|
|
locations: selectedProduct.locations,
|
|
unit: selectedProduct.unit || 'pc',
|
|
unit_display: selectedProduct.unit_display || 'Pièce(s)',
|
|
quantity: quantity
|
|
});
|
|
}
|
|
|
|
// Réinitialiser les champs
|
|
productSearch.value = '';
|
|
productQuantity.value = '1';
|
|
selectedProduct = null;
|
|
|
|
// Mettre à jour l'affichage
|
|
renderProductsList();
|
|
});
|
|
|
|
// Afficher la liste des produits ajoutés
|
|
function renderProductsList() {
|
|
productsContainer.innerHTML = '';
|
|
|
|
if (productsAdded.length === 0) {
|
|
emptyMessage.style.display = 'block';
|
|
updateFormset();
|
|
return;
|
|
}
|
|
|
|
emptyMessage.style.display = 'none';
|
|
|
|
productsAdded.forEach((product, index) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'product-card';
|
|
|
|
// Construire les emplacements
|
|
let locationsHtml = '';
|
|
if (product.locations && product.locations.length > 0) {
|
|
locationsHtml = '<div class="mt-2"><small class="text-muted"><i class="bi bi-geo-alt"></i> Emplacements:</small><br>';
|
|
product.locations.forEach(loc => {
|
|
const stockBadge = loc.quantity > 0
|
|
? `<span class="badge bg-success">${loc.quantity}</span>`
|
|
: `<span class="badge bg-secondary">0</span>`;
|
|
locationsHtml += `<span class="location-badge">${loc.warehouse} - ${loc.name} ${stockBadge}</span>`;
|
|
});
|
|
locationsHtml += '</div>';
|
|
}
|
|
|
|
const stockClass = product.total_stock < product.threshold ? 'text-danger' : 'text-success';
|
|
|
|
card.innerHTML = `
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-1">${product.text}</h6>
|
|
<div class="product-info">
|
|
<span class="text-muted">Code: ${product.code}</span>
|
|
${product.category ? ` | <span class="text-muted">${product.category}</span>` : ''}
|
|
<br>
|
|
<span class="${stockClass}">
|
|
<i class="bi bi-box-seam"></i> Stock total: ${product.total_stock}
|
|
</span>
|
|
${product.reserved_quantity > 0 ? ` | <span class="text-warning"><i class="bi bi-lock"></i> Réservé: ${product.reserved_quantity}</span>` : ''}
|
|
<br>
|
|
<span class="text-${product.available_stock >= product.quantity ? 'success' : 'danger'}">
|
|
<i class="bi bi-check-circle"></i> Disponible: ${product.available_stock}
|
|
</span>
|
|
${product.total_stock < product.threshold ? ' <i class="bi bi-exclamation-triangle text-warning"></i>' : ''}
|
|
</div>
|
|
${locationsHtml}
|
|
</div>
|
|
<div class="text-end ms-3">
|
|
<div class="mb-2">
|
|
<strong class="fs-5">${product.quantity}</strong>
|
|
<small class="text-muted">${product.unit_display || 'unités'}</small>
|
|
</div>
|
|
<button type="button" class="btn btn-danger btn-sm" onclick="removeProduct(${index})">
|
|
<i class="bi bi-trash"></i> {% translate "Retirer" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
productsContainer.appendChild(card);
|
|
});
|
|
|
|
// Mettre à jour le formset caché
|
|
updateFormset();
|
|
}
|
|
|
|
// Supprimer un produit
|
|
window.removeProduct = function(index) {
|
|
productsAdded.splice(index, 1);
|
|
renderProductsList();
|
|
};
|
|
|
|
// Mettre à jour le formset caché pour la soumission
|
|
function updateFormset() {
|
|
formsetContainer.innerHTML = '';
|
|
const totalFormsInput = managementForm.querySelector('[id$="-TOTAL_FORMS"]');
|
|
totalFormsInput.value = productsAdded.length;
|
|
|
|
productsAdded.forEach((product, index) => {
|
|
const productInput = document.createElement('input');
|
|
productInput.type = 'hidden';
|
|
productInput.name = `${formPrefix}-${index}-product`;
|
|
productInput.value = product.id;
|
|
|
|
const quantityInput = document.createElement('input');
|
|
quantityInput.type = 'hidden';
|
|
quantityInput.name = `${formPrefix}-${index}-quantity`;
|
|
quantityInput.value = product.quantity;
|
|
|
|
const idInput = document.createElement('input');
|
|
idInput.type = 'hidden';
|
|
idInput.name = `${formPrefix}-${index}-id`;
|
|
idInput.value = '';
|
|
|
|
const deleteInput = document.createElement('input');
|
|
deleteInput.type = 'hidden';
|
|
deleteInput.name = `${formPrefix}-${index}-DELETE`;
|
|
deleteInput.value = '';
|
|
|
|
formsetContainer.appendChild(productInput);
|
|
formsetContainer.appendChild(quantityInput);
|
|
formsetContainer.appendChild(idInput);
|
|
formsetContainer.appendChild(deleteInput);
|
|
});
|
|
}
|
|
|
|
// Soumettre le formulaire - valider qu'il y a au moins un produit ou une autre ligne
|
|
document.getElementById('preparation-order-form').addEventListener('submit', function(e) {
|
|
if (productsAdded.length === 0 && otherItemsAdded.length === 0) {
|
|
e.preventDefault();
|
|
alert('{% translate "Veuillez ajouter au moins un produit ou une autre ligne" %}');
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Permettre d'ajouter avec Enter dans le champ quantité
|
|
productQuantity.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
addProductBtn.click();
|
|
}
|
|
});
|
|
|
|
// ================================
|
|
// Section "Autres produits"
|
|
// ================================
|
|
const otherDescription = document.getElementById('other-description');
|
|
const otherQuantity = document.getElementById('other-quantity');
|
|
const otherCategory = document.getElementById('other-category');
|
|
const otherSigntypeWrap = document.getElementById('other-signtype-wrapper');
|
|
const otherSigntype = document.getElementById('other-signtype');
|
|
const otherSigntypeRes = document.getElementById('other-signtype-results');
|
|
const otherSigntypeId = document.getElementById('other-signtype-id');
|
|
const addOtherBtn = document.getElementById('add-other-btn');
|
|
const otherItemsList = document.getElementById('other-items-list');
|
|
const otherEmptyMsg = document.getElementById('other-empty-message');
|
|
const otherTotalInput = document.getElementById('other-TOTAL_FORMS');
|
|
const otherFormset = document.getElementById('other-formset-container');
|
|
const signPanelTypesUrl = "{% url 'stock:sign_panel_types_api' %}";
|
|
|
|
// IDs des catégories "Signalisation"
|
|
const signalisationIds = new Set({{ signalisation_category_ids|default:"[]"|safe }});
|
|
|
|
let otherItemsAdded = [];
|
|
let signTypeTimeout = null;
|
|
let selectedSignType = null;
|
|
|
|
// Afficher/cacher le champ "type signalisation" selon la catégorie
|
|
otherCategory.addEventListener('change', function() {
|
|
const catId = parseInt(this.value) || 0;
|
|
if (signalisationIds.has(catId)) {
|
|
otherSigntypeWrap.style.display = '';
|
|
} else {
|
|
otherSigntypeWrap.style.display = 'none';
|
|
otherSigntype.value = '';
|
|
otherSigntypeId.value = '';
|
|
selectedSignType = null;
|
|
}
|
|
});
|
|
|
|
// Autocomplete pour les types de panneaux
|
|
otherSigntype.addEventListener('input', function() {
|
|
clearTimeout(signTypeTimeout);
|
|
const q = this.value.trim();
|
|
otherSigntypeId.value = '';
|
|
selectedSignType = null;
|
|
if (q.length < 1) { otherSigntypeRes.style.display = 'none'; return; }
|
|
signTypeTimeout = setTimeout(() => {
|
|
fetch(`${signPanelTypesUrl}?q=${encodeURIComponent(q)}`)
|
|
.then(r => r.json())
|
|
.then(data => showSignTypeResults(data.results))
|
|
.catch(() => {});
|
|
}, 250);
|
|
});
|
|
|
|
function showSignTypeResults(results) {
|
|
otherSigntypeRes.innerHTML = '';
|
|
if (!results || results.length === 0) {
|
|
otherSigntypeRes.innerHTML = '<div class="list-group-item text-muted small">{% translate "Aucun type trouvé" %}</div>';
|
|
} else {
|
|
results.forEach(function(pt) {
|
|
const item = document.createElement('div');
|
|
item.className = 'list-group-item list-group-item-action';
|
|
item.innerHTML = `<strong>${pt.code}</strong> — <span class="text-muted">${pt.name}</span>`;
|
|
item.addEventListener('click', () => selectSignType(pt));
|
|
otherSigntypeRes.appendChild(item);
|
|
});
|
|
}
|
|
otherSigntypeRes.style.display = 'block';
|
|
}
|
|
|
|
function selectSignType(pt) {
|
|
selectedSignType = pt;
|
|
otherSigntype.value = `${pt.code} — ${pt.name}`;
|
|
otherSigntypeId.value = pt.id;
|
|
otherSigntypeRes.style.display = 'none';
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!otherSigntype.contains(e.target) && !otherSigntypeRes.contains(e.target)) {
|
|
otherSigntypeRes.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Ajouter une ligne "autre produit"
|
|
addOtherBtn.addEventListener('click', function() {
|
|
const desc = otherDescription.value.trim();
|
|
if (!desc) {
|
|
alert('{% translate "Veuillez saisir une description" %}');
|
|
otherDescription.focus();
|
|
return;
|
|
}
|
|
const qty = parseInt(otherQuantity.value) || 1;
|
|
const catId = parseInt(otherCategory.value) || null;
|
|
const catText = catId ? otherCategory.options[otherCategory.selectedIndex].text : '';
|
|
const isSignalisation = catId && signalisationIds.has(catId);
|
|
let signTypeId = null;
|
|
let signTypeText = '';
|
|
if (isSignalisation && otherSigntypeId.value) {
|
|
signTypeId = parseInt(otherSigntypeId.value);
|
|
signTypeText = otherSigntype.value;
|
|
}
|
|
otherItemsAdded.push({ desc, qty, catId, catText, signTypeId, signTypeText });
|
|
// Réinitialiser
|
|
otherDescription.value = '';
|
|
otherQuantity.value = '1';
|
|
otherCategory.value = '';
|
|
otherSigntypeWrap.style.display = 'none';
|
|
otherSigntype.value = '';
|
|
otherSigntypeId.value = '';
|
|
selectedSignType = null;
|
|
renderOtherItems();
|
|
});
|
|
|
|
otherDescription.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') { e.preventDefault(); addOtherBtn.click(); }
|
|
});
|
|
|
|
function renderOtherItems() {
|
|
otherItemsList.querySelectorAll('.other-item-card').forEach(el => el.remove());
|
|
otherEmptyMsg.style.display = otherItemsAdded.length === 0 ? 'block' : 'none';
|
|
otherItemsAdded.forEach(function(item, index) {
|
|
const card = document.createElement('div');
|
|
card.className = 'other-item-card product-card d-flex justify-content-between align-items-start';
|
|
card.innerHTML = `
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-1">${item.desc}</h6>
|
|
<small class="text-muted">
|
|
${item.catText ? `<i class="bi bi-tag"></i> ${item.catText}` : ''}
|
|
${item.signTypeText ? ` • <i class="bi bi-sign-stop"></i> ${item.signTypeText}` : ''}
|
|
</small>
|
|
</div>
|
|
<div class="text-end ms-3">
|
|
<div class="mb-2"><strong class="fs-5">${item.qty}</strong> <small class="text-muted">{% translate "unités" %}</small></div>
|
|
<button type="button" class="btn btn-danger btn-sm" onclick="removeOtherItem(${index})">
|
|
<i class="bi bi-trash"></i> {% translate "Retirer" %}
|
|
</button>
|
|
</div>`;
|
|
otherItemsList.appendChild(card);
|
|
});
|
|
updateOtherFormset();
|
|
}
|
|
|
|
window.removeOtherItem = function(index) {
|
|
otherItemsAdded.splice(index, 1);
|
|
renderOtherItems();
|
|
};
|
|
|
|
function updateOtherFormset() {
|
|
otherFormset.innerHTML = '';
|
|
otherTotalInput.value = otherItemsAdded.length;
|
|
otherItemsAdded.forEach(function(item, i) {
|
|
const f = (name, val) => {
|
|
const inp = document.createElement('input');
|
|
inp.type = 'hidden';
|
|
inp.name = `other-${i}-${name}`;
|
|
inp.value = val !== null && val !== undefined ? val : '';
|
|
otherFormset.appendChild(inp);
|
|
};
|
|
f('description', item.desc);
|
|
f('quantity', item.qty);
|
|
f('category_id', item.catId || '');
|
|
f('sign_panel_type_id', item.signTypeId || '');
|
|
});
|
|
}
|
|
|
|
// ================================
|
|
// Zone de dépôt documents
|
|
// ================================
|
|
(function() {
|
|
const dropZone = document.getElementById('prep-drop-zone');
|
|
const fileInput = document.getElementById('prep_documents_input');
|
|
const fileList = document.getElementById('prep-file-list');
|
|
if (!dropZone || !fileInput) return;
|
|
|
|
let selectedFiles = new DataTransfer();
|
|
|
|
function renderFileList() {
|
|
fileList.innerHTML = '';
|
|
Array.from(selectedFiles.files).forEach(function(file, idx) {
|
|
const item = document.createElement('div');
|
|
item.className = 'd-flex align-items-center gap-2 bg-white border rounded p-2 mt-1';
|
|
item.innerHTML = `
|
|
<i class="bi bi-file-earmark text-secondary"></i>
|
|
<span class="flex-grow-1 text-truncate small">${file.name}</span>
|
|
<small class="text-muted">${(file.size/1024).toFixed(0)} Ko</small>
|
|
<button type="button" class="btn btn-sm btn-outline-danger py-0 px-1 remove-file-btn" data-idx="${idx}">
|
|
<i class="bi bi-x"></i>
|
|
</button>`;
|
|
fileList.appendChild(item);
|
|
});
|
|
fileInput.files = selectedFiles.files;
|
|
}
|
|
|
|
function addFiles(files) {
|
|
Array.from(files).forEach(function(f) { selectedFiles.items.add(f); });
|
|
renderFileList();
|
|
}
|
|
|
|
dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.classList.add('dragover'); });
|
|
dropZone.addEventListener('dragleave', function() { dropZone.classList.remove('dragover'); });
|
|
dropZone.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('dragover');
|
|
addFiles(e.dataTransfer.files);
|
|
});
|
|
|
|
fileInput.addEventListener('change', function() {
|
|
addFiles(this.files);
|
|
this.value = '';
|
|
});
|
|
|
|
fileList.addEventListener('click', function(e) {
|
|
const btn = e.target.closest('.remove-file-btn');
|
|
if (!btn) return;
|
|
const idx = parseInt(btn.dataset.idx);
|
|
const dt = new DataTransfer();
|
|
Array.from(selectedFiles.files).forEach(function(f, i) { if (i !== idx) dt.items.add(f); });
|
|
selectedFiles = dt;
|
|
renderFileList();
|
|
});
|
|
})();
|
|
});
|
|
</script>
|
|
<style>
|
|
.drop-zone { border: 2px dashed #6c757d; border-radius: 0.5rem; padding: 2rem; text-align: center; cursor: pointer; transition: background .2s, border-color .2s; }
|
|
.drop-zone.dragover, .drop-zone:hover { border-color: #0d6efd; background: #e8f0fe; }
|
|
</style>
|
|
{% endblock script %}
|