diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 55337e3fa8..e4b90a9f26 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -133,11 +133,14 @@ function loadBomTable(table, options) { title: 'Part', sortable: true, formatter: function(value, row, index, field) { - var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url); + var url = `/part/${row.sub_part}/`; + var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url); // Display an extra icon if this part is an assembly if (row.sub_part_detail.assembly) { - html += ""; + var text = ``; + + html += renderLink(text, `/part/${row.sub_part}/bom/`); } return html; @@ -185,26 +188,20 @@ function loadBomTable(table, options) { if (!options.editable) { cols.push( { - field: 'sub_part_detail.total_stock', + field: 'sub_part_detail.stock', title: 'Available', searchable: false, sortable: true, formatter: function(value, row, index, field) { - var text = ""; - - if (row.quantity < row.sub_part_detail.total_stock) - { - text = "" + value + ""; + + var url = `/part/${row.sub_part_detail.pk}/stock/`; + var text = value; + + if (value == null || value <= 0) { + text = `No Stock`; } - else - { - if (!value) { - value = 'No Stock'; - } - text = "" + value + ""; - } - - return renderLink(text, row.sub_part_detail.url + "stock/"); + + return renderLink(text, url); } }); diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 3ac225fd6e..e76123c6d8 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -27,12 +27,17 @@ def inventreeDjangoVersion(): def inventreeCommitHash(): """ Returns the git commit hash for the running codebase """ - return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip() + try: + return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip() + except FileNotFoundError: + return None def inventreeCommitDate(): """ Returns the git commit date for the running codebase """ - d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip() - - return d.split(' ')[0] + try: + d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip() + return d.split(' ')[0] + except FileNotFoundError: + return None diff --git a/InvenTree/company/migrations/0019_auto_20200413_0642.py b/InvenTree/company/migrations/0019_auto_20200413_0642.py index f683ee783a..c81dfd795a 100644 --- a/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -95,7 +95,7 @@ def associate_manufacturers(apps, schema_editor): cursor = connection.cursor() response = cursor.execute(query) - row = response.fetchone() + row = cursor.fetchone() if len(row) > 0: return row[0] diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 51a4a35938..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 @@ -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: @@ -198,16 +197,9 @@ 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() - 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 +209,71 @@ 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(cat_id) + + # 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[cat_id] + else: + detail = None + + part['category_detail'] = detail + + """ + 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: We wish to save the user who created this part! @@ -239,15 +296,16 @@ 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 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? @@ -468,15 +526,15 @@ class BomList(generics.ListCreateAPIView): # Do we wish to include extra detail? try: - part_detail = str2bool(self.request.GET.get('part_detail', None)) - sub_part_detail = str2bool(self.request.GET.get('sub_part_detail', None)) + kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', None)) except AttributeError: - part_detail = None - sub_part_detail = None - - kwargs['part_detail'] = part_detail - kwargs['sub_part_detail'] = sub_part_detail + pass + try: + kwargs['sub_part_detail'] = str2bool(self.request.GET.get('sub_part_detail', None)) + except AttributeError: + pass + # Ensure the request context is passed through! kwargs['context'] = self.get_serializer_context() @@ -486,6 +544,12 @@ class BomList(generics.ListCreateAPIView): queryset = BomItem.objects.all() queryset = self.get_serializer_class().setup_eager_loading(queryset) + return queryset + + def filter_queryset(self, queryset): + + queryset = super().filter_queryset(queryset) + # Filter by part? part = self.request.query_params.get('part', None) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 021695f9bf..6f6b0d7ddf 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1242,6 +1242,17 @@ class BomItem(models.Model): child=self.sub_part.full_name, n=helpers.decimal2string(self.quantity)) + def available_stock(self): + """ + Return the available stock items for the referenced sub_part + """ + + query = self.sub_part.stock_items.filter(StockModels.StockItem.IN_STOCK_FILTER).aggregate( + available=Coalesce(Sum('quantity'), 0) + ) + + return query['available'] + def get_overage_quantity(self, quantity): """ Calculate overage quantity """ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 8cb3584664..920e0486c3 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -54,6 +54,8 @@ class PartBriefSerializer(InvenTreeModelSerializer): thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) + stock = serializers.FloatField(source='total_stock') + class Meta: model = Part fields = [ @@ -65,6 +67,7 @@ class PartBriefSerializer(InvenTreeModelSerializer): 'assembly', 'purchaseable', 'salable', + 'stock', 'virtual', ] @@ -98,6 +101,8 @@ class PartSerializer(InvenTreeModelSerializer): return queryset.prefetch_related( 'category', + 'category__parts', + 'category__parent', 'stock_items', 'bom_items', 'builds', @@ -125,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, @@ -139,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) ) diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index 1d3589a0d3..ce0491a165 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -1,10 +1,10 @@ {% extends "part/part_base.html" %} - +{% load i18n %} {% block details %} {% include 'part/tabs.html' with tab='used' %} -

Assemblies

+

{% trans "Assemblies" %}


@@ -35,10 +35,11 @@ title: 'Part', sortable: true, formatter: function(value, row, index, field) { - var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value.full_name, value.url + 'bom/'); + var link = `/part/${value.pk}/bom/`; + var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value.full_name, link); if (!row.part_detail.active) { - html += "INACTIVE"; + html += "{% trans "INACTIVE" %}"; } return html; diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index c31c1b8993..6b7c15f980 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -7,15 +7,20 @@ 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 +from .serializers import LocationSerializer, LocationBriefSerializer from .serializers import StockTrackingSerializer from InvenTree.views import TreeSerializer @@ -322,45 +327,153 @@ 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['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: - 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 + 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 + + # Keep track of which related models we need to query + location_ids = set() + part_ids = set() + supplier_part_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: + part_id = stock_item['supplier_part'] + stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None) + + # 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( + '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'] + stock_item['location_detail'] = location_map.get(loc_id, 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. + + Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft + """ + + 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) @@ -369,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) @@ -382,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) @@ -403,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"}) @@ -418,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"}) @@ -432,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 @@ -451,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"}) @@ -460,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, @@ -510,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/models.py b/InvenTree/stock/models.py index 36e8064138..f514eea3da 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -137,7 +137,6 @@ class StockItem(MPTTModel): sales_order=None, build_order=None, belongs_to=None, - status__in=StockStatus.AVAILABLE_CODES ) def save(self, *args, **kwargs): diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 4e586b789e..e04e2a149b 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 @@ -17,15 +20,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', ] @@ -65,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', @@ -82,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) @@ -91,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): @@ -143,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', 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 }}