296 lines
15 KiB
HTML
296 lines
15 KiB
HTML
{% extends "base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid mt-3">
|
|
|
|
<!-- En-tête -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h3 class="h3 d-flex align-items-center gap-2">
|
|
<i class="bi bi-upc-scan"></i>
|
|
<span class="font-monospace">{{ item.serial_number }}</span>
|
|
{% if not item.is_active %}
|
|
<span class="badge bg-danger ms-2"><i class="bi bi-x-circle me-1"></i>{% translate "Inactif" %}</span>
|
|
{% endif %}
|
|
</h3>
|
|
<div class="d-flex gap-2">
|
|
{% if can_manage and item.is_active %}
|
|
<button type="button" class="btn btn-outline-danger"
|
|
data-bs-toggle="modal" data-bs-target="#deleteItemModal">
|
|
<i class="bi bi-trash"></i> {% translate "Supprimer" %}
|
|
</button>
|
|
{% endif %}
|
|
<a href="{% url 'stock:serialized_items_list' %}" class="btn btn-light">
|
|
<i class="bi bi-arrow-left"></i> {% translate "Retour" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if can_manage and item.is_active %}
|
|
<!-- Modal suppression -->
|
|
<div class="modal fade" id="deleteItemModal" 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 sn=item.serial_number %}
|
|
Êtes-vous sûr de vouloir rendre l'article <strong>{{ sn }}</strong> inactif ?
|
|
Il ne sera plus visible pour les nouvelles opérations de stock, 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:serialized_item_delete' item.pk %}">
|
|
{% csrf_token %}
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="bi bi-x-circle me-1"></i> {% translate "Rendre inactif" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Cartes d'informations -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-subtitle mb-2 text-muted"><i class="bi bi-box-seam me-1"></i>{% translate "Produit" %}</h6>
|
|
<p class="card-text fs-5 mb-1">
|
|
<a href="{% url 'stock:product_detail' item.product.pk %}">{{ item.product.name }}</a>
|
|
</p>
|
|
<small class="text-muted">{{ item.product.code }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card h-100">
|
|
<div class="card-body text-center">
|
|
<h6 class="card-subtitle mb-2 text-muted">{% translate "Statut" %}</h6>
|
|
{% if item.status == 'available' %}
|
|
<span class="badge bg-success fs-6">{{ item.get_status_display }}</span>
|
|
{% elif item.status == 'assigned' %}
|
|
<span class="badge bg-primary fs-6">{{ item.get_status_display }}</span>
|
|
{% elif item.status == 'lost' %}
|
|
<span class="badge bg-danger fs-6">{{ item.get_status_display }}</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary fs-6">{{ item.get_status_display }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-subtitle mb-2 text-muted"><i class="bi bi-person me-1"></i>{% translate "Détenteur actuel" %}</h6>
|
|
{% if item.current_holder %}
|
|
<p class="card-text fs-5 mb-0">{{ item.current_holder.name }}</p>
|
|
<small class="text-muted">{{ item.current_holder.teams_display|default:"—" }}</small>
|
|
{% else %}
|
|
<p class="card-text text-muted fst-italic">{% translate "Aucun" %}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-subtitle mb-2 text-muted"><i class="bi bi-geo-alt me-1"></i>{% translate "Emplacement actuel" %}</h6>
|
|
{% if item.current_location %}
|
|
<p class="card-text fs-5 mb-0">{{ item.current_location.name }}</p>
|
|
<small class="text-muted">{{ item.current_location.warehouse.name }}</small>
|
|
{% else %}
|
|
<p class="card-text text-muted fst-italic">{% translate "Non renseigné" %}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if item.qr_code %}
|
|
<div class="col-md-1">
|
|
<div class="card h-100">
|
|
<div class="card-body text-center p-2">
|
|
<h6 class="card-subtitle mb-2 text-muted small">{% translate "QR Code" %}</h6>
|
|
<a href="{{ item.qr_code.url }}" target="_blank" title="{% translate 'Ouvrir le QR code' %}">
|
|
<img src="{{ item.qr_code.url }}" alt="QR Code {{ item.serial_number }}" class="img-fluid">
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if item.notes %}
|
|
<div class="alert alert-light border mb-4">
|
|
<strong><i class="bi bi-sticky me-1"></i>{% translate "Notes" %} :</strong> {{ item.notes }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Formulaire d'événement -->
|
|
{% if can_manage and item.status != 'retired' and item.is_active %}
|
|
<div class="card mb-4 border-primary">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0"><i class="bi bi-plus-circle me-1"></i>{% translate "Enregistrer un événement" %}</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="post" action="{% url 'stock:serialized_item_event_create' item.pk %}">
|
|
{% csrf_token %}
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-semibold">{% translate "Type d'événement" %} *</label>
|
|
<select name="event_type" class="form-select" id="event_type_select" required>
|
|
<option value="">— {% translate "Choisir" %} —</option>
|
|
<option value="assigned">{% translate "Attribué" %}</option>
|
|
<option value="returned">{% translate "Rendu" %}</option>
|
|
<option value="transferred">{% translate "Transféré" %}</option>
|
|
<option value="lost">{% translate "Perdu" %}</option>
|
|
<option value="retired">{% translate "Retiré du service" %}</option>
|
|
<option value="note">{% translate "Note" %}</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3" id="field_person">
|
|
<label class="form-label fw-semibold">{% translate "Personne" %}</label>
|
|
<select name="person" class="form-select">
|
|
<option value="">— {% translate "Aucune" %} —</option>
|
|
{% for member in members %}
|
|
<option value="{{ member.pk }}" {% if item.current_holder_id == member.pk %}selected{% endif %}>
|
|
{{ member.name }}{% if member.teams.exists %} ({{ member.teams_display }}){% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3" id="field_to_location">
|
|
<label class="form-label fw-semibold">{% translate "Emplacement de destination" %}</label>
|
|
<select name="to_location" class="form-select">
|
|
<option value="">— {% translate "Aucun" %} —</option>
|
|
{% for loc in locations %}
|
|
<option value="{{ loc.pk }}">{{ loc.warehouse.name }} — {{ loc.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-semibold">{% translate "Remarques" %}</label>
|
|
<input type="text" name="notes" class="form-control" placeholder="{% translate 'Remarques optionnelles…' %}">
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-check2"></i> {% translate "Enregistrer" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Historique -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="bi bi-clock-history me-1"></i>{% translate "Historique" %}</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{% translate "Date" %}</th>
|
|
<th>{% translate "Événement" %}</th>
|
|
<th>{% translate "Personne" %}</th>
|
|
<th>{% translate "Depuis" %}</th>
|
|
<th>{% translate "Vers" %}</th>
|
|
<th>{% translate "Remarques" %}</th>
|
|
<th>{% translate "Enregistré par" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for event in events %}
|
|
<tr>
|
|
<td class="text-nowrap">{{ event.date|date:"d/m/Y H:i" }}</td>
|
|
<td>
|
|
{% if event.event_type == 'entry' %}
|
|
<span class="badge bg-info text-dark">{{ event.get_event_type_display }}</span>
|
|
{% elif event.event_type == 'assigned' %}
|
|
<span class="badge bg-primary">{{ event.get_event_type_display }}</span>
|
|
{% elif event.event_type == 'returned' %}
|
|
<span class="badge bg-success">{{ event.get_event_type_display }}</span>
|
|
{% elif event.event_type == 'transferred' %}
|
|
<span class="badge bg-warning text-dark">{{ event.get_event_type_display }}</span>
|
|
{% elif event.event_type == 'lost' %}
|
|
<span class="badge bg-danger">{{ event.get_event_type_display }}</span>
|
|
{% elif event.event_type == 'retired' %}
|
|
<span class="badge bg-secondary">{{ event.get_event_type_display }}</span>
|
|
{% else %}
|
|
<span class="badge bg-light text-dark border">{{ event.get_event_type_display }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ event.person.name|default:"—" }}</td>
|
|
<td>
|
|
{% if event.from_location %}
|
|
{{ event.from_location.name }}
|
|
<small class="text-muted d-block">{{ event.from_location.warehouse.name }}</small>
|
|
{% else %}—{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if event.to_location %}
|
|
{{ event.to_location.name }}
|
|
<small class="text-muted d-block">{{ event.to_location.warehouse.name }}</small>
|
|
{% else %}—{% endif %}
|
|
</td>
|
|
<td>{{ event.notes|default:"—" }}</td>
|
|
<td class="text-muted small">{{ event.created_by.get_full_name|default:event.created_by.username|default:"—" }}</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted py-3">{% translate "Aucun événement enregistré." %}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-muted small mt-2">
|
|
{% translate "Enregistré le" %} {{ item.created_at|date:"d/m/Y H:i" }}
|
|
{% if item.created_by %}{% translate "par" %} {{ item.created_by.get_full_name|default:item.created_by.username }}{% endif %}
|
|
</p>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
const locationSelect = document.querySelector('select[name="to_location"]');
|
|
if (locationSelect) {
|
|
const originalOptions = Array.from(locationSelect.options);
|
|
const apiBaseUrl = "{% url 'stock:product_locations_api' item.product.id %}";
|
|
|
|
locationSelect.disabled = true;
|
|
fetch(apiBaseUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const allowedIds = new Set(data.locations.map(loc => loc.id.toString()));
|
|
|
|
locationSelect.innerHTML = "";
|
|
originalOptions.forEach(opt => {
|
|
if (opt.value === "" || allowedIds.has(opt.value)) {
|
|
locationSelect.appendChild(opt.cloneNode(true));
|
|
}
|
|
});
|
|
locationSelect.disabled = false;
|
|
})
|
|
.catch(error => {
|
|
console.error("Error fetching locations:", error);
|
|
locationSelect.disabled = false;
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock content %}
|