diff --git a/InvenTree/build/migrations/0026_auto_20201023_1228.py b/InvenTree/build/migrations/0026_auto_20201023_1228.py new file mode 100644 index 0000000000..cef13c534c --- /dev/null +++ b/InvenTree/build/migrations/0026_auto_20201023_1228.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-23 12:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0052_stockitem_is_building'), + ('build', '0025_auto_20201020_1248'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='builditem', + unique_together={('build', 'stock_item', 'install_into')}, + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index dd62a3e6da..8cf8b4538d 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -551,6 +551,39 @@ class Build(MPTTModel): return parts + def getAvailableStockItems(self, part=None, output=None): + """ + Return available stock items for the build. + """ + + items = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER) + + if part: + # Filter items which match the given Part + items = items.filter(part=part) + + if output: + # Exclude items which are already allocated to the particular build output + + to_exclude = BuildItem.objects.filter( + build=self, + stock_item__part=part, + install_into=output + ) + + items = items.exclude( + id__in=[item.stock_item.id for item in to_exclude.all()] + ) + + # Limit query to stock items which are "downstream" of the source location + if self.take_from is not None: + items = items.filter( + location__in=[loc for loc in self.take_from.getUniqueChildren()] + ) + + return items + + @property def can_build(self): """ Return true if there are enough parts to supply build """ @@ -597,7 +630,7 @@ class BuildItem(models.Model): class Meta: unique_together = [ - ('build', 'stock_item'), + ('build', 'stock_item', 'install_into'), ] def clean(self): @@ -613,24 +646,34 @@ class BuildItem(models.Model): errors = {} try: + # Allocated part must be in the BOM for the master part if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False): errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'".format(p=self.build.part.full_name))] + # Allocated quantity cannot exceed available stock quantity if self.quantity > self.stock_item.quantity: errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format( n=normalize(self.quantity), q=normalize(self.stock_item.quantity) ))] + # Allocated quantity cannot cause the stock item to be over-allocated if self.stock_item.quantity - self.stock_item.allocation_count() + self.quantity < self.quantity: errors['quantity'] = _('StockItem is over-allocated') + # Allocated quantity must be positive if self.quantity <= 0: errors['quantity'] = _('Allocation quantity must be greater than zero') + # Quantity must be 1 for serialized stock if self.stock_item.serial and not self.quantity == 1: errors['quantity'] = _('Quantity must be 1 for serialized stock') + # Part reference must match between output stock item and built part + if self.install_into is not None: + if not self.install_into.part == self.build.part: + errors['install_into'] = _('Part reference differs between build and build output') + except (StockModels.StockItem.DoesNotExist, PartModels.Part.DoesNotExist): pass diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index fdea5534fe..9797e67ca1 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -535,6 +535,10 @@ class BuildItemCreate(AjaxCreateView): form = super(AjaxCreateView, self).get_form() + self.build = None + self.part = None + self.output = None + # If the Build object is specified, hide the input field. # We do not want the users to be able to move a BuildItem to a different build build_id = form['build'].value() @@ -546,14 +550,13 @@ class BuildItemCreate(AjaxCreateView): """ form.fields['build'].widget = HiddenInput() form.fields['install_into'].queryset = StockItem.objects.filter(build=build_id, is_building=True) + self.build = Build.objects.get(pk=build_id) else: """ Build has *not* been selected """ pass - self.output = None - # If the output stock item is specified, hide the input field output_id = form['install_into'].value() @@ -568,47 +571,18 @@ class BuildItemCreate(AjaxCreateView): # If the sub_part is supplied, limit to matching stock items part_id = self.get_param('part') - # We need to precisely control which StockItem objects the user can choose to allocate - stock_filter = form.fields['stock_item'].queryset - - # Restrict to only items which are "in stock" - stock_filter = stock_filter.filter(StockItem.IN_STOCK_FILTER) - if part_id: try: self.part = Part.objects.get(pk=part_id) - # Only allow StockItem objects which match the current part - stock_filter = stock_filter.filter(part=part_id) - - if build_id is not None: - try: - build = Build.objects.get(id=build_id) - - if build.take_from is not None: - # Limit query to stock items that are downstream of the 'take_from' location - stock_filter = stock_filter.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()]) - - except Build.DoesNotExist: - pass - - # Exclude StockItem objects which are already allocated to this build and part - to_exclude = BuildItem.objects.filter(build=build_id, stock_item__part=part_id) - if self.output: - to_exclude = to_exclude.filter(install_into=self.output) - - stock_filter = stock_filter.exclude(id__in=[item.stock_item.id for item in to_exclude.all()]) - - except Part.DoesNotExist: - self.part = None + except (ValueError, Part.DoesNotExist): pass - else: - self.part = None + if self.build and self.part: + available_items = self.build.getAvailableStockItems(part=self.part, output=self.output) + form.fields['stock_item'].queryset = available_items - form.fields['stock_item'].queryset = stock_filter - - self.available_stock = stock_filter.all() + self.available_stock = form.fields['stock_item'].queryset.all() # If there is only a single stockitem available, select it! if len(self.available_stock) == 1: diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f9079e1936..5400388e54 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -139,6 +139,7 @@ class StockItem(MPTTModel): # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock" IN_STOCK_FILTER = Q( + quantity__gt=0, sales_order=None, build_order=None, belongs_to=None,