From 434f563a41f6dcf78365c96e634d941b0c63e3ab Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 4 Mar 2022 15:26:00 +1100 Subject: [PATCH 1/9] Adds API endpoint for "auto allocating" stock items against a build order. - If stock exists in multiple locations, and the user "does not care" where to take from, simply iterate through and take --- InvenTree/build/api.py | 32 ++++++++ InvenTree/build/models.py | 82 +++++++++++++++++++++ InvenTree/build/serializers.py | 48 ++++++++++++ InvenTree/build/templates/build/detail.html | 19 ++++- InvenTree/part/models.py | 9 ++- InvenTree/templates/js/translated/build.js | 29 ++++++++ 6 files changed, 214 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 310d4d7f09..114268fa2b 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -322,6 +322,37 @@ class BuildFinish(generics.CreateAPIView): return ctx +class BuildAutoAllocate(generics.CreateAPIView): + """ + API endpoint for 'automatically' allocating stock against a build order. + + - Only looks at 'untracked' parts + - If stock exists in a single location, easy! + - If user decides that stock items are "fungible", allocate against multiple stock items + - If the user wants to, allocate substite parts if the primary parts are not available. + """ + + queryset = Build.objects.none() + + serializer_class = build.serializers.BuildAutoAllocationSerializer + + def get_serializer_context(self): + """ + Provide the Build object to the serializer context + """ + + context = super().get_serializer_context() + + try: + context['build'] = Build.objects.get(pk=self.kwargs.get('pk', None)) + except: + pass + + context['request'] = self.request + + return context + + class BuildAllocate(generics.CreateAPIView): """ API endpoint to allocate stock items to a build order @@ -477,6 +508,7 @@ build_api_urls = [ # Build Detail url(r'^(?P\d+)/', include([ url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'), + url(r'^auto-allocate/', BuildAutoAllocate.as_view(), name='api-build-auto-allocate'), url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'), url(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'), url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'), diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 01c2c781e9..e2a7be0c45 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -25,6 +25,8 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey from mptt.exceptions import InvalidMove +from rest_framework import serializers + from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin @@ -823,6 +825,86 @@ class Build(MPTTModel, ReferenceIndexingMixin): self.save() + @transaction.atomic + def auto_allocate_stock(self, user, **kwargs): + """ + Automatically allocate stock items against this build order, + following a number of 'guidelines': + + - Only "untracked" BOM items are considered (tracked BOM items must be manually allocated) + - If a particular BOM item is already fully allocated, it is skipped + - Extract all available stock items for the BOM part + - If variant stock is allowed, extract stock for those too + - If substitute parts are available, extract stock for those also + - If a single stock item is found, we can allocate that and move on! + - If multiple stock items are found, we *may* be able to allocate: + - If the calling function has specified that items are interchangeable + """ + + location = kwargs.get('location', None) + interchangeable = kwargs.get('interchangeable', False) + substitutes = kwargs.get('substitutes', True) + + # Get a list of all 'untracked' BOM items + for bom_item in self.untracked_bom_items: + + unallocated_quantity = self.unallocated_quantity(bom_item) + + if unallocated_quantity <= 0: + # This BomItem is fully allocated, we can continue + continue + + # Check which parts we can "use" (may include variants and substitutes) + available_parts = bom_item.get_valid_parts_for_allocation( + allow_variants=True, + allow_substitutes=substitutes, + ) + + # Look for available stock items + available_stock = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER) + + # Filter by list of available parts + available_stock = available_stock.filter( + part__in=[p for p in available_parts], + ) + + if location: + # Filter only stock items located "below" the specified location + sublocations = location.get_descendants(include_self=True) + available_stock = available_stock.filter(location__in=[loc for loc in sublocations]) + + if available_stock.count() == 0: + # No stock items are available + continue + elif available_stock.count() == 1 or interchangeable: + # Either there is only a single stock item available, + # or all items are "interchangeable" and we don't care where we take stock from + + for stock_item in available_stock: + # How much of the stock item is "available" for allocation? + quantity = min(unallocated_quantity, stock_item.unallocated_quantity()) + + if quantity > 0: + + try: + BuildItem.objects.create( + build=self, + bom_item=bom_item, + stock_item=stock_item, + quantity=quantity, + ) + + # Subtract the required quantity + unallocated_quantity -= quantity + + except (ValidationError, serializers.ValidationError) as exc: + # Catch model errors and re-throw as DRF errors + raise ValidationError(detail=serializers.as_serializer_error(exc)) + + if unallocated_quantity <= 0: + # We have now fully-allocated this BomItem - no need to continue! + break + def required_quantity(self, bom_item, output=None): """ Get the quantity of a part required to complete the particular build output. diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 0a8964ee82..9085a25f30 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -709,6 +709,54 @@ class BuildAllocationSerializer(serializers.Serializer): raise ValidationError(detail=serializers.as_serializer_error(exc)) +class BuildAutoAllocationSerializer(serializers.Serializer): + """ + DRF serializer for auto allocating stock items against a build order + """ + + class Meta: + fields = [ + 'location', + 'interchangeable', + 'substitutes', + ] + + location = serializers.PrimaryKeyRelatedField( + queryset=StockLocation.objects.all(), + many=False, + allow_null=True, + required=False, + label=_('Source Location'), + help_text=_('Stock location where parts are to be sourced (leave blank to take from any location)'), + ) + + interchangeable = serializers.BooleanField( + default=False, + label=_('Interchangeable Stock'), + help_text=_('Stock items in multiple locations can be used interchangeably'), + ) + + substitutes = serializers.BooleanField( + default=True, + label=_('Substitute Stock'), + help_text=_('Allow allocation of substitute parts'), + ) + + def save(self): + + data = self.validated_data + + request = self.context['request'] + build = self.context['build'] + + build.auto_allocate_stock( + request.user, + location=data.get('location', None), + interchangeable=data['interchangeable'], + substitutes=data['substitutes'], + ) + + class BuildItemSerializer(InvenTreeModelSerializer): """ Serializes a BuildItem object """ diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 1e31857ba5..ca909f82f7 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -177,7 +177,10 @@ - +