diff --git a/streetup/stock/models.py b/streetup/stock/models.py index 04f5295..ba3f3be 100644 --- a/streetup/stock/models.py +++ b/streetup/stock/models.py @@ -1387,12 +1387,55 @@ class PreparationOrder(models.Model): self.save() self.update_reserved_quantities() + def reopen(self): + """Réouvre un bon de préparation traité pour modification (statut : to_process).""" + if self.status == "processed": + self.status = "to_process" + self.save() + self.update_reserved_quantities() + def cancel(self): """Annule le bon et libère les quantités réservées""" self.status = "cancelled" self.save() self.update_reserved_quantities() + def get_executed_outputs_by_product_and_location(self): + """ + Retourne un dictionnaire des mouvements de sortie nets déjà exécutés pour ce bon. + Structure : { product_id: { location_id: net_quantity } } + Calcul : sorties (TYPE_OUT) moins entrées/retours (TYPE_IN). + """ + executed = {} + for batch in self.stock_batches.all(): + for move in batch.movements.all(): + prod_id = move.product_id + if prod_id not in executed: + executed[prod_id] = {} + + if batch.movement_type == "out" and move.source_location_id: + loc_id = move.source_location_id + executed[prod_id][loc_id] = executed[prod_id].get(loc_id, 0) + move.quantity + elif batch.movement_type == "in" and move.destination_location_id: + loc_id = move.destination_location_id + executed[prod_id][loc_id] = executed[prod_id].get(loc_id, 0) - move.quantity + + res = {} + for p_id, loc_dict in executed.items(): + filtered_locs = {l_id: qty for l_id, qty in loc_dict.items() if qty > 0} + if filtered_locs: + res[p_id] = filtered_locs + return res + + def get_executed_output_quantities(self): + """ + Retourne la quantité totale nette sortie par produit pour ce bon. + Structure : { product_id: net_total_quantity } + """ + outputs_by_loc = self.get_executed_outputs_by_product_and_location() + return {p_id: sum(loc_dict.values()) for p_id, loc_dict in outputs_by_loc.items()} + + class PreparationOrderItem(models.Model): preparation_order = models.ForeignKey(PreparationOrder, on_delete=models.CASCADE) diff --git a/streetup/stock/templates/stock/preparation_order_detail.html b/streetup/stock/templates/stock/preparation_order_detail.html index 65adfba..d05bc88 100644 --- a/streetup/stock/templates/stock/preparation_order_detail.html +++ b/streetup/stock/templates/stock/preparation_order_detail.html @@ -21,7 +21,11 @@
- {% if preparation_order.status == 'to_process' %} + {% if is_reopened %} + + {% translate "Réouvert" %} + + {% elif preparation_order.status == 'to_process' %} {{ preparation_order.get_status_display }} @@ -60,6 +64,18 @@
+ {% if is_reopened %} + + {% endif %} + {% if preparation_order.intervention %}
@@ -232,19 +248,23 @@
- +
+ {% if has_executed_outputs %} + + {% endif %} {% if preparation_order.status == 'to_process' or preparation_order.status == 'pending' %} {% endif %} + @@ -268,13 +288,38 @@ + {% if has_executed_outputs %} + + {% endif %} {% if preparation_order.status == 'to_process' or preparation_order.status == 'pending' %} + {% endfor %}
{% translate "Produit" %} {% translate "Code" %} {% translate "Catégorie" %} {% translate "Quantité demandée" %}{% translate "À prélever / à restituer" %}{% translate "Quantité réservée" %} {% translate "Stock disponible" %} {% translate "Emplacements" %}{% translate "Bon de confection" %}{% translate "Préparé" %}
{% if item_data.item.quantity > 0 %} - {{ item_data.item.quantity }} {{ item_data.item.product.unit }} + {{ item_data.item.quantity }} {{ item_data.item.product.unit }} {% else %} {% translate "Ajouté" %} {% endif %} + {% if item_data.executed_qty > 0 %} + {% if item_data.delta > 0 %} + + +{{ item_data.delta }} {% translate "à prélever" %} + +
{% translate "Sorti" %}: {{ item_data.executed_qty }} + {% elif item_data.delta < 0 %} + + {{ item_data.delta }} {% translate "à restituer" %} + +
{% translate "Sorti" %}: {{ item_data.executed_qty }} + {% else %} + + {% translate "Inchangé" %} ({{ item_data.executed_qty }}) + + {% endif %} + {% else %} + + {{ item_data.item.quantity }} {% translate "à prélever" %} + + {% endif %} +
@@ -316,11 +361,67 @@ — {% endif %} +
+ +
+
+ {% if deleted_items_to_return %} +
+
+ {% translate "Articles supprimés du bon (restitution automatique au stock)" %} : +
+
+ + + + + + + + + + + {% for del in deleted_items_to_return %} + + + + + + + {% endfor %} + +
{% translate "Produit" %}{% translate "Emplacement(s) d'origine" %}{% translate "Quantité à restituer" %}{% translate "Préparé" %}
{{ del.product.name }} ({{ del.product.code }}){{ del.location_info }} + +{{ del.executed_qty }} {{ del.product.unit }} {% translate "à restituer" %} + + {% if del.item %} +
+ +
+ {% else %} + — + {% endif %} +
+
+
+ {% endif %}
@@ -335,7 +436,7 @@
- +
@@ -343,6 +444,7 @@ {% if can_manage %}{% endif %} + @@ -384,6 +486,17 @@ {% endif %} {% endif %} + {% endfor %} @@ -449,11 +562,11 @@
-
{% translate "Traiter le bon de préparation" %}
-

{% translate "Sortir les produits du stock et marquer comme traité" %}

- - {% translate "Traiter" %} - +
{% translate "Signaler comme traité" %}
+

{% translate "Valider la préparation et enregistrer les mouvements de stock" %}

+
@@ -508,6 +621,42 @@ {% endif %} + {% elif preparation_order.status == 'processed' %} +
+
+
+ +
{% translate "Réouvrir le bon de préparation" %}
+

{% translate "Permet de modifier les articles du bon (repasse au statut À traiter)" %}

+
+ {% csrf_token %} + + +
+
+
+ + {% if can_cancel %} +
+
+
+ +
{% translate "Annuler le bon de préparation" %}
+

{% translate "Annuler le bon et remettre tous les produits sortis en stock" %}

+
+ {% csrf_token %} + + +
+
+
+ {% endif %} {% endif %} @@ -674,9 +823,6 @@ @@ -705,7 +851,6 @@ } function markChanged() { hasChanges = true; - document.getElementById('reloadPageBtn').style.display = 'inline-block'; } function buildRow(item) { @@ -719,12 +864,7 @@
{% translate "Description" %}{% translate "Type (Signalisation)" %} {% translate "Quantité" %}{% translate "Préparé" %}
+
+ +
+
${item.product_name} ${lockBadge} ${item.product_code} -
- - -
+
${catLabel} ${signLabel} -
- - -
+
+ +
+ {% csrf_token %} + + + +
+ + + +