341 lines
17 KiB
HTML
341 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block content %}
|
|
<div class="container mt-3">
|
|
<div class="row">
|
|
<div class="col-lg-8 mx-auto">
|
|
|
|
<!-- En-tête -->
|
|
<div class="mb-4">
|
|
<h3>
|
|
<i class="bi bi-box-seam"></i>
|
|
{% if action == 'create' %}
|
|
{% translate "Nouveau produit" %}
|
|
{% else %}
|
|
{% translate "Modifier le produit" %} — {{ product.name }}
|
|
{% endif %}
|
|
</h3>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item">
|
|
<a href="{% url 'stock:warehouses' %}?tab=products">{% translate "Produits" %}</a>
|
|
</li>
|
|
{% if action == 'edit' %}
|
|
<li class="breadcrumb-item">
|
|
<a href="{% url 'stock:product_detail' product.pk %}">{{ product.name }}</a>
|
|
</li>
|
|
{% endif %}
|
|
<li class="breadcrumb-item active">
|
|
{% if action == 'create' %}{% translate "Nouveau" %}{% else %}{% translate "Modifier" %}{% endif %}
|
|
</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Formulaire -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<form method="post">
|
|
{% csrf_token %}
|
|
|
|
{% if form.non_field_errors %}
|
|
<div class="alert alert-danger">
|
|
{{ form.non_field_errors }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Identification -->
|
|
<h6 class="text-muted text-uppercase fw-semibold mb-3 border-bottom pb-1">
|
|
{% translate "Identification" %}
|
|
</h6>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label for="{{ form.code.id_for_label }}" class="form-label">
|
|
{{ form.code.label }} <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.code }}
|
|
{% if form.code.errors %}
|
|
<div class="text-danger small mt-1">{{ form.code.errors }}</div>
|
|
{% endif %}
|
|
<div class="form-text">
|
|
{% if action == 'create' %}
|
|
<i class="bi bi-lock"></i> {% translate "Généré automatiquement" %}
|
|
{% else %}
|
|
{% translate "Code unique du produit" %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4 mb-3">
|
|
<label for="{{ form.sku.id_for_label }}" class="form-label">
|
|
{{ form.sku.label }} <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.sku }}
|
|
{% if form.sku.errors %}
|
|
<div class="text-danger small mt-1">{{ form.sku.errors }}</div>
|
|
{% endif %}
|
|
<div class="form-text">
|
|
{% if action == 'create' %}
|
|
<i class="bi bi-lock"></i> {% translate "Généré automatiquement" %}
|
|
{% else %}
|
|
{% translate "Référence fournisseur / interne" %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4 mb-3">
|
|
<label for="{{ form.category.id_for_label }}" class="form-label">
|
|
{{ form.category.label }}
|
|
</label>
|
|
{{ form.category }}
|
|
{% if form.category.errors %}
|
|
<div class="text-danger small mt-1">{{ form.category.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="{{ form.name.id_for_label }}" class="form-label">
|
|
{{ form.name.label }} <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.name }}
|
|
{% if form.name.errors %}
|
|
<div class="text-danger small mt-1">{{ form.name.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
|
{{ form.description.label }}
|
|
</label>
|
|
{{ form.description }}
|
|
{% if form.description.errors %}
|
|
<div class="text-danger small mt-1">{{ form.description.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Traçabilité -->
|
|
<h6 class="text-muted text-uppercase fw-semibold mb-3 border-bottom pb-1 mt-4">
|
|
{% translate "Traçabilité" %}
|
|
</h6>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
{{ form.is_serialized }}
|
|
<label class="form-check-label" for="{{ form.is_serialized.id_for_label }}">
|
|
{{ form.is_serialized.label }}
|
|
</label>
|
|
</div>
|
|
{% if form.is_serialized.help_text %}
|
|
<div class="form-text">{{ form.is_serialized.help_text }}</div>
|
|
{% endif %}
|
|
{% if form.is_serialized.errors %}
|
|
<div class="text-danger small mt-1">{{ form.is_serialized.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Unité, prix & seuil -->
|
|
<h6 class="text-muted text-uppercase fw-semibold mb-3 border-bottom pb-1 mt-4">
|
|
{% translate "Stock & tarification" %}
|
|
</h6>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label for="{{ form.unit.id_for_label }}" class="form-label">
|
|
{{ form.unit.label }} <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.unit }}
|
|
{% if form.unit.errors %}
|
|
<div class="text-danger small mt-1">{{ form.unit.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-md-4 mb-3">
|
|
<label for="{{ form.price.id_for_label }}" class="form-label">
|
|
{{ form.price.label }}
|
|
</label>
|
|
<div class="input-group">
|
|
{{ form.price }}
|
|
<span class="input-group-text">€</span>
|
|
</div>
|
|
{% if form.price.errors %}
|
|
<div class="text-danger small mt-1">{{ form.price.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-md-4 mb-3">
|
|
<label for="{{ form.threshold.id_for_label }}" class="form-label">
|
|
{{ form.threshold.label }}
|
|
<span class="text-danger">*</span>
|
|
<i class="bi bi-info-circle text-muted"
|
|
data-bs-toggle="tooltip"
|
|
title="{% translate 'Quantité minimale déclenchant une alerte de stock' %}"></i>
|
|
</label>
|
|
{{ form.threshold }}
|
|
{% if form.threshold.errors %}
|
|
<div class="text-danger small mt-1">{{ form.threshold.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Emplacements autorisés -->
|
|
<h6 class="text-muted text-uppercase fw-semibold mb-3 border-bottom pb-1 mt-4">
|
|
{% translate "Emplacements autorisés" %}
|
|
</h6>
|
|
|
|
<div class="mb-3">
|
|
<label for="{{ form.authorized_locations.id_for_label }}" class="form-label">
|
|
{{ form.authorized_locations.label }}
|
|
</label>
|
|
{{ form.authorized_locations }}
|
|
{% if form.authorized_locations.help_text %}
|
|
<div class="form-text">{{ form.authorized_locations.help_text }}</div>
|
|
{% endif %}
|
|
{% if form.authorized_locations.errors %}
|
|
<div class="text-danger small mt-1">{{ form.authorized_locations.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if action == 'create' %}
|
|
<div id="initial-quantities-section" class="mt-4 p-3 bg-light rounded border border-light-subtle" style="display: none;">
|
|
<h6 class="text-secondary fw-semibold mb-3">
|
|
<i class="bi bi-plus-circle me-1"></i>{% translate "Quantités initiales pour les emplacements sélectionnés" %}
|
|
</h6>
|
|
<div id="initial-quantities-container"></div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Boutons -->
|
|
<div class="d-flex justify-content-between mt-4">
|
|
<a href="{% if action == 'edit' %}{% url 'stock:product_detail' product.pk %}{% else %}{% url 'stock:warehouses' %}?tab=products{% endif %}"
|
|
class="btn btn-light">
|
|
<i class="bi bi-arrow-left"></i> {% translate "Annuler" %}
|
|
</a>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-check-lg"></i>
|
|
{% if action == 'create' %}
|
|
{% translate "Créer le produit" %}
|
|
{% else %}
|
|
{% translate "Enregistrer les modifications" %}
|
|
{% endif %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zone de danger (suppression) -->
|
|
{% if action == 'edit' %}
|
|
<div class="card border-danger mt-4">
|
|
<div class="card-header bg-danger text-white">
|
|
<i class="bi bi-exclamation-triangle"></i> {% translate "Désactivation" %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% if product.is_active %}
|
|
<p class="text-muted mb-3">
|
|
{% translate "Rendre ce produit inactif empêchera son utilisation pour les futurs mouvements de stock ou bons de commande, mais préservera tout son historique." %}
|
|
</p>
|
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteProductModal">
|
|
<i class="bi bi-trash"></i> {% translate "Désactiver ce produit" %}
|
|
</button>
|
|
{% else %}
|
|
<span class="badge bg-danger p-2"><i class="bi bi-x-circle me-1"></i>{% translate "Ce produit est déjà inactif" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal suppression -->
|
|
<div class="modal fade" id="deleteProductModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title text-danger">
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
{% translate "Confirmer la désactivation" %}
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
{% blocktranslate with name=product.name %}
|
|
Êtes-vous sûr de vouloir rendre le produit <strong>{{ name }}</strong> inactif ?
|
|
Il ne sera plus visible pour les nouvelles opérations de stock ou d'intervention, mais restera accessible dans l'historique.
|
|
{% endblocktranslate %}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
{% translate "Annuler" %}
|
|
</button>
|
|
<form method="post" action="{% url 'stock:product_delete' product.pk %}">
|
|
{% csrf_token %}
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="bi bi-trash"></i> {% translate "Désactiver" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Activer tous les tooltips Bootstrap
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
tooltipTriggerList.forEach(function (el) {
|
|
new bootstrap.Tooltip(el);
|
|
});
|
|
});
|
|
|
|
{% if action == 'create' %}
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const locationsSelect = document.getElementById('id_authorized_locations');
|
|
const section = document.getElementById('initial-quantities-section');
|
|
const container = document.getElementById('initial-quantities-container');
|
|
|
|
if (locationsSelect && section && container) {
|
|
function updateQuantities() {
|
|
container.innerHTML = '';
|
|
const selectedOptions = Array.from(locationsSelect.selectedOptions);
|
|
|
|
if (selectedOptions.length === 0) {
|
|
section.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
section.style.display = 'block';
|
|
selectedOptions.forEach(opt => {
|
|
const locationId = opt.value;
|
|
const locationName = opt.text;
|
|
|
|
const row = document.createElement('div');
|
|
row.className = 'row mb-3 align-items-center';
|
|
row.innerHTML = `
|
|
<div class="col-md-6">
|
|
<span class="fw-semibold text-muted" style="font-size: 0.9rem;">
|
|
<i class="bi bi-geo-alt me-1 text-primary"></i>${locationName}
|
|
</span>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text bg-white text-muted">{% translate "Quantité" %}</span>
|
|
<input type="number" name="initial_qty_${locationId}" class="form-control" min="0" value="0" style="max-width: 150px;">
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(row);
|
|
});
|
|
}
|
|
|
|
locationsSelect.addEventListener('change', updateQuantities);
|
|
// Initial run
|
|
updateQuantities();
|
|
}
|
|
});
|
|
{% endif %}
|
|
</script>
|
|
{% endblock content %}
|