From ac3dcac64167700ea701c898117aaf71171d57a4 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Mon, 2 Aug 2021 15:05:24 -0400
Subject: [PATCH] Re-enabled installing stock items into others

---
 .../build/templates/build/build_base.html     |  4 +-
 InvenTree/stock/forms.py                      | 23 +++--
 InvenTree/stock/templates/stock/item.html     | 19 ++++
 .../stock/templates/stock/item_base.html      | 24 ++++-
 .../stock/templates/stock/item_install.html   | 22 ++++-
 InvenTree/stock/views.py                      | 92 +++++++++++++++----
 6 files changed, 146 insertions(+), 38 deletions(-)

diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index 5770777d28..e3119e6fdb 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -111,8 +111,8 @@ src="{% static 'img/blank_image.png' %}"
             <li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
             {% endif %}
             {% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
-            <li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a>
-                {% endif %}
+            <li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build" %}</a>
+            {% endif %}
         </ul>
     </div>
     {% endif %}
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index b23b71a2d6..b088a6cdef 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -241,16 +241,21 @@ class InstallStockForm(HelperForm):
         help_text=_('Stock item to install')
     )
 
-    quantity_to_install = RoundingDecimalFormField(
-        max_digits=10, decimal_places=5,
-        initial=1,
-        label=_('Quantity'),
-        help_text=_('Stock quantity to assign'),
-        validators=[
-            MinValueValidator(0.001)
-        ]
+    to_install = forms.BooleanField(
+        widget=forms.HiddenInput(), 
+        required=False
     )
 
+    # quantity_to_install = RoundingDecimalFormField(
+    #     max_digits=10, decimal_places=5,
+    #     initial=1,
+    #     label=_('Quantity'),
+    #     help_text=_('Stock quantity to assign'),
+    #     validators=[
+    #         MinValueValidator(0.001)
+    #     ]
+    # )
+
     notes = forms.CharField(
         required=False,
         help_text=_('Notes')
@@ -261,7 +266,7 @@ class InstallStockForm(HelperForm):
         fields = [
             'part',
             'stock_item',
-            'quantity_to_install',
+            # 'quantity_to_install',
             'notes',
         ]
 
diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html
index 8a00c1c5e6..d380ea3369 100644
--- a/InvenTree/stock/templates/stock/item.html
+++ b/InvenTree/stock/templates/stock/item.html
@@ -119,6 +119,11 @@
         <h4>{% trans "Installed Stock Items" %}</h4>
     </div>
     <div class='panel-content'>
+        <div class='btn-group'>
+        <button type='button' class='btn btn-success' id='stock-item-install'>
+            <span class='fas fa-plus-circle'></span> {% trans "Install Stock Item" %}
+        </button>
+        </div>
         <table class='table table-striped table-condensed' id='installed-table'></table>
     </div>
 </div>
@@ -128,6 +133,20 @@
 {% block js_ready %}
 {{ block.super }}
 
+    $('#stock-item-install').click(function() {
+
+        launchModalForm(
+            "{% url 'stock-item-install' item.pk %}",
+            {
+                data: {
+                    'part': {{ item.part.pk }},
+                    'install_item': true,
+                },
+                reload: true,
+            }
+        );
+    });
+
     loadInstalledInTable(
         $('#installed-table'),
         {
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index b16d9b0b1a..93698e3d05 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -127,9 +127,11 @@
                 <li><a href='#' id='stock-return-from-customer' title='{% trans "Return to stock" %}'><span class='fas fa-undo'></span> {% trans "Return to stock" %}</a></li>
                 {% endif %}
                 {% if item.belongs_to %}
-                <li>
-                    <a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a>
-                </li>
+                <li><a href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a></li>
+                {% else %}
+                {% if item.part.get_used_in %}
+                <li><a href='#' id='stock-install-in' title='{% trans "Install stock item" %}'><span class='fas fa-link'></span> {% trans "Install" %}</a></li>
+                {% endif %}
                 {% endif %}
             </ul>
         </div>
@@ -461,13 +463,27 @@ $("#stock-serialize").click(function() {
     );
 });
 
+$('#stock-install-in').click(function() {
+
+    launchModalForm(
+        "{% url 'stock-item-install' item.pk %}",
+        {
+            data: {
+                'part': {{ item.part.pk }},
+                'install_in': true,
+            },
+            reload: true,
+        }
+    );
+});
+
 $('#stock-uninstall').click(function() {
 
     launchModalForm(
         "{% url 'stock-item-uninstall' %}",
         {
             data: {
-                'items[]': [{{ item.pk}}],
+                'items[]': [{{ item.pk }}],
             },
             reload: true,
         }
diff --git a/InvenTree/stock/templates/stock/item_install.html b/InvenTree/stock/templates/stock/item_install.html
index 04798972d2..8a94f304d3 100644
--- a/InvenTree/stock/templates/stock/item_install.html
+++ b/InvenTree/stock/templates/stock/item_install.html
@@ -3,15 +3,31 @@
 
 {% block pre_form_content %}
 
+{% if install_item %}
 <p>
-    {% trans "Install another StockItem into this item." %}
+    {% trans "Install another Stock Item into this item." %}
 </p>
 <p>
     {% trans "Stock items can only be installed if they meet the following criteria" %}:
 
     <ul>
-        <li>{% trans "The StockItem links to a Part which is in the BOM for this StockItem" %}</li>
-        <li>{% trans "The StockItem is currently in stock" %}</li>
+        <li>{% trans "The Stock Item links to a Part which is in the BOM for this Stock Item" %}</li>
+        <li>{% trans "The Stock Item is currently in stock" %}</li>
+        <li>{% trans "The Stock Item is serialized and does not belong to another item" %}</li>
     </ul>
 </p>
+{% elif install_in %}
+<p>
+    {% trans "Install this Stock Item in another stock item." %}
+</p>
+<p>
+    {% trans "Stock items can only be installed if they meet the following criteria" %}:
+
+    <ul>
+        <li>{% trans "The part associated to this Stock Item belongs to another part's BOM" %}</li>
+        <li>{% trans "This Stock Item is serialized and does not belong to another item" %}</li>
+    </ul>
+</p>
+{% endif %}
+
 {% endblock %}
\ No newline at end of file
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 80968e5aa9..25f8cefac1 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -518,36 +518,73 @@ class StockItemInstall(AjaxUpdateView):
 
     part = None
 
+    def get_params(self):
+        """ Retrieve GET parameters """
+
+        # Look at GET params
+        self.part_id = self.request.GET.get('part', None)
+        self.install_in = self.request.GET.get('install_in', False)
+        self.install_item = self.request.GET.get('install_item', False)
+
+        if self.part_id is None:
+            # Look at POST params
+            self.part_id = self.request.POST.get('part', None)
+
+        try:
+            self.part = Part.objects.get(pk=self.part_id)
+        except (ValueError, Part.DoesNotExist):
+            self.part = None
+
     def get_stock_items(self):
         """
         Return a list of stock items suitable for displaying to the user.
 
         Requirements:
         - Items must be in stock
-
-        Filters:
-        - Items can be filtered by Part reference
+        - Items must be in BOM of stock item
+        - Items must be serialized
         """
-
+        
+        # Filter items in stock
         items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
 
-        # Filter by Part association
+        # Filter serialized stock items
+        items = items.exclude(serial__isnull=True).exclude(serial__exact='')
 
-        # Look at GET params
-        part_id = self.request.GET.get('part', None)
+        if self.part:
+            # Filter for parts to install this item in
+            if self.install_in:
+                # Get parts using this part
+                allowed_parts = self.part.get_used_in()
+                # Filter
+                items = items.filter(part__in=allowed_parts)
 
-        if part_id is None:
-            # Look at POST params
-            part_id = self.request.POST.get('part', None)
-
-        try:
-            self.part = Part.objects.get(pk=part_id)
-            items = items.filter(part=self.part)
-        except (ValueError, Part.DoesNotExist):
-            self.part = None
+            # Filter for parts to install in this item
+            if self.install_item:
+                # Get parts used in this part's BOM
+                bom_items = self.part.get_bom_items()
+                allowed_parts = [item.sub_part for item in bom_items]
+                # Filter
+                items = items.filter(part__in=allowed_parts)
 
         return items
 
+    def get_context_data(self, **kwargs):
+        """ Retrieve parameters and update context """
+
+        ctx = super().get_context_data(**kwargs)
+
+        # Get request parameters
+        self.get_params()
+
+        ctx.update({
+            'part': self.part,
+            'install_in': self.install_in,
+            'install_item': self.install_item,
+        })
+
+        return ctx
+
     def get_initial(self):
 
         initials = super().get_initial()
@@ -558,11 +595,17 @@ class StockItemInstall(AjaxUpdateView):
         if items.count() == 1:
             item = items.first()
             initials['stock_item'] = item.pk
-            initials['quantity_to_install'] = item.quantity
+            # initials['quantity_to_install'] = item.quantity
 
         if self.part:
             initials['part'] = self.part
 
+        try:
+            # Is this stock item being installed in the other stock item?
+            initials['to_install'] = self.install_in or not self.install_item
+        except AttributeError:
+            pass
+
         return initials
 
     def get_form(self):
@@ -575,6 +618,8 @@ class StockItemInstall(AjaxUpdateView):
 
     def post(self, request, *args, **kwargs):
 
+        self.get_params()
+
         form = self.get_form()
 
         valid = form.is_valid()
@@ -584,13 +629,20 @@ class StockItemInstall(AjaxUpdateView):
             data = form.cleaned_data
 
             other_stock_item = data['stock_item']
-            quantity = data['quantity_to_install']
+            # quantity = data['quantity_to_install']
+            # Quantity will always be 1 for serialized item
+            quantity = 1
             notes = data['notes']
 
-            # Install the other stock item into this one
+            # Get stock item
             this_stock_item = self.get_object()
 
-            this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
+            if data['to_install']:
+                # Install this stock item into the other stock item
+                other_stock_item.installStockItem(this_stock_item, quantity, request.user, notes)
+            else:
+                # Install the other stock item into this one
+                this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
 
         data = {
             'form_valid': valid,