feat: add preparation order management features and mobile intervention detail view
This commit is contained in:
parent
ee90cf3173
commit
fecd7430ad
5 changed files with 195 additions and 6 deletions
|
|
@ -1057,6 +1057,99 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Stock / Bons de préparation -->
|
||||||
|
{% if can_create_preparation_order or preparation_orders %}
|
||||||
|
<div class="card mt-4 mb-4 bg-white" id="section-stock">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fw-semibold d-flex align-items-center gap-1">
|
||||||
|
<i class="bi bi-box-seam me-1 text-primary"></i>{% translate "Stock" %}
|
||||||
|
</span>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
{% if preparation_orders %}
|
||||||
|
<span class="badge bg-secondary rounded-pill" style="font-size:0.75rem;">
|
||||||
|
{{ preparation_orders|length }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_create_preparation_order %}
|
||||||
|
<a href="{% url 'stock:new_preparation_order_from_intervention' intervention.id %}?v=mobile"
|
||||||
|
class="btn btn-light border-0 p-1"
|
||||||
|
title="{% translate 'Créer un bon de préparation' %}">
|
||||||
|
<i class="bi bi-plus-circle text-primary fs-5"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if preparation_orders %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for order in preparation_orders %}
|
||||||
|
<div class="list-group-item px-0 py-2 {% if not forloop.last %}border-bottom{% endif %}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start gap-2">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap mb-1">
|
||||||
|
<a href="{% url 'mobile:stock_preparation_order_detail_mobile' order.pk %}" class="fw-semibold text-decoration-none">
|
||||||
|
#{{ order.order_number }}
|
||||||
|
</a>
|
||||||
|
<span class="badge {{ order.status_badge_class }}">
|
||||||
|
<i class="bi {{ order.status_icon }} me-1"></i>{{ order.get_status_display }}
|
||||||
|
</span>
|
||||||
|
<span class="badge {{ order.priority_badge_class }}">
|
||||||
|
{{ order.get_priority_display }}
|
||||||
|
</span>
|
||||||
|
{% if order.is_loan %}
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
<i class="bi bi-arrow-left-right"></i> {% translate "Prêt" %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mb-1">
|
||||||
|
<i class="bi bi-calendar3 me-1"></i>{{ order.date|date:"d/m/Y H:i" }}
|
||||||
|
</div>
|
||||||
|
{% with items=order.preparationorderitem_set.all other_items=order.other_items.all %}
|
||||||
|
{% if items or other_items %}
|
||||||
|
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||||
|
{% for item in items|slice:":3" %}
|
||||||
|
<span class="badge bg-light text-dark border" style="font-size:0.75rem;">
|
||||||
|
{{ item.product.name }} × {{ item.quantity }}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% for item in other_items|slice:":2" %}
|
||||||
|
<span class="badge bg-light text-dark border" style="font-size:0.75rem;">
|
||||||
|
{{ item.name }} × {{ item.quantity }}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% with total_count=items.count|add:other_items.count %}
|
||||||
|
{% if total_count > 3 %}
|
||||||
|
<span class="badge bg-secondary-subtle text-muted" style="font-size:0.75rem;">
|
||||||
|
+{{ total_count|add:"-3" }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small fst-italic">{% translate "Aucun article" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'mobile:stock_preparation_order_detail_mobile' order.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-primary align-self-center flex-shrink-0"
|
||||||
|
title="{% translate 'Voir les détails' %}">
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0 small">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>{% translate "Aucun bon de préparation créé pour cette intervention." %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<!-- Contrôles / Rapports de suivi -->
|
<!-- Contrôles / Rapports de suivi -->
|
||||||
{% if intervention.status != 'in_preparation' %}
|
{% if intervention.status != 'in_preparation' %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1073,10 +1073,25 @@ def intervention_detail_mobile(request, intervention_id):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
can_create_preparation_order = bool(
|
||||||
|
user_config and (
|
||||||
|
user_config.has_role('admin')
|
||||||
|
or (can_edit_mobile and user_config.can_manage_preparation_orders_any())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
preparation_orders = (
|
||||||
|
intervention.preparation_orders.all()
|
||||||
|
.prefetch_related('preparationorderitem_set__product', 'other_items')
|
||||||
|
.order_by('-date')
|
||||||
|
if hasattr(intervention, 'preparation_orders') else []
|
||||||
|
)
|
||||||
|
|
||||||
sequenced_data = get_sequenced_intervention_documents(intervention, notes_qs=notes)
|
sequenced_data = get_sequenced_intervention_documents(intervention, notes_qs=notes)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'intervention': intervention,
|
'intervention': intervention,
|
||||||
|
'can_create_preparation_order': can_create_preparation_order,
|
||||||
|
'preparation_orders': preparation_orders,
|
||||||
'sequenced_blocks': sequenced_data['blocks'],
|
'sequenced_blocks': sequenced_data['blocks'],
|
||||||
'document_blocks': sequenced_data['blocks'],
|
'document_blocks': sequenced_data['blocks'],
|
||||||
'all_sequenced_documents': sequenced_data['all_documents'],
|
'all_sequenced_documents': sequenced_data['all_documents'],
|
||||||
|
|
|
||||||
|
|
@ -103,3 +103,73 @@ class MobileStockPermissionTests(TestCase):
|
||||||
stock_response = self.client.get(reverse("mobile:stock_index_mobile"))
|
stock_response = self.client.get(reverse("mobile:stock_index_mobile"))
|
||||||
self.assertEqual(stock_response.status_code, 200)
|
self.assertEqual(stock_response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class MobileInterventionDetailStockTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from stock.models import PreparationOrder, PreparationOrderItem, Product, ProductCategory
|
||||||
|
from interventions.models import Intervention
|
||||||
|
|
||||||
|
self.user = User.objects.create_user(username="detailstockuser", password="password")
|
||||||
|
self.client.login(username="detailstockuser", password="password")
|
||||||
|
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
|
||||||
|
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
||||||
|
self.user_config.roles.add(self.admin_role)
|
||||||
|
|
||||||
|
self.thematic, _ = Thematic.objects.get_or_create(code="trafficlights", defaults={"name_fr": "Feux", "name_nl": "Verkeerslichten"})
|
||||||
|
self.user_config.default_thematic = self.thematic
|
||||||
|
self.user_config.save()
|
||||||
|
|
||||||
|
self.intervention = Intervention.objects.create(
|
||||||
|
title="Intervention Test Stock",
|
||||||
|
status="in_progress",
|
||||||
|
thematic=self.thematic,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.category, _ = ProductCategory.objects.get_or_create(code="SIG", defaults={"name_fr": "Signalisation"})
|
||||||
|
self.product = Product.objects.create(
|
||||||
|
name="Poteau feux",
|
||||||
|
code="POT-01",
|
||||||
|
category=self.category,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.prep_order = PreparationOrder.objects.create(
|
||||||
|
order_number="BP-TEST-001",
|
||||||
|
intervention=self.intervention,
|
||||||
|
status="to_process",
|
||||||
|
priority="2",
|
||||||
|
)
|
||||||
|
PreparationOrderItem.objects.create(
|
||||||
|
preparation_order=self.prep_order,
|
||||||
|
product=self.product,
|
||||||
|
quantity=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stock_section_in_mobile_intervention_detail(self):
|
||||||
|
url = reverse("mobile:intervention_detail_mobile", args=[self.intervention.id])
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn("can_create_preparation_order", response.context)
|
||||||
|
self.assertTrue(response.context["can_create_preparation_order"])
|
||||||
|
self.assertIn("preparation_orders", response.context)
|
||||||
|
self.assertEqual(len(response.context["preparation_orders"]), 1)
|
||||||
|
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
self.assertIn("BP-TEST-001", content)
|
||||||
|
self.assertIn("Poteau feux", content)
|
||||||
|
self.assertIn("section-stock", content)
|
||||||
|
|
||||||
|
def test_stock_section_without_create_rights(self):
|
||||||
|
# Non-admin user without preparation order management rights
|
||||||
|
self.user_config.roles.remove(self.admin_role)
|
||||||
|
observer_role, _ = Role.objects.get_or_create(name="observer")
|
||||||
|
self.user_config.roles.add(observer_role)
|
||||||
|
|
||||||
|
url = reverse("mobile:intervention_detail_mobile", args=[self.intervention.id])
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFalse(response.context["can_create_preparation_order"])
|
||||||
|
self.assertEqual(len(response.context["preparation_orders"]), 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" id="preparation-order-form">
|
<form method="post" enctype="multipart/form-data" id="preparation-order-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% if variant %}<input type="hidden" name="v" value="{{ variant }}">{% endif %}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
|
|
@ -262,9 +263,15 @@
|
||||||
<!-- Envoi du formulaire -->
|
<!-- Envoi du formulaire -->
|
||||||
<div class="d-flex justify-content-between mt-4">
|
<div class="d-flex justify-content-between mt-4">
|
||||||
{% if from_intervention %}
|
{% if from_intervention %}
|
||||||
<a href="{% url 'interventions:intervention_detail' intervention.id %}" class="btn btn-secondary">
|
{% if variant == 'mobile' %}
|
||||||
<i class="bi bi-arrow-left"></i> {% translate "Retour à l'intervention" %}
|
<a href="{% url 'mobile:intervention_detail_mobile' intervention.id %}" class="btn btn-secondary">
|
||||||
</a>
|
<i class="bi bi-arrow-left"></i> {% translate "Retour à l'intervention" %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'interventions:intervention_detail' intervention.id %}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> {% translate "Retour à l'intervention" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'stock:stock_movements' %}" class="btn btn-secondary">
|
<a href="{% url 'stock:stock_movements' %}" class="btn btn-secondary">
|
||||||
<i class="bi bi-arrow-left"></i> {% translate "Retour" %}
|
<i class="bi bi-arrow-left"></i> {% translate "Retour" %}
|
||||||
|
|
|
||||||
|
|
@ -3562,12 +3562,15 @@ def new_preparation_order_from_intervention(request, intervention_id):
|
||||||
from interventions.permissions import can_edit_intervention
|
from interventions.permissions import can_edit_intervention
|
||||||
from common.models import UserConfig
|
from common.models import UserConfig
|
||||||
|
|
||||||
|
variant = request.GET.get('v') or request.POST.get('v')
|
||||||
|
redirect_target = "mobile:intervention_detail_mobile" if variant == 'mobile' else "interventions:intervention_detail"
|
||||||
|
|
||||||
# Vérifier les permissions
|
# Vérifier les permissions
|
||||||
try:
|
try:
|
||||||
user_config = request.user.config
|
user_config = request.user.config
|
||||||
except UserConfig.DoesNotExist:
|
except UserConfig.DoesNotExist:
|
||||||
messages.error(request, _("Configuration utilisateur non trouvée."))
|
messages.error(request, _("Configuration utilisateur non trouvée."))
|
||||||
return redirect("interventions:intervention_detail", intervention_id=intervention_id)
|
return redirect(redirect_target, intervention_id=intervention_id)
|
||||||
|
|
||||||
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
||||||
|
|
||||||
|
|
@ -3581,7 +3584,7 @@ def new_preparation_order_from_intervention(request, intervention_id):
|
||||||
|
|
||||||
if not can_create_from_intervention:
|
if not can_create_from_intervention:
|
||||||
messages.error(request, _("Vous n'avez pas la permission de créer des bons de préparation."))
|
messages.error(request, _("Vous n'avez pas la permission de créer des bons de préparation."))
|
||||||
return redirect("interventions:intervention_detail", intervention_id=intervention_id)
|
return redirect(redirect_target, intervention_id=intervention_id)
|
||||||
|
|
||||||
preparation_order_item_formset = inlineformset_factory(
|
preparation_order_item_formset = inlineformset_factory(
|
||||||
PreparationOrder, PreparationOrderItem, form=PreparationOrderItemForm, extra=3, can_delete=True
|
PreparationOrder, PreparationOrderItem, form=PreparationOrderItemForm, extra=3, can_delete=True
|
||||||
|
|
@ -3609,7 +3612,7 @@ def new_preparation_order_from_intervention(request, intervention_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, _("Bon de préparation créé avec succès."))
|
messages.success(request, _("Bon de préparation créé avec succès."))
|
||||||
return redirect("interventions:intervention_detail", intervention_id=intervention_id)
|
return redirect(redirect_target, intervention_id=intervention_id)
|
||||||
else:
|
else:
|
||||||
# Générer un numéro de bon automatique
|
# Générer un numéro de bon automatique
|
||||||
import datetime
|
import datetime
|
||||||
|
|
@ -3637,6 +3640,7 @@ def new_preparation_order_from_intervention(request, intervention_id):
|
||||||
"from_intervention": True,
|
"from_intervention": True,
|
||||||
"categories": categories,
|
"categories": categories,
|
||||||
"signalisation_category_ids": signalisation_ids,
|
"signalisation_category_ids": signalisation_ids,
|
||||||
|
"variant": variant,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue