loko/streetup/stock/templates/stock/preparation_order_create_purchase.html
2026-07-22 14:48:40 +02:00

531 lines
23 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="container-fluid mt-3">
<div class="row">
<div class="col-12">
<!-- En-tête -->
<div class="mb-4">
<h3>
<i class="bi bi-cart-plus"></i>
{% translate "Créer un bon de commande" %}
</h3>
<p class="text-muted">
{% translate "Depuis le bon de préparation" %} #{{ preparation_order.order_number }}
</p>
</div>
<!-- Produits manquants -->
{% if missing_products %}
<div class="card mb-3 shadow-sm">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">
<i class="bi bi-exclamation-triangle"></i>
{% translate "Produits manquants ou en stock insuffisant" %}
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>{% translate "Produit" %}</th>
<th>{% translate "Code" %}</th>
<th class="text-center">{% translate "Quantité nécessaire" %}</th>
<th class="text-center">{% translate "Stock disponible" %}</th>
<th class="text-center">{% translate "À commander" %}</th>
</tr>
</thead>
<tbody>
{% for product_data in missing_products %}
<tr>
<td><strong>{{ product_data.product.name }}</strong></td>
<td><code>{{ product_data.product.code }}</code></td>
<td class="text-center">
<span class="badge bg-primary">{{ product_data.needed }} {{ product_data.product.unit }}</span>
</td>
<td class="text-center">
<span class="badge bg-danger">{{ product_data.available }} {{ product_data.product.unit }}</span>
</td>
<td class="text-center">
<span class="badge bg-warning text-dark">{{ product_data.to_order }} {{ product_data.product.unit }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="alert alert-success">
<i class="bi bi-check-circle"></i>
{% translate "Tous les produits sont en stock suffisant. Vous pouvez créer un bon de commande pour réapprovisionner." %}
</div>
{% endif %}
<!-- Formulaire de création de bon de commande -->
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-file-earmark-text"></i>
{% translate "Informations du bon de commande" %}
</h5>
</div>
<div class="card-body">
<form method="post" id="purchase-form">
{% csrf_token %}
<div class="row mb-3">
<div class="col-md-4">
<label for="{{ form.supplier.id_for_label }}" class="form-label">
{{ form.supplier.label }}
</label>
{{ form.supplier }}
{{ form.supplier.errors }}
</div>
<div class="col-md-4">
<label for="{{ form.warehouse.id_for_label }}" class="form-label">
{{ form.warehouse.label }}
</label>
{{ form.warehouse }}
{{ form.warehouse.errors }}
</div>
<div class="col-md-4">
<label for="{{ form.status.id_for_label }}" class="form-label">
{{ form.status.label }}
</label>
{{ form.status }}
{{ form.status.errors }}
</div>
</div>
<!-- Section produits -->
<div class="card mb-3 border">
<div class="card-header bg-light">
<h6 class="mb-0">
<i class="bi bi-box-seam"></i> {% translate "Produits à commander" %}
</h6>
</div>
<div class="card-body">
<div id="products-container">
<p class="text-muted text-center" id="empty-message">
<i class="bi bi-inbox"></i> {% translate "Cliquez sur 'Ajouter produit manquant' ou ajoutez des produits manuellement" %}
</p>
</div>
<div class="mt-3">
{% if missing_products %}
<button type="button" class="btn btn-warning" id="add-missing-btn">
<i class="bi bi-plus-circle"></i> {% translate "Ajouter tous les produits manquants" %}
</button>
{% endif %}
<button type="button" class="btn btn-primary" id="add-product-btn" data-bs-toggle="modal" data-bs-target="#addProductModal">
<i class="bi bi-plus-circle"></i> {% translate "Ajouter un produit" %}
</button>
</div>
<!-- Formset caché -->
{{ formset.management_form }}
<div id="formset-container"></div>
</div>
</div>
<!-- Actions -->
<div class="d-flex justify-content-between">
<a href="{% url 'stock:preparation_order_detail' preparation_order.pk %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> {% translate "Annuler" %}
</a>
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle"></i> {% translate "Créer le bon de commande" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Modal pour ajouter un produit -->
<div class="modal fade" id="addProductModal" tabindex="-1" aria-labelledby="addProductModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addProductModalLabel">
<i class="bi bi-plus-circle"></i> {% translate "Ajouter un produit" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="product-search" class="form-label">
<i class="bi bi-search"></i> {% translate "Rechercher un produit" %}
</label>
<input type="text"
id="product-search"
class="form-control"
placeholder="{% translate "Tapez pour rechercher..." %}"
autocomplete="off">
<div id="search-results" class="list-group mt-2" style="display: none; max-height: 300px; overflow-y: auto;"></div>
</div>
<div class="mb-3" id="selected-product-info" style="display: none;">
<label class="form-label">{% translate "Produit sélectionné" %}</label>
<div class="card">
<div class="card-body">
<h6 id="selected-product-name"></h6>
<p class="mb-0"><small class="text-muted">Code: <span id="selected-product-code"></span></small></p>
<p class="mb-0"><small>Stock disponible: <span id="selected-product-stock" class="badge bg-info"></span></small></p>
</div>
</div>
</div>
<div class="mb-3">
<label for="product-quantity" class="form-label">{% translate "Quantité" %}</label>
<input type="number" id="product-quantity" class="form-control" value="1" min="1">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% translate "Annuler" %}
</button>
<button type="button" class="btn btn-primary" id="confirm-add-product" disabled>
<i class="bi bi-check-circle"></i> {% translate "Ajouter" %}
</button>
</div>
</div>
</div>
</div>
<script>
const productsApiUrl = "{% url 'stock:products_autocomplete_api' %}";
let selectedProduct = null;
document.addEventListener('DOMContentLoaded', function() {
const productsContainer = document.getElementById('products-container');
const emptyMessage = document.getElementById('empty-message');
const formsetContainer = document.getElementById('formset-container');
const managementForm = document.querySelector('[name="{{ formset.management_form.prefix }}-TOTAL_FORMS"]').closest('div');
const formPrefix = '{{ formset.prefix }}';
const warehouseSelect = document.querySelector('[name="warehouse"]');
let productsAdded = [];
let warehouseLocations = [];
const missingProducts = [
{% for product_data in missing_products %}
{
id: {{ product_data.product.id }},
name: "{{ product_data.product.name|escapejs }}",
code: "{{ product_data.product.code|escapejs }}",
to_order: {{ product_data.to_order }}
}{% if not forloop.last %},{% endif %}
{% endfor %}
];
// ============ Autocomplete pour la recherche de produits ============
const productSearch = document.getElementById('product-search');
const searchResults = document.getElementById('search-results');
const confirmAddBtn = document.getElementById('confirm-add-product');
const selectedProductInfo = document.getElementById('selected-product-info');
const productQuantityInput = document.getElementById('product-quantity');
let searchTimeout;
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 => {
searchResults.innerHTML = '';
if (data.results && data.results.length > 0) {
data.results.forEach(product => {
const item = document.createElement('a');
item.href = '#';
item.className = 'list-group-item list-group-item-action';
item.innerHTML = `
<div class="d-flex justify-content-between">
<div>
<strong>${product.text}</strong><br>
<small class="text-muted">Code: ${product.code}</small>
</div>
<div class="text-end">
<span class="badge ${product.total_stock > 0 ? 'bg-success' : 'bg-danger'}">
Stock: ${product.total_stock}
</span>
</div>
</div>
`;
item.addEventListener('click', (e) => {
e.preventDefault();
selectProduct(product);
});
searchResults.appendChild(item);
});
searchResults.style.display = 'block';
} else {
searchResults.innerHTML = '<div class="list-group-item text-muted">{% translate "Aucun produit trouvé" %}</div>';
searchResults.style.display = 'block';
}
})
.catch(error => {
console.error('Erreur lors de la recherche:', error);
});
}, 300);
});
function selectProduct(product) {
selectedProduct = product;
productSearch.value = '';
searchResults.style.display = 'none';
document.getElementById('selected-product-name').textContent = product.text;
document.getElementById('selected-product-code').textContent = product.code;
document.getElementById('selected-product-stock').textContent = product.total_stock;
selectedProductInfo.style.display = 'block';
confirmAddBtn.disabled = false;
productQuantityInput.focus();
}
// Réinitialiser le modal quand il est fermé
const addProductModal = document.getElementById('addProductModal');
addProductModal.addEventListener('hidden.bs.modal', function() {
productSearch.value = '';
searchResults.style.display = 'none';
selectedProductInfo.style.display = 'none';
confirmAddBtn.disabled = true;
selectedProduct = null;
productQuantityInput.value = 1;
});
// Confirmer l'ajout du produit
confirmAddBtn.addEventListener('click', function() {
if (!selectedProduct) return;
const quantity = parseInt(productQuantityInput.value) || 1;
// Vérifier si le produit est déjà dans la liste
const existingIndex = productsAdded.findIndex(p => p.id === selectedProduct.id);
if (existingIndex !== -1) {
// Augmenter la quantité
productsAdded[existingIndex].quantity += quantity;
} else {
// Ajouter le nouveau produit
productsAdded.push({
id: selectedProduct.id,
name: selectedProduct.text,
code: selectedProduct.code,
quantity: quantity,
location: ''
});
}
renderProductsList();
// Fermer le modal
const modal = bootstrap.Modal.getInstance(addProductModal);
modal.hide();
});
// Fermer l'autocomplete si clic ailleurs
document.addEventListener('click', function(e) {
if (!productSearch.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
// Charger les emplacements lorsque l'entrepôt change
function loadWarehouseLocations(warehouseId) {
if (!warehouseId) {
warehouseLocations = [];
updateAllLocationSelects();
return;
}
fetch(`/stock/warehouses/${warehouseId}/locations/api/`)
.then(response => response.json())
.then(data => {
warehouseLocations = data;
updateAllLocationSelects();
})
.catch(error => {
console.error('Erreur lors du chargement des emplacements:', error);
warehouseLocations = [];
updateAllLocationSelects();
});
}
// Mettre à jour tous les selects d'emplacement
function updateAllLocationSelects() {
const defaultLocation = warehouseLocations.find(l => l.is_default);
const locationSelects = document.querySelectorAll('.location-select');
locationSelects.forEach(select => {
const currentValue = select.value;
select.innerHTML = '<option value="">{% translate "Sélectionner" %}</option>';
warehouseLocations.forEach(location => {
const option = document.createElement('option');
option.value = location.id;
option.textContent = location.name;
if (location.id.toString() === currentValue) {
option.selected = true;
}
select.appendChild(option);
});
// Pre-select the default location if no selection is already set
if (!currentValue && defaultLocation) {
select.value = defaultLocation.id;
}
});
}
// Écouter les changements d'entrepôt
if (warehouseSelect) {
warehouseSelect.addEventListener('change', function() {
loadWarehouseLocations(this.value);
});
// Charger les emplacements de l'entrepôt initial si défini
if (warehouseSelect.value) {
loadWarehouseLocations(warehouseSelect.value);
}
}
// Ajouter tous les produits manquants
document.getElementById('add-missing-btn')?.addEventListener('click', function() {
missingProducts.forEach(product => {
const existing = productsAdded.find(p => p.id === product.id);
if (!existing) {
productsAdded.push({
id: product.id,
name: product.name,
code: product.code,
quantity: product.to_order,
location: ''
});
}
});
renderProductsList();
});
// Afficher la liste des produits
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 = 'card mb-2';
let locationOptions = '<option value="">{% translate "Sélectionner" %}</option>';
const defaultLoc = warehouseLocations.find(l => l.is_default);
const effectiveLocation = product.location || (defaultLoc ? defaultLoc.id.toString() : '');
warehouseLocations.forEach(location => {
const selected = location.id.toString() === effectiveLocation ? 'selected' : '';
locationOptions += `<option value="${location.id}" ${selected}>${location.name}</option>`;
});
card.innerHTML = `
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-5">
<strong>${product.name}</strong><br>
<small class="text-muted">Code: ${product.code}</small>
</div>
<div class="col-md-3">
<label class="form-label small">{% translate "Quantité" %}</label>
<input type="number" class="form-control" value="${product.quantity}"
min="1" onchange="updateQuantity(${index}, this.value)">
</div>
<div class="col-md-2">
<label class="form-label small">{% translate "Emplacement" %}</label>
<select class="form-select location-select" onchange="updateLocation(${index}, this.value)">
${locationOptions}
</select>
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-danger btn-sm" onclick="removeProduct(${index})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`;
productsContainer.appendChild(card);
});
updateFormset();
}
// Mettre à jour le formset
function updateFormset() {
formsetContainer.innerHTML = '';
const totalFormsInput = managementForm.querySelector('[id$="-TOTAL_FORMS"]');
totalFormsInput.value = productsAdded.length;
productsAdded.forEach((product, index) => {
const fields = ['product', 'quantity', 'location', 'id', 'DELETE'];
fields.forEach(field => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = `${formPrefix}-${index}-${field}`;
if (field === 'product') input.value = product.id;
else if (field === 'quantity') input.value = product.quantity;
else if (field === 'location') input.value = product.location || '';
else input.value = '';
formsetContainer.appendChild(input);
});
});
}
window.updateQuantity = function(index, value) {
productsAdded[index].quantity = parseInt(value);
updateFormset();
};
window.updateLocation = function(index, value) {
productsAdded[index].location = value;
updateFormset();
};
window.removeProduct = function(index) {
productsAdded.splice(index, 1);
renderProductsList();
};
// Validation du formulaire
document.getElementById('purchase-form').addEventListener('submit', function(e) {
if (productsAdded.length === 0) {
e.preventDefault();
alert('{% translate "Veuillez ajouter au moins un produit" %}');
return false;
}
});
});
</script>
<style>
.form-select, .form-control {
font-size: 0.9rem;
}
</style>
{% endblock content %}