diff --git a/streetup/mobile/views.py b/streetup/mobile/views.py
index e022ccb..7d4fc98 100644
--- a/streetup/mobile/views.py
+++ b/streetup/mobile/views.py
@@ -963,21 +963,60 @@ def stock_preparation_order_detail_mobile(request, pk):
messages.success(request, _('Document(s) ajouté(s) avec succès.'))
return redirect('mobile:stock_preparation_order_detail_mobile', pk=pk)
+ from stock.models import PreparationOrderItem, WarehouseLocation
+ executed_by_loc = preparation_order.get_executed_outputs_by_product_and_location()
+ executed_totals = {p_id: sum(locs.values()) for p_id, locs in executed_by_loc.items()}
+ has_executed_outputs = bool(executed_totals)
+ is_reopened = preparation_order.status == 'to_process' and has_executed_outputs
+
+ # S'assurer qu'une ligne PreparationOrderItem avec quantity=0 existe pour tout produit sorti mais supprimé du bon
+ for p_id, loc_dict in executed_by_loc.items():
+ executed_qty = sum(loc_dict.values())
+ if executed_qty > 0:
+ PreparationOrderItem.objects.get_or_create(
+ preparation_order=preparation_order,
+ product_id=p_id,
+ defaults={'quantity': 0}
+ )
+
items_with_stock = []
- for item in preparation_order.preparationorderitem_set.all():
+ for item in preparation_order.preparationorderitem_set.filter(quantity__gt=0):
product = item.product
available_stock = product.total_stock
reserved_stock = product.reserved_quantity
prod_order = item.production_orders.first()
+ executed_qty = executed_totals.get(product.id, 0)
+ delta = item.quantity - executed_qty
+
items_with_stock.append({
'item': item,
'available_stock': available_stock,
'reserved_stock': reserved_stock,
'is_sufficient': available_stock >= item.quantity,
'production_order': prod_order,
+ 'executed_qty': executed_qty,
+ 'delta': delta,
'locations': Stock.objects.filter(product=product, quantity__gt=0).select_related('location__warehouse'),
})
+ deleted_items_to_return = []
+ for item in preparation_order.preparationorderitem_set.filter(quantity=0):
+ executed_qty = executed_totals.get(item.product_id, 0)
+ if executed_qty > 0:
+ product = item.product
+ loc_dict = executed_by_loc.get(product.id, {})
+ loc_names = []
+ for l_id, q in loc_dict.items():
+ loc_obj = WarehouseLocation.objects.filter(pk=l_id).select_related('warehouse').first()
+ if loc_obj:
+ loc_names.append(f"{loc_obj.name} ({loc_obj.warehouse.name}): {q}")
+ deleted_items_to_return.append({
+ 'item': item,
+ 'product': product,
+ 'executed_qty': executed_qty,
+ 'location_info': ", ".join(loc_names),
+ })
+
try:
user_config = request.user.config
can_manage = user_config.has_role('stock_manager') or user_config.has_role('admin') or user_config.can_manage_preparation_orders_any()
@@ -1007,6 +1046,9 @@ def stock_preparation_order_detail_mobile(request, pk):
'can_manage_purchase': can_manage_purchase,
'exit_batches': exit_batches,
'return_batches': return_batches,
+ 'has_executed_outputs': has_executed_outputs,
+ 'is_reopened': is_reopened,
+ 'deleted_items_to_return': deleted_items_to_return,
})
diff --git a/streetup/stock/templates/stock/mobile/stock_preparation_order_detail_mobile.html b/streetup/stock/templates/stock/mobile/stock_preparation_order_detail_mobile.html
index c7f9ff9..062f305 100644
--- a/streetup/stock/templates/stock/mobile/stock_preparation_order_detail_mobile.html
+++ b/streetup/stock/templates/stock/mobile/stock_preparation_order_detail_mobile.html
@@ -69,15 +69,33 @@
{{ preparation_order.date|date:"d/m/Y H:i" }}
-
- {{ preparation_order.get_status_display }}
-
+ {% if is_reopened %}
+
+ {% translate "Réouvert" %}
+
+ {% else %}
+
+ {{ preparation_order.get_status_display }}
+
+ {% endif %}
{{ preparation_order.get_priority_display }}
+{% if is_reopened %}
+
+
+
+
+
{% translate "Bon réouvert pour modification" %}
+
{% translate "Ce bon a fait l'objet d'un traitement initial. Les deltas et restitutions seront traités à la confirmation." %}
+
+
+
+{% endif %}
+
{% if preparation_order.intervention %}
@@ -250,6 +268,42 @@
{% endif %}
{% endif %}
+
+
+ {% if has_executed_outputs %}
+
+ {% if item_data.executed_qty > 0 %}
+ {% if item_data.delta > 0 %}
+ +{{ item_data.delta }} {{ item_data.item.product.unit }} {% translate "à prélever" %}
+ ({% translate "Sorti" %}: {{ item_data.executed_qty }})
+ {% elif item_data.delta < 0 %}
+ {{ item_data.delta }} {{ item_data.item.product.unit }} {% translate "à restituer" %}
+ ({% translate "Sorti" %}: {{ item_data.executed_qty }})
+ {% else %}
+ {% translate "Inchangé" %} ({{ item_data.executed_qty }})
+ {% endif %}
+ {% else %}
+ {{ item_data.item.quantity }} {{ item_data.item.product.unit }} {% translate "à prélever" %}
+ {% endif %}
+
+ {% endif %}
+
+
+
+
+ {% translate "Préparé" %}
+
+
+
+
+
+
{% if item_data.production_order %}
{% endif %}
+
+
+
+ {% translate "Préparé" %}
+
+
+
+
+
{% endfor %}
{% endif %}
-{% if not items_with_stock and not other_items %}
+
+{% if deleted_items_to_return %}
+
+ {% translate "Articles supprimés (restitution au stock)" %}
+ {{ deleted_items_to_return|length }}
+
+{% for del in deleted_items_to_return %}
+
+
+
{{ del.product.name }}
+
+{{ del.executed_qty }} {{ del.product.unit }} {% translate "à restituer" %}
+
+
+ {{ del.product.code }} · {{ del.location_info }}
+
+ {% if del.item %}
+
+
+ {% translate "Restitué / Préparé" %}
+
+
+
+
+
+ {% endif %}
+
+{% endfor %}
+{% endif %}
+
+{% if not items_with_stock and not other_items and not deleted_items_to_return %}
{% translate "Aucun article dans ce bon." %}
@@ -322,9 +426,9 @@
{% if can_manage %}
{% if preparation_order.status == 'to_process' or preparation_order.status == 'pending' %}
-
- {% translate "Traiter le bon" %}
-
+
{% if can_manage_purchase %}
{% translate "Créer un bon de commande" %}
@@ -522,6 +626,93 @@ document.addEventListener('DOMContentLoaded', function() {
modalIsLoan.addEventListener('change', toggleModalReturnDate);
toggleModalReturnDate();
}
+
+ // Bascule Ajax état Préparé
+ document.addEventListener('change', function (e) {
+ const toggle = e.target.closest('.prepare-toggle-input');
+ if (!toggle) return;
+ const itemId = toggle.dataset.itemId;
+ const isOther = toggle.dataset.isOther === 'true';
+ const url = isOther
+ ? `/stock/preparation-order-other-items/${itemId}/toggle-prepared/`
+ : `/stock/preparation-order-items/${itemId}/toggle-prepared/`;
+
+ fetch(url, {
+ method: 'POST',
+ headers: {'X-CSRFToken': '{{ csrf_token }}', 'X-Requested-With': 'XMLHttpRequest'},
+ })
+ .then(r => r.json())
+ .then(data => {
+ if (!data.success) {
+ toggle.checked = !toggle.checked;
+ alert(data.error || 'Erreur lors de la mise à jour');
+ }
+ })
+ .catch(() => {
+ toggle.checked = !toggle.checked;
+ });
+ });
});
+
+
+
{% endblock content %}
diff --git a/streetup/stock/views.py b/streetup/stock/views.py
index a6bf5af..033baf5 100644
--- a/streetup/stock/views.py
+++ b/streetup/stock/views.py
@@ -4461,6 +4461,9 @@ def preparation_order_process(request, pk):
messages.success(request, _("Le bon de préparation #{} a été traité avec succès.").format(
preparation_order.order_number
))
+ next_url = request.POST.get('next') or request.META.get('HTTP_REFERER', '')
+ if 'mobile' in str(next_url):
+ return redirect('mobile:stock_preparation_order_detail_mobile', pk=pk)
return redirect('stock:preparation_order_detail', pk=pk)
# Préparer les données pour le formulaire