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 %} + +{% 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