Repository: inventree/InvenTree

Issue:
Feature: Add installed stock items after build creation
Include an "Add stock item" button to the install stock items tab for a stock item.

Currently installed stock items can only be added whilst in production through a build order. Would be beneficial to be added after production as well.


Discussion:
@SchrodingersGat , 
Wasn't this a feature in the past? https://github.com/inventree/InvenTree/issues/977
Did it removed purposefully and/or can it be added back?  Our workflow requires the ability to uninstall/install stock items in a completed build output.  
@reilly-callaway Your original question is a bit confusing, do you expect a "Add Stock Item" button somewhere on the build order page? Where exactly?

@wolffam Are you saying you would like to link a stock item to a build order, using the "Build Outputs" tab of the build order page?
Ideally (and I am pretty sure this feature existed at one point) we would be able to take an assembly stock item that is the result of a build and "uninstall" a stock item component from that assembly and "install" a replacement stock item component.  Let me know if that can use any further clarification.  I'd prefer not to fall back to an older version of the software if I can avoid it
@wolffam No need to fallback, this was unintentionally skipped during the build order revamp @SchrodingersGat did a few months back, it will be added again.

I'm still not quite sure to grab your feature request, neither the one from the OP...

If you could add screenshots to what you expect that'd be better, or if @SchrodingersGat or @matmair know what are you referring to, then nevermind!

@eeintech , are you asking me for some further clarification? If so, I'd be happy to provide it.
I can also make a new issue if you feel this is separate from the OP's request.
@wolffam I think this issue is fine.
So TLDR: You want the button to add /edit the stock items installed in a stock item? Technically I do not see problems, as far as I remember this was lost during a refactor as @eeintech said.
Do you fell confident enough to implement it yourself? Please be advised we got a new forms API (for example see the sales order forms) which generates builds client-side - implementing the new function this way would be preferred.
That is correct @matmair (in terms of the feature).  We have finished assemblies that we sometimes need to replace a specific part in and therefore need the feature for "hotswapping" stock items in an assembly stock item.

I will not be able to implement it myself without a fair bit of hand holding unfortunately. I had done some poking around in the codebase last year but haven't looked at it in a long while and am a django newbie to top it off 
@wolffam yes this feature did exist, and has been lost! It will be restored soon...

The idea of swapping out particular installed items is definitely a feature I wish to keep supporting.
@SchrodingersGat , can I aid in helping fix this bug in any way?  Our workflow is dead in the water without it so I would be happy to dig into something if it helps.
@wolffam maybe try implementing it and submit a PR; often is the fastest way. If you get stuck just submit what you have as a draft PR so others can help.
@wolffam happy for you to submit a PR - I'm too busy to look at it myself right now. 

Resolving patch:
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -8,7 +8,6 @@
 from django import forms
 from django.forms.utils import ErrorDict
 from django.utils.translation import ugettext_lazy as _
-from django.core.validators import MinValueValidator
 from django.core.exceptions import ValidationError
 
 from mptt.fields import TreeNodeChoiceField
@@ -241,14 +240,9 @@ 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,
     )
 
     notes = forms.CharField(
@@ -261,7 +255,7 @@ class Meta:
         fields = [
             'part',
             'stock_item',
-            'quantity_to_install',
+            # 'quantity_to_install',
             'notes',
         ]
 
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -518,35 +518,72 @@ 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)
+
+            # 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)
 
-        if part_id is None:
-            # Look at POST params
-            part_id = self.request.POST.get('part', None)
+        return items
 
-        try:
-            self.part = Part.objects.get(pk=part_id)
-            items = items.filter(part=self.part)
-        except (ValueError, Part.DoesNotExist):
-            self.part = None
+    def get_context_data(self, **kwargs):
+        """ Retrieve parameters and update context """
 
-        return items
+        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):
 
@@ -558,11 +595,16 @@ def get_initial(self):
         if items.count() == 1:
             item = items.first()
             initials['stock_item'] = item.pk
-            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 +617,8 @@ def get_form(self):
 
     def post(self, request, *args, **kwargs):
 
+        self.get_params()
+
         form = self.get_form()
 
         valid = form.is_valid()
@@ -584,13 +628,19 @@ def post(self, request, *args, **kwargs):
             data = form.cleaned_data
 
             other_stock_item = data['stock_item']
-            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,


Test patch: