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

282 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% load i18n %}
{% load stock_custom_filters %}
{% block content %}
<div class="container mt-5">
<h1 class="h3 mb-3">{% if edit_mode %}{% translate "Modifier l'inventaire" %} #{{ inventory.id }}{% else %}{% translate "Nouvel inventaire" %}{% endif %}</h1>
<form method="post" id="inventory-form">
{% csrf_token %}
<div class="card mb-3">
<div class="card-header">{% translate "Informations" %}</div>
<div class="card-body">
{{ form.non_field_errors }}
<div class="row g-3">
{% for field in form %}
<div class="col-md-4">
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
{% if field.field.widget.input_type == 'select' %}
{{ field|add_class:"form-select" }}
{% else %}
{{ field|add_class:"form-control" }}
{% endif %}
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
{% for error in field.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
{% endfor %}
</div>
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
{% translate "Articles" %}
<span id="items-count-badge" class="badge bg-secondary ms-1 d-none"></span>
</span>
<div class="d-flex gap-2 align-items-center">
<span id="prefill-status" class="text-muted small fst-italic"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="add-item">
<i class="bi bi-plus-circle"></i> {% translate "Ajouter un article" %}
</button>
</div>
</div>
<div class="card-body p-0">
{{ formset.management_form }}
<table class="table table-sm mb-0 align-middle" id="inventory-items-table">
<thead class="table-light">
<tr>
<th>{% translate "Produit" %}</th>
<th style="width:140px">{% translate "Quantité attendue" %}</th>
<th style="width:140px">{% translate "Quantité comptée" %}</th>
<th style="min-width:180px">{% translate "Commentaire" %}</th>
{% if can_validate %}
<th style="width:48px"></th>
{% endif %}
</tr>
</thead>
<tbody id="inventory-items-container">
{% for item_form in formset %}
<tr class="inventory-item">
{% for hidden in item_form.hidden_fields %}{{ hidden }}{% endfor %}
<td>
{% if edit_mode %}
{% if item_form.instance.pk %}
{# Produit fixé, déjà rendu en champ caché via hidden_fields #}
<span class="fw-semibold">{{ item_form.instance.product.name }}</span>
{% endif %}
{% else %}
{% with field=item_form.product %}
{{ field|add_class:"form-select form-select-sm" }}
{% for error in field.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
{% endwith %}
{% endif %}
</td>
<td>
{% with field=item_form.expected_quantity %}
{{ field|add_class:"form-control form-control-sm" }}
{% endwith %}
</td>
<td>
{% with field=item_form.counted_quantity %}
{{ field|add_class:"form-control form-control-sm" }}
{% for error in field.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
{% endwith %}
</td>
<td>
{% with field=item_form.comment %}
{{ field|add_class:"form-control form-control-sm" }}
{% endwith %}
</td>
{% if can_validate %}
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-danger remove-item" title="{% translate 'Supprimer' %}">
<i class="bi bi-trash"></i>
</button>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<div id="empty-state" class="text-center text-muted py-4 {% if formset.forms %}d-none{% endif %}">
<i class="bi bi-box-seam fs-3 d-block mb-2 opacity-50"></i>
{% translate "Sélectionnez un emplacement pour charger automatiquement les produits en stock." %}
</div>
</div>
</div>
<div class="mt-3 text-end">
<a href="{% if edit_mode %}{% url 'stock:inventory_detail' inventory.pk %}{% else %}{% url 'stock:stock_movements' %}{% endif %}" class="btn btn-secondary me-2">{% translate "Annuler" %}</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-floppy"></i> {% translate "Enregistrer" %}
</button>
</div>
</form>
</div>
{% endblock content %}
{% block script %}
<script>
(function () {
const PREFILL_URL = "{% url 'stock:inventory_prefill_api' %}";
const FORMSET_PREFIX = '{{ formset.prefix }}';
const PRODUCTS = {{ products_json|safe }};
const canValidate = {{ can_validate|yesno:"true,false" }};
const container = document.getElementById('inventory-items-container');
const emptyState = document.getElementById('empty-state');
const countBadge = document.getElementById('items-count-badge');
const prefillStatus = document.getElementById('prefill-status');
// ── Gestion TOTAL_FORMS ────────────────────────────────────────────────
function getTotalForms() {
return parseInt(document.querySelector(`[name="${FORMSET_PREFIX}-TOTAL_FORMS"]`).value, 10);
}
function setTotalForms(n) {
document.querySelector(`[name="${FORMSET_PREFIX}-TOTAL_FORMS"]`).value = n;
updateCountBadge();
}
function updateCountBadge() {
const n = container.querySelectorAll('tr.inventory-item').length;
if (n > 0) {
countBadge.textContent = n;
countBadge.classList.remove('d-none');
} else {
countBadge.classList.add('d-none');
}
emptyState.classList.toggle('d-none', n > 0);
}
// ── Construction d'une ligne de formset (auto-remplie) ────────────────
function buildAutoRow(index, productId, productName, productCode, expectedQty) {
const tr = document.createElement('tr');
tr.className = 'inventory-item';
tr.innerHTML = `
<td>
<input type="hidden" name="${FORMSET_PREFIX}-${index}-id" value="">
<input type="hidden" name="${FORMSET_PREFIX}-${index}-product" value="${productId}">
<span class="fw-semibold">${productCode} – ${productName}</span>
</td>
<td>
<input type="number" name="${FORMSET_PREFIX}-${index}-expected_quantity"
value="${expectedQty}" class="form-control form-control-sm" readonly>
</td>
<td>
<input type="number" name="${FORMSET_PREFIX}-${index}-counted_quantity"
value="${expectedQty}" class="form-control form-control-sm" min="0" required>
</td>
<td>
<textarea name="${FORMSET_PREFIX}-${index}-comment"
class="form-control form-control-sm" rows="1"
placeholder="Commentaire optionnel…"></textarea>
</td>
${canValidate ? `
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-danger remove-item" title="Supprimer">
<i class="bi bi-trash"></i>
</button>
</td>` : ''}`;
return tr;
}
// ── Construction d'une ligne manuelle (avec select produit) ───────────
function buildManualRow(index) {
const tr = document.createElement('tr');
tr.className = 'inventory-item';
let options = '<option value="">-- Sélectionner --</option>';
PRODUCTS.forEach(p => {
options += `<option value="${p.id}">${p.code} – ${p.name}</option>`;
});
tr.innerHTML = `
<td>
<input type="hidden" name="${FORMSET_PREFIX}-${index}-id" value="">
<select name="${FORMSET_PREFIX}-${index}-product" class="form-select form-select-sm" required>
${options}
</select>
</td>
<td>
<input type="number" name="${FORMSET_PREFIX}-${index}-expected_quantity"
value="" class="form-control form-control-sm" min="0">
</td>
<td>
<input type="number" name="${FORMSET_PREFIX}-${index}-counted_quantity"
value="" class="form-control form-control-sm" min="0" required>
</td>
<td>
<textarea name="${FORMSET_PREFIX}-${index}-comment"
class="form-control form-control-sm" rows="1"
placeholder="Commentaire optionnel…"></textarea>
</td>
${canValidate ? `
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-danger remove-item" title="Supprimer">
<i class="bi bi-trash"></i>
</button>
</td>` : ''}`;
return tr;
}
// ── Chargement automatique via API ─────────────────────────────────────
function prefillFromLocation(locationId) {
if (!locationId) return;
prefillStatus.textContent = '{% translate "Chargement…" %}';
fetch(`${PREFILL_URL}?location_id=${locationId}`)
.then(r => r.json())
.then(data => {
container.innerHTML = '';
if (!data.stocks || data.stocks.length === 0) {
setTotalForms(0);
prefillStatus.textContent = '{% translate "Aucun produit en stock à cet emplacement." %}';
return;
}
data.stocks.forEach((s, i) => {
container.appendChild(buildAutoRow(i, s.product_id, s.product_name, s.product_code, s.quantity));
});
setTotalForms(data.stocks.length);
prefillStatus.textContent = `{% translate "Chargement automatique :" %} ${data.stocks.length} {% translate "produit(s)." %}`;
})
.catch(() => {
prefillStatus.textContent = '{% translate "Erreur lors du chargement." %}';
});
}
// ── Écoute le changement de warehouse_location ─────────────────────────
const locationSelect = document.getElementById('id_warehouse_location');
if (locationSelect) {
locationSelect.addEventListener('change', function () {
if (this.value) {
prefillFromLocation(this.value);
} else {
container.innerHTML = '';
setTotalForms(0);
prefillStatus.textContent = '';
}
});
}
// ── Bouton "Ajouter un article" (ligne manuelle) ───────────────────────
document.getElementById('add-item').addEventListener('click', function () {
const index = getTotalForms();
container.appendChild(buildManualRow(index));
setTotalForms(index + 1);
});
// ── Suppression d'une ligne ────────────────────────────────────────────
container.addEventListener('click', function (e) {
const btn = e.target.closest('.remove-item');
if (!btn) return;
btn.closest('tr').remove();
// Renuméroter les indices
container.querySelectorAll('tr.inventory-item').forEach((row, i) => {
row.querySelectorAll('[name]').forEach(el => {
el.name = el.name.replace(new RegExp(`${FORMSET_PREFIX}-\\d+-`), `${FORMSET_PREFIX}-${i}-`);
});
});
setTotalForms(container.querySelectorAll('tr.inventory-item').length);
});
// Init badge au chargement
updateCountBadge();
})();
</script>
{% endblock script %}