From 4a60da67fd92f0027ad5604a98582064a3985661 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 2 May 2020 09:49:05 +1000 Subject: [PATCH 1/7] Significant increase in query speed for Part list - Custom list method - Cache PartCategory objects in memory --- InvenTree/part/api.py | 68 +++++++++++++++++++++++++++++++---- InvenTree/part/serializers.py | 2 ++ 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 310cf3b94f..9eb184b1c2 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -110,7 +110,7 @@ class PartThumbs(generics.ListAPIView): serializer_class = part_serializers.PartThumbSerializer - def list(self, reguest, *args, **kwargs): + def list(self, request, *args, **kwargs): """ Serialize the available Part images. - Images may be used for multiple parts! @@ -142,6 +142,7 @@ class PartDetail(generics.RetrieveUpdateAPIView): queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) queryset = part_serializers.PartSerializer.annotate_queryset(queryset) + return queryset permission_classes = [ @@ -151,15 +152,13 @@ class PartDetail(generics.RetrieveUpdateAPIView): def get_serializer(self, *args, **kwargs): try: - cat_detail = str2bool(self.request.query_params.get('category_detail', False)) + kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', False)) except AttributeError: - cat_detail = None + pass # Ensure the request context is passed through kwargs['context'] = self.get_serializer_context() - kwargs['category_detail'] = cat_detail - # Pass a list of "starred" parts fo the current user to the serializer # We do this to reduce the number of database queries required! if self.starred_parts is None and self.request is not None: @@ -206,8 +205,6 @@ class PartList(generics.ListCreateAPIView): # Ensure the request context is passed through kwargs['context'] = self.get_serializer_context() - kwargs['category_detail'] = cat_detail - # Pass a list of "starred" parts fo the current user to the serializer # We do this to reduce the number of database queries required! if self.starred_parts is None and self.request is not None: @@ -217,6 +214,63 @@ class PartList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) + def list(self, request, *args, **kwargs): + """ + Overide the 'list' method, as the PartCategory objects are + very expensive to serialize! + + So we will serialize them first, and keep them in memory, + so that they do not have to be serialized multiple times... + """ + + queryset = self.filter_queryset(self.get_queryset()) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + + data = serializer.data + + # Do we wish to include PartCategory detail? + if str2bool(request.query_params.get('category_detail', False)): + + # Work out which part categorie we need to query + category_ids = set() + + for part in data: + cat_id = part['category'] + + if cat_id is not None: + category_ids.add(part['category']) + + # Fetch only the required PartCategory objects from the database + categories = PartCategory.objects.filter(pk__in=category_ids).prefetch_related( + 'parts', + 'parent', + 'children', + ) + + category_map = {} + + # Serialize each PartCategory object + for category in categories: + category_map[category.pk] = part_serializers.CategorySerializer(category).data + + for part in data: + cat_id = part['category'] + + if cat_id is not None and cat_id in category_map.keys(): + detail = category_map[part['category']] + else: + detail = None + + part['category_detail'] = detail + + return Response(data) + def create(self, request, *args, **kwargs): """ Override the default 'create' behaviour: We wish to save the user who created this part! diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index e713de455f..86a3189c43 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -101,6 +101,8 @@ class PartSerializer(InvenTreeModelSerializer): return queryset.prefetch_related( 'category', + 'category__parts', + 'category__parent', 'stock_items', 'bom_items', 'builds', From a537b6df6e35d5b38ff5114aabf8af6e16c1865a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 2 May 2020 09:50:18 +1000 Subject: [PATCH 2/7] PEP fixes --- InvenTree/part/api.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 9eb184b1c2..bcbd5bf41a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -197,11 +197,6 @@ class PartList(generics.ListCreateAPIView): def get_serializer(self, *args, **kwargs): - try: - cat_detail = str2bool(self.request.query_params.get('category_detail', False)) - except AttributeError: - cat_detail = None - # Ensure the request context is passed through kwargs['context'] = self.get_serializer_context() From 44319d24e4e0b22490e2ae24e214f5173d5fab0a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 2 May 2020 10:05:35 +1000 Subject: [PATCH 3/7] Custom list serializer for 'location_detail' --- InvenTree/part/api.py | 4 +-- InvenTree/stock/api.py | 63 ++++++++++++++++++++++++++++++---- InvenTree/stock/serializers.py | 3 -- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index bcbd5bf41a..f776c208b5 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -239,7 +239,7 @@ class PartList(generics.ListCreateAPIView): cat_id = part['category'] if cat_id is not None: - category_ids.add(part['category']) + category_ids.add(cat_id) # Fetch only the required PartCategory objects from the database categories = PartCategory.objects.filter(pk__in=category_ids).prefetch_related( @@ -258,7 +258,7 @@ class PartList(generics.ListCreateAPIView): cat_id = part['category'] if cat_id is not None and cat_id in category_map.keys(): - detail = category_map[part['category']] + detail = category_map[cat_id] else: detail = None diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index c31c1b8993..d1f6c2d944 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -15,7 +15,7 @@ from .models import StockItemTracking from part.models import Part, PartCategory from .serializers import StockItemSerializer -from .serializers import LocationSerializer +from .serializers import LocationSerializer, LocationBriefSerializer from .serializers import StockTrackingSerializer from InvenTree.views import TreeSerializer @@ -332,11 +332,6 @@ class StockList(generics.ListCreateAPIView): except AttributeError: pass - try: - kwargs['location_detail'] = str2bool(self.request.query_params.get('location_detail', None)) - except AttributeError: - pass - try: kwargs['supplier_part_detail'] = str2bool(self.request.query_params.get('supplier_part_detail', None)) except AttributeError: @@ -350,6 +345,62 @@ class StockList(generics.ListCreateAPIView): # TODO - Override the 'create' method for this view, # to allow the user to be recorded when a new StockItem object is created + def list(self, request, *args, **kwargs): + """ + Override the 'list' method, as the StockLocation objects + are very expensive to serialize. + + So, we fetch and serialize the required StockLocation objects only as required. + """ + + queryset = self.filter_queryset(self.get_queryset()) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + + data = serializer.data + + # Do we wish to include StockLocation detail? + if str2bool(request.query_params.get('location_detail', False)): + + # Work out which locations we need to query + location_ids = set() + + for stock_item in data: + loc_id = stock_item['location'] + + if loc_id is not None: + location_ids.add(loc_id) + + # Fetch only the required StockLocation objects from the database + locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related( + 'parent', + 'children', + ) + + location_map = {} + + # Serialize each StockLocation object + for location in locations: + location_map[location.pk] = LocationBriefSerializer(location).data + + # Now update each StockItem with the related StockLocation data + for stock_item in data: + loc_id = stock_item['location'] + + if loc_id is not None and loc_id in location_map.keys(): + detail = location_map[loc_id] + else: + detail = None + + stock_item['location_detail'] = detail + + return Response(data) + def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 4e586b789e..eeab49eb4b 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -17,15 +17,12 @@ class LocationBriefSerializer(InvenTreeModelSerializer): Provides a brief serializer for a StockLocation object """ - url = serializers.CharField(source='get_absolute_url', read_only=True) - class Meta: model = StockLocation fields = [ 'pk', 'name', 'pathstring', - 'url', ] From 4197e29fce1c28fe43a6020f129ca56045c79ff0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 2 May 2020 13:46:19 +1000 Subject: [PATCH 4/7] Major major major (improvements for StockItem list API) OK LISTEN UP - Lots of work went into making this speedier: - For related detail fields (e.g. part_detail), we pre-fetch and cache the model data - This eliminates duplicate database hits for the same model instances - Perform all field filtering manually, rather than using the DRF 'filter_fields' concept (this seems to add a lot of overhead) - Use query annotations to getch calculated fields rather than fetching one-at-a-time - And finally, if the request is AJAX then return a JsonResponse which is SO FREAKING MUCH FASTER --- InvenTree/part/api.py | 4 +- InvenTree/stock/api.py | 176 +++++++++++++++++++++------------ InvenTree/stock/serializers.py | 20 +++- 3 files changed, 134 insertions(+), 66 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index f776c208b5..0d61025a8e 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -293,10 +293,10 @@ class PartList(generics.ListCreateAPIView): def filter_queryset(self, queryset): """ - Perform custom filtering of the queryset + Perform custom filtering of the queryset. + We overide the DRF filter_fields here because """ - # Perform basic filtering queryset = super().filter_queryset(queryset) # Filter by 'starred' parts? diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index d1f6c2d944..4816966718 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -7,12 +7,17 @@ from django_filters import NumberFilter from django.conf.urls import url, include from django.urls import reverse +from django.http import JsonResponse from django.db.models import Q from .models import StockLocation, StockItem from .models import StockItemTracking from part.models import Part, PartCategory +from part.serializers import PartBriefSerializer + +from company.models import SupplierPart +from company.serializers import SupplierPartSerializer from .serializers import StockItemSerializer from .serializers import LocationSerializer, LocationBriefSerializer @@ -322,26 +327,8 @@ class StockList(generics.ListCreateAPIView): """ serializer_class = StockItemSerializer - queryset = StockItem.objects.all() - def get_serializer(self, *args, **kwargs): - - try: - kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None)) - except AttributeError: - pass - - try: - kwargs['supplier_part_detail'] = str2bool(self.request.query_params.get('supplier_part_detail', None)) - except AttributeError: - pass - - # Ensure the request context is passed through - kwargs['context'] = self.get_serializer_context() - - return self.serializer_class(*args, **kwargs) - # TODO - Override the 'create' method for this view, # to allow the user to be recorded when a new StockItem object is created @@ -364,17 +351,59 @@ class StockList(generics.ListCreateAPIView): data = serializer.data - # Do we wish to include StockLocation detail? - if str2bool(request.query_params.get('location_detail', False)): + # Keep track of which related models we need to query + location_ids = set() + part_ids = set() + supplier_part_ids = set() - # Work out which locations we need to query - location_ids = set() + # Iterate through each StockItem and grab some data + for item in data: + loc = item['location'] + if loc: + location_ids.add(loc) + + part = item['part'] + if part: + part_ids.add(part) + + sp = item['supplier_part'] + if sp: + supplier_part_ids.add(sp) + + # Do we wish to include Part detail? + if str2bool(request.query_params.get('part_detail', False)): + + # Fetch only the required Part objects from the database + parts = Part.objects.filter(pk__in=part_ids).prefetch_related( + 'category', + ) + + part_map = {} + + for part in parts: + part_map[part.pk] = PartBriefSerializer(part).data + + # Now update each StockItem with the related Part data + for stock_item in data: + part_id = stock_item['part'] + stock_item['part_detail'] = part_map.get(part_id, None) + + # Do we wish to include SupplierPart detail? + if str2bool(request.query_params.get('supplier_part_detail', False)): + + supplier_parts = SupplierPart.objects.filter(pk__in=supplier_part_ids) + + supplier_part_map = {} + + for part in supplier_parts: + supplier_part_map[part.pk] = SupplierPartSerializer(part).data for stock_item in data: - loc_id = stock_item['location'] + part_id = stock_item['supplier_part'] + stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None) - if loc_id is not None: - location_ids.add(loc_id) + # Do we wish to include StockLocation detail? + if str2bool(request.query_params.get('location_detail', False)): # Fetch only the required StockLocation objects from the database locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related( @@ -391,27 +420,60 @@ class StockList(generics.ListCreateAPIView): # Now update each StockItem with the related StockLocation data for stock_item in data: loc_id = stock_item['location'] + stock_item['supplier_detail'] = location_map.get(loc_id, None) - if loc_id is not None and loc_id in location_map.keys(): - detail = location_map[loc_id] - else: - detail = None + """ + Determine the response type based on the request. + a) For HTTP requests (e.g. via the browseable API) return a DRF response + b) For AJAX requests, simply return a JSON rendered response. - stock_item['location_detail'] = detail + Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft + """ - return Response(data) + if request.is_ajax(): + return JsonResponse(data, safe=False) + else: + return Response(data) def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) queryset = StockItemSerializer.prefetch_queryset(queryset) + queryset = StockItemSerializer.annotate_queryset(queryset) return queryset def filter_queryset(self, queryset): - # Start with all objects - stock_list = super().filter_queryset(queryset) + params = self.request.query_params + + # Perform basic filtering: + # Note: We do not let DRF filter here, it be slow AF + + supplier_part = params.get('supplier_part', None) + + if supplier_part: + queryset = queryset.filter(supplier_part=supplier_part) + + belongs_to = params.get('belongs_to', None) + + if belongs_to: + queryset = queryset.filter(belongs_to=belongs_to) + + build = params.get('build', None) + + if build: + queryset = queryset.filter(build=build) + + build_order = params.get('build_order', None) + + if build_order: + queryset = queryset.filter(build_order=build_order) + + sales_order = params.get('sales_order', None) + + if sales_order: + queryset = queryset.filter(sales_order=sales_order) in_stock = self.request.query_params.get('in_stock', None) @@ -420,10 +482,10 @@ class StockList(generics.ListCreateAPIView): if in_stock: # Filter out parts which are not actually "in stock" - stock_list = stock_list.filter(StockItem.IN_STOCK_FILTER) + queryset = queryset.filter(StockItem.IN_STOCK_FILTER) else: # Only show parts which are not in stock - stock_list = stock_list.exclude(StockItem.IN_STOCK_FILTER) + queryset = queryset.exclude(StockItem.IN_STOCK_FILTER) # Filter by 'allocated' patrs? allocated = self.request.query_params.get('allocated', None) @@ -433,17 +495,17 @@ class StockList(generics.ListCreateAPIView): if allocated: # Filter StockItem with either build allocations or sales order allocations - stock_list = stock_list.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)) + queryset = queryset.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)) else: # Filter StockItem without build allocations or sales order allocations - stock_list = stock_list.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) + queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) # Do we wish to filter by "active parts" active = self.request.query_params.get('active', None) if active is not None: active = str2bool(active) - stock_list = stock_list.filter(part__active=active) + queryset = queryset.filter(part__active=active) # Does the client wish to filter by the Part ID? part_id = self.request.query_params.get('part', None) @@ -454,9 +516,9 @@ class StockList(generics.ListCreateAPIView): # If the part is a Template part, select stock items for any "variant" parts under that template if part.is_template: - stock_list = stock_list.filter(part__in=[part.id for part in Part.objects.filter(variant_of=part_id)]) + queryset = queryset.filter(part__in=[part.id for part in Part.objects.filter(variant_of=part_id)]) else: - stock_list = stock_list.filter(part=part_id) + queryset = queryset.filter(part=part_id) except (ValueError, Part.DoesNotExist): raise ValidationError({"part": "Invalid Part ID specified"}) @@ -469,7 +531,7 @@ class StockList(generics.ListCreateAPIView): ancestor = StockItem.objects.get(pk=anc_id) # Only allow items which are descendants of the specified StockItem - stock_list = stock_list.filter(id__in=[item.pk for item in ancestor.children.all()]) + queryset = queryset.filter(id__in=[item.pk for item in ancestor.children.all()]) except (ValueError, Part.DoesNotExist): raise ValidationError({"ancestor": "Invalid ancestor ID specified"}) @@ -483,15 +545,15 @@ class StockList(generics.ListCreateAPIView): # Filter by 'null' location (i.e. top-level items) if isNull(loc_id): - stock_list = stock_list.filter(location=None) + queryset = queryset.filter(location=None) else: try: # If '?cascade=true' then include items which exist in sub-locations if cascade: location = StockLocation.objects.get(pk=loc_id) - stock_list = stock_list.filter(location__in=location.getUniqueChildren()) + queryset = queryset.filter(location__in=location.getUniqueChildren()) else: - stock_list = stock_list.filter(location=loc_id) + queryset = queryset.filter(location=loc_id) except (ValueError, StockLocation.DoesNotExist): pass @@ -502,7 +564,7 @@ class StockList(generics.ListCreateAPIView): if cat_id: try: category = PartCategory.objects.get(pk=cat_id) - stock_list = stock_list.filter(part__category__in=category.getUniqueChildren()) + queryset = queryset.filter(part__category__in=category.getUniqueChildren()) except (ValueError, PartCategory.DoesNotExist): raise ValidationError({"category": "Invalid category id specified"}) @@ -511,44 +573,42 @@ class StockList(generics.ListCreateAPIView): status = self.request.query_params.get('status', None) if status: - stock_list = stock_list.filter(status=status) + queryset = queryset.filter(status=status) # Filter by supplier_part ID supplier_part_id = self.request.query_params.get('supplier_part', None) if supplier_part_id: - stock_list = stock_list.filter(supplier_part=supplier_part_id) + queryset = queryset.filter(supplier_part=supplier_part_id) # Filter by company (either manufacturer or supplier) company = self.request.query_params.get('company', None) if company is not None: - stock_list = stock_list.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer=company)) + queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer=company)) # Filter by supplier supplier = self.request.query_params.get('supplier', None) if supplier is not None: - stock_list = stock_list.filter(supplier_part__supplier=supplier) + queryset = queryset.filter(supplier_part__supplier=supplier) # Filter by manufacturer manufacturer = self.request.query_params.get('manufacturer', None) if manufacturer is not None: - stock_list = stock_list.filter(supplier_part__manufacturer=manufacturer) + queryset = queryset.filter(supplier_part__manufacturer=manufacturer) # Also ensure that we pre-fecth all the related items - stock_list = stock_list.prefetch_related( + queryset = queryset.prefetch_related( 'part', 'part__category', 'location' ) - stock_list = stock_list.order_by('part__name') + queryset = queryset.order_by('part__name') - return stock_list - - serializer_class = StockItemSerializer + return queryset permission_classes = [ permissions.IsAuthenticated, @@ -561,12 +621,6 @@ class StockList(generics.ListCreateAPIView): ] filter_fields = [ - 'supplier_part', - 'belongs_to', - 'build', - 'build_order', - 'sales_order', - 'build_order', ] diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index eeab49eb4b..6b34fbe6ce 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -7,6 +7,9 @@ from rest_framework import serializers from .models import StockItem, StockLocation from .models import StockItemTracking +from django.db.models import Sum, Count +from django.db.models.functions import Coalesce + from company.serializers import SupplierPartSerializer from part.serializers import PartBriefSerializer from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer @@ -62,6 +65,10 @@ class StockItemSerializer(InvenTreeModelSerializer): """ return queryset.prefetch_related( + 'belongs_to', + 'build', + 'build_order', + 'sales_order', 'supplier_part', 'supplier_part__supplier', 'supplier_part__manufacturer', @@ -79,7 +86,13 @@ class StockItemSerializer(InvenTreeModelSerializer): performing database queries as efficiently as possible. """ - # TODO - Add custom annotated fields + queryset = queryset.annotate( + allocated = Coalesce( + Sum('sales_order_allocations__quantity', distinct=True), 0) + Coalesce( + Sum('allocations__quantity', distinct=True), 0), + tracking_items = Count('tracking_info'), + ) + return queryset status_text = serializers.CharField(source='get_status_display', read_only=True) @@ -88,10 +101,10 @@ class StockItemSerializer(InvenTreeModelSerializer): location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True) - tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) + tracking_items = serializers.IntegerField() quantity = serializers.FloatField() - allocated = serializers.FloatField(source='allocation_count', read_only=True) + allocated = serializers.FloatField() def __init__(self, *args, **kwargs): @@ -140,6 +153,7 @@ class StockItemSerializer(InvenTreeModelSerializer): They can be updated by accessing the appropriate API endpoints """ read_only_fields = [ + 'allocated', 'stocktake_date', 'stocktake_user', 'updated', From 7fca496de8aaf55fd47e4df94c76bd8dad7068c6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 2 May 2020 13:51:29 +1000 Subject: [PATCH 5/7] Bug fix for StockItem list API - The wrong detail data was being set --- InvenTree/part/api.py | 2 +- InvenTree/stock/api.py | 2 +- InvenTree/stock/serializers.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 0d61025a8e..335853331c 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -294,7 +294,7 @@ class PartList(generics.ListCreateAPIView): def filter_queryset(self, queryset): """ Perform custom filtering of the queryset. - We overide the DRF filter_fields here because + We overide the DRF filter_fields here because """ queryset = super().filter_queryset(queryset) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 4816966718..6b7c15f980 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -420,7 +420,7 @@ class StockList(generics.ListCreateAPIView): # Now update each StockItem with the related StockLocation data for stock_item in data: loc_id = stock_item['location'] - stock_item['supplier_detail'] = location_map.get(loc_id, None) + stock_item['location_detail'] = location_map.get(loc_id, None) """ Determine the response type based on the request. diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 6b34fbe6ce..e04e2a149b 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -87,10 +87,10 @@ class StockItemSerializer(InvenTreeModelSerializer): """ queryset = queryset.annotate( - allocated = Coalesce( + allocated=Coalesce( Sum('sales_order_allocations__quantity', distinct=True), 0) + Coalesce( Sum('allocations__quantity', distinct=True), 0), - tracking_items = Count('tracking_info'), + tracking_items=Count('tracking_info'), ) return queryset From cc11df917e321d8ac421c08229ee83be084194e8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 2 May 2020 14:03:17 +1000 Subject: [PATCH 6/7] Part list API adjustments --- InvenTree/part/api.py | 13 +++++++++++-- InvenTree/part/serializers.py | 13 ++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 335853331c..6f5327baa4 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -6,7 +6,7 @@ Provides a JSON API for the Part app from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend - +from django.http import JsonResponse from django.db.models import Q, F, Count from rest_framework import status @@ -264,7 +264,15 @@ class PartList(generics.ListCreateAPIView): part['category_detail'] = detail - return Response(data) + """ + Determine the response type based on the request. + a) For HTTP requests (e.g. via the browseable API) return a DRF response + b) For AJAX requests, simply return a JSON rendered response. + """ + if request.is_ajax(): + return JsonResponse(data, safe=False) + else: + return Response(data) def create(self, request, *args, **kwargs): """ Override the default 'create' behaviour: @@ -288,6 +296,7 @@ class PartList(generics.ListCreateAPIView): queryset = super().get_queryset(*args, **kwargs) queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) + queryset = part_serializers.PartSerializer.annotate_queryset(queryset) return queryset diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 86a3189c43..920e0486c3 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -130,12 +130,7 @@ class PartSerializer(InvenTreeModelSerializer): # Annotate the number total stock count queryset = queryset.annotate( - in_stock=Coalesce(Sum('stock_items__quantity', filter=stock_filter, distinct=True), Decimal(0)) - ) - - # Annotate the number of parts "on order" - # Total "on order" parts = "Quantity" - "Received" for each active purchase order - queryset = queryset.annotate( + in_stock=Coalesce(Sum('stock_items__quantity', filter=stock_filter, distinct=True), Decimal(0)), ordering=Coalesce(Sum( 'supplier_parts__purchase_order_line_items__quantity', filter=order_filter, @@ -144,11 +139,7 @@ class PartSerializer(InvenTreeModelSerializer): 'supplier_parts__purchase_order_line_items__received', filter=order_filter, distinct=True - ), Decimal(0)) - ) - - # Annotate number of parts being build - queryset = queryset.annotate( + ), Decimal(0)), building=Coalesce( Sum('builds__quantity', filter=build_filter, distinct=True), Decimal(0) ) From 9b2045025b0b6250743fe520991ef30c301152b0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 2 May 2020 14:05:52 +1000 Subject: [PATCH 7/7] StockItem page tweaks --- InvenTree/stock/templates/stock/item_base.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index bfbc737181..4e2d6b7439 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -105,9 +105,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} - Part + {% trans "Base Part" %} - {% include "hover_image.html" with image=item.part.image hover=True %} {{ item.part.full_name }} @@ -145,7 +144,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} {% if item.serialized %} - + {% trans "Serial Number" %} {{ item.serial }}