From 85d1c585c088a2a636317fda6ecaf52d88551df9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 19 Apr 2020 22:44:16 +1000 Subject: [PATCH 01/15] Update to django3! - This is required to fix some issues with query aggregations as "distinct=True" cannot be set for Sum operations until django 3.0 - Multiple annotations on the same queryset were returning invalid results - Ref: https://code.djangoproject.com/ticket/10060 - Ref: https://docs.djangoproject.com/en/3.0/topics/db/aggregation/#combining-multiple-aggregations - django-dbbackup required updating for django3 support - django-qr-code for now is disabled, a new solution needs to be worked out --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/InvenTree/urls.py | 4 ++-- requirements.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index fa6bf96ec7..4f1e172e5a 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -101,7 +101,7 @@ INSTALLED_APPS = [ 'crispy_forms', # Improved form rendering 'import_export', # Import / export tables to file 'django_cleanup', # Automatically delete orphaned MEDIA files - 'qr_code', # Generate QR codes + #'qr_code', # Generate QR codes 'mptt', # Modified Preorder Tree Traversal 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d9600333f4..76ab422e7d 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -8,7 +8,7 @@ Passes URL lookup downstream to each app as required. from django.conf.urls import url, include from django.contrib import admin from django.contrib.auth import views as auth_views -from qr_code import urls as qr_code_urls +#from qr_code import urls as qr_code_urls from company.urls import company_urls from company.urls import supplier_part_urls @@ -99,7 +99,7 @@ urlpatterns = [ url(r'^admin/', admin.site.urls, name='inventree-admin'), - url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), + #url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), url(r'^index/', IndexView.as_view(), name='index'), url(r'^search/', SearchView.as_view(), name='search'), diff --git a/requirements.txt b/requirements.txt index 4eace551ea..a6c780b6ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ wheel>=0.34.2 # Wheel -Django==2.2.10 # Django package +Django==3.0.5 # Django package pillow==6.2.0 # Image manipulation djangorestframework==3.10.3 # DRF framework +django-dbbackup==3.3.0 # Database backup / restore functionality django-cors-headers==3.2.0 # CORS headers extension for DRF django_filter==2.2.0 # Extended filtering options django-mptt==0.10.0 # Modified Preorder Tree Traversal -django-dbbackup==3.2.0 # Database backup / restore functionality django-markdownx==3.0.1 # Markdown form fields django-markdownify==0.8.0 # Markdown rendering coreapi==2.3.0 # API documentation From 69b8eed028a6a43767fcaf43546820f6c6ece114 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 19 Apr 2020 22:54:46 +1000 Subject: [PATCH 02/15] Fixes for aggregation issues - Ensure that "distinct=True" is set! - ARRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH --- InvenTree/part/api.py | 153 ++++++++++++++++++++-------------- InvenTree/part/serializers.py | 104 ++++++++++++++++++----- 2 files changed, 175 insertions(+), 82 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index e36d4a568b..6699dff64f 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -166,6 +166,8 @@ class PartList(generics.ListCreateAPIView): serializer_class = part_serializers.PartSerializer + queryset = Part.objects.all() + def create(self, request, *args, **kwargs): """ Override the default 'create' behaviour: We wish to save the user who created this part! @@ -184,7 +186,89 @@ class PartList(generics.ListCreateAPIView): headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - def list(self, request, *args, **kwargs): + + def filter_queryset(self, queryset): + """ + Perform custom filtering of the queryset + """ + + # Perform basic filtering + queryset = super().filter_queryset(queryset) + + # Filter by 'starred' parts? + starred = str2bool(self.request.query_params.get('starred', None)) + + if starred is not None: + starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()] + + if starred: + queryset = queryset.filter(pk__in=starred_parts) + else: + queryset = queryset.exclude(pk__in=starred_parts) + + # Cascade? + cascade = str2bool(self.request.query_params.get('cascade', None)) + + # Does the user wish to filter by category? + cat_id = self.request.query_params.get('category', None) + + if cat_id is None: + # No category filtering if category is not specified + pass + + else: + # Category has been specified! + if isNull(cat_id): + # A 'null' category is the top-level category + if cascade is False: + # Do not cascade, only list parts in the top-level category + queryset = queryset.filter(category=None) + + else: + try: + category = PartCategory.objects.get(pk=cat_id) + + # If '?cascade=true' then include parts which exist in sub-categories + if cascade: + queryset = queryset.filter(category__in=category.getUniqueChildren()) + # Just return parts directly in the requested category + else: + queryset = queryset.filter(category=cat_id) + except (ValueError, PartCategory.DoesNotExist): + pass + + # Annotate calculated data to the queryset + # (This will be used for further filtering) + queryset = part_serializers.PartSerializer.annotate_queryset(queryset) + + # Filter by whether the part has stock + has_stock = self.request.query_params.get("has_stock", None) + if has_stock is not None: + has_stock = str2bool(has_stock) + + if has_stock: + queryset = queryset.filter(Q(in_stock__gt=0)) + else: + queryset = queryset.filter(Q(in_stock_lte=0)) + + # If we are filtering by 'low_stock' status + low_stock = self.request.query_params.get('low_stock', None) + + if low_stock is not None: + low_stock = str2bool(low_stock) + + if low_stock: + # Ignore any parts which do not have a specified 'minimum_stock' level + queryset = queryset.exclude(minimum_stock=0) + # Filter items which have an 'in_stock' level lower than 'minimum_stock' + queryset = queryset.filter(Q(in_stock__lt=F('minimum_stock'))) + else: + # Filter items which have an 'in_stock' level higher than 'minimum_stock' + queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock'))) + + return queryset + + def dolist(self, request, *args, **kwargs): """ Instead of using the DRF serialiser to LIST, we serialize the objects manually. @@ -193,10 +277,11 @@ class PartList(generics.ListCreateAPIView): queryset = self.filter_queryset(self.get_queryset()) - # Filters for annotations + queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) - # "in_stock" count should only sum stock items which are "in stock" - stock_filter = Q(stock_items__status__in=StockStatus.AVAILABLE_CODES) + queryset = part_serializers.PartSerializer.annotate_queryset(queryset) + + # Filters for annotations # "on_order" items should only sum orders which are currently outstanding order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN) @@ -225,9 +310,9 @@ class PartList(generics.ListCreateAPIView): 'active', ).annotate( # Quantity of items which are "in stock" - in_stock=Coalesce(Sum('stock_items__quantity', filter=stock_filter), Decimal(0)), - on_order=Coalesce(Sum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter), Decimal(0)), - building=Coalesce(Sum('builds__quantity', filter=build_filter), Decimal(0)), + in_stock=Coalesce(Sum('stock_items__quantity'), Decimal(0)) #, filter=stock_filter), Decimal(0)), + #on_order=Coalesce(Sum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter), Decimal(0)), + #building=Coalesce(Sum('builds__quantity', filter=build_filter), Decimal(0)), ) # If we are filtering by 'has_stock' status @@ -300,60 +385,6 @@ class PartList(generics.ListCreateAPIView): return Response(data) - def get_queryset(self): - """ - Implement custom filtering for the Part list API - """ - - # Start with all objects - parts_list = Part.objects.all() - - # Filter by 'starred' parts? - starred = str2bool(self.request.query_params.get('starred', None)) - - if starred is not None: - starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()] - - if starred: - parts_list = parts_list.filter(pk__in=starred_parts) - else: - parts_list = parts_list.exclude(pk__in=starred_parts) - - cascade = str2bool(self.request.query_params.get('cascade', None)) - - # Does the user wish to filter by category? - cat_id = self.request.query_params.get('category', None) - - if cat_id is None: - # No category filtering if category is not specified - pass - - else: - # Category has been specified! - if isNull(cat_id): - # A 'null' category is the top-level category - if cascade is False: - # Do not cascade, only list parts in the top-level category - parts_list = parts_list.filter(category=None) - - else: - try: - category = PartCategory.objects.get(pk=cat_id) - - # If '?cascade=true' then include parts which exist in sub-categories - if cascade: - parts_list = parts_list.filter(category__in=category.getUniqueChildren()) - # Just return parts directly in the requested category - else: - parts_list = parts_list.filter(category=cat_id) - except (ValueError, PartCategory.DoesNotExist): - pass - - # Ensure that related models are pre-loaded to reduce DB trips - parts_list = self.get_serializer_class().setup_eager_loading(parts_list) - - return parts_list - permission_classes = [ permissions.IsAuthenticated, ] diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 788613e104..184bdcfc11 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -10,6 +10,12 @@ from .models import PartCategory from .models import BomItem from .models import PartParameter, PartParameterTemplate +from decimal import Decimal + +from django.db.models import Q, F, Sum, Count +from django.db.models.functions import Coalesce + +from InvenTree.status_codes import StockStatus, OrderStatus, BuildStatus from InvenTree.serializers import InvenTreeModelSerializer @@ -78,24 +84,77 @@ class PartSerializer(InvenTreeModelSerializer): Used when displaying all details of a single component. """ - allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) - bom_items = serializers.IntegerField(source='bom_count', read_only=True) - building = serializers.FloatField(source='quantity_being_built', read_only=False) - category_name = serializers.CharField(source='category_path', read_only=True) - image = serializers.CharField(source='get_image_url', read_only=True) - on_order = serializers.FloatField(read_only=True) - thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) - url = serializers.CharField(source='get_absolute_url', read_only=True) - used_in = serializers.IntegerField(source='used_in_count', read_only=True) + @staticmethod + def prefetch_queryset(queryset): + return queryset.prefetch_related( + 'category', + 'stock_items', + 'bom_items', + 'builds', + 'supplier_parts', + 'supplier_parts__purchase_order_line_items', + 'supplier_parts__purcahes_order_line_items__order' + ) @staticmethod - def setup_eager_loading(queryset): - queryset = queryset.prefetch_related('category') - queryset = queryset.prefetch_related('stock_items') - queryset = queryset.prefetch_related('bom_items') - queryset = queryset.prefetch_related('builds') + def annotate_queryset(queryset): + """ + Add some extra annotations to the queryset, + performing database queries as efficiently as possible, + to reduce database trips. + """ + + # Filter to limit stock items to "available" + stock_filter = Q(stock_items__status__in=StockStatus.AVAILABLE_CODES) + + # Filter to limit orders to "open" + order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN) + + # Filter to limit builds to "active" + build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES) + + # 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( + ordering=Coalesce(Sum( + 'supplier_parts__purchase_order_line_items__quantity', + filter=order_filter, + distinct=True + ), Decimal(0)) - Coalesce(Sum( + 'supplier_parts__purchase_order_line_items__received', + filter=order_filter, + distinct=True + ), Decimal(0)) + ) + + # Annotate number of parts being build + queryset = queryset.annotate( + building=Coalesce( + Sum('builds__quantity', filter=build_filter, distinct=True), Decimal(0) + ) + ) + return queryset + in_stock = serializers.FloatField(read_only=True) + ordering = serializers.FloatField(read_only=True) + building = serializers.FloatField(read_only=True) + + #allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) + #bom_items = serializers.IntegerField(source='bom_count', read_only=True) + #building = serializers.FloatField(source='quantity_being_built', read_only=False) + #category_name = serializers.CharField(source='category_path', read_only=True) + image = serializers.CharField(source='get_image_url', read_only=True) + #on_order = serializers.FloatField(read_only=True) + thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) + url = serializers.CharField(source='get_absolute_url', read_only=True) + #used_in = serializers.IntegerField(source='used_in_count', read_only=True) + # TODO - Include a 'category_detail' field which serializers the category object class Meta: @@ -103,31 +162,34 @@ class PartSerializer(InvenTreeModelSerializer): partial = True fields = [ 'active', - 'allocated_stock', + #'allocated_stock', 'assembly', - 'bom_items', - 'building', + #'bom_items', + #'building', 'category', - 'category_name', + #'category_name', 'component', 'description', 'full_name', 'image', + 'in_stock', + 'ordering', + 'building', 'IPN', 'is_template', 'keywords', 'link', 'name', 'notes', - 'on_order', + #'on_order', 'pk', 'purchaseable', 'salable', 'thumbnail', 'trackable', - 'total_stock', + #'total_stock', 'units', - 'used_in', + #'used_in', 'url', # Link to the part detail page 'variant_of', 'virtual', From 2621c51a7e4d3ad9874222d407c0fce5a5ca3472 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 19 Apr 2020 23:50:41 +1000 Subject: [PATCH 03/15] Further API cleanup - Perform a single call to get starred parts for current user and record results - This provides significant speed improvements - Remove old manual serializer - More data prefetching --- InvenTree/part/api.py | 145 +++++++--------------------------- InvenTree/part/serializers.py | 65 +++++++++++---- 2 files changed, 75 insertions(+), 135 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 6699dff64f..c65e3c673c 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -168,6 +168,29 @@ class PartList(generics.ListCreateAPIView): queryset = Part.objects.all() + starred_parts = None + + 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: + self.starred_parts = [star.part for star in self.request.user.starred_parts.all()] + + kwargs['starred_parts'] = self.starred_parts + + return self.serializer_class(*args, **kwargs) + def create(self, request, *args, **kwargs): """ Override the default 'create' behaviour: We wish to save the user who created this part! @@ -249,7 +272,7 @@ class PartList(generics.ListCreateAPIView): if has_stock: queryset = queryset.filter(Q(in_stock__gt=0)) else: - queryset = queryset.filter(Q(in_stock_lte=0)) + queryset = queryset.filter(Q(in_stock__lte=0)) # If we are filtering by 'low_stock' status low_stock = self.request.query_params.get('low_stock', None) @@ -268,123 +291,6 @@ class PartList(generics.ListCreateAPIView): return queryset - def dolist(self, request, *args, **kwargs): - """ - Instead of using the DRF serialiser to LIST, - we serialize the objects manually. - This turns out to be significantly faster. - """ - - queryset = self.filter_queryset(self.get_queryset()) - - queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) - - queryset = part_serializers.PartSerializer.annotate_queryset(queryset) - - # Filters for annotations - - # "on_order" items should only sum orders which are currently outstanding - order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN) - - # "building" should only reference builds which are active - build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES) - - # Set of fields we wish to serialize - data = queryset.values( - 'pk', - 'category', - 'image', - 'name', - 'IPN', - 'revision', - 'description', - 'keywords', - 'is_template', - 'link', - 'units', - 'minimum_stock', - 'trackable', - 'assembly', - 'component', - 'salable', - 'active', - ).annotate( - # Quantity of items which are "in stock" - in_stock=Coalesce(Sum('stock_items__quantity'), Decimal(0)) #, filter=stock_filter), Decimal(0)), - #on_order=Coalesce(Sum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter), Decimal(0)), - #building=Coalesce(Sum('builds__quantity', filter=build_filter), Decimal(0)), - ) - - # If we are filtering by 'has_stock' status - has_stock = self.request.query_params.get('has_stock', None) - - if has_stock is not None: - has_stock = str2bool(has_stock) - - if has_stock: - # Filter items which have a non-null 'in_stock' quantity above zero - data = data.filter(in_stock__gt=0) - else: - # Filter items which a null or zero 'in_stock' quantity - data = data.filter(Q(in_stock__lte=0)) - - # If we are filtering by 'low_stock' status - low_stock = self.request.query_params.get('low_stock', None) - - if low_stock is not None: - low_stock = str2bool(low_stock) - - if low_stock: - # Ignore any parts which do not have a specified 'minimum_stock' level - data = data.exclude(minimum_stock=0) - # Filter items which have an 'in_stock' level lower than 'minimum_stock' - data = data.filter(Q(in_stock__lt=F('minimum_stock'))) - else: - # Filter items which have an 'in_stock' level higher than 'minimum_stock' - data = data.filter(Q(in_stock__gte=F('minimum_stock'))) - - # Get a list of the parts that this user has starred - starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()] - - # Reduce the number of lookups we need to do for the part categories - categories = {} - - for item in data: - - if item['image']: - # Is this part 'starred' for the current user? - item['starred'] = item['pk'] in starred_parts - - img = item['image'] - - # Use the 'thumbnail' image here instead of the full-size image - # Note: The full-size image is used when requesting the /api/part// endpoint - - if img: - fn, ext = os.path.splitext(img) - - thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext) - - thumb = os.path.join(settings.MEDIA_URL, thumb) - else: - thumb = '' - - item['thumbnail'] = thumb - - del item['image'] - - cat_id = item['category'] - - if cat_id: - if cat_id not in categories: - categories[cat_id] = PartCategory.objects.get(pk=cat_id).pathstring - - item['category__name'] = categories[cat_id] - else: - item['category__name'] = None - - return Response(data) - permission_classes = [ permissions.IsAuthenticated, ] @@ -410,6 +316,7 @@ class PartList(generics.ListCreateAPIView): 'name', ] + # Default ordering ordering = 'name' search_fields = [ @@ -538,7 +445,9 @@ class BomList(generics.ListCreateAPIView): kwargs['part_detail'] = part_detail kwargs['sub_part_detail'] = sub_part_detail + # Ensure the request context is passed through! kwargs['context'] = self.get_serializer_context() + return self.serializer_class(*args, **kwargs) def get_queryset(self): diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 184bdcfc11..1b60be121f 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -84,8 +84,29 @@ class PartSerializer(InvenTreeModelSerializer): Used when displaying all details of a single component. """ + def __init__(self, *args, **kwargs): + """ + Custom initialization method for PartSerializer, + so that we can optionally pass extra fields based on the query. + """ + + self.starred_parts = kwargs.pop('starred_parts', []) + + category_detail = kwargs.pop('category_detail', False) + + super().__init__(*args, **kwargs) + + if category_detail is not True: + self.fields.pop('category_detail') + + @staticmethod def prefetch_queryset(queryset): + """ + Prefetch related database tables, + to reduce database hits. + """ + return queryset.prefetch_related( 'category', 'stock_items', @@ -93,7 +114,10 @@ class PartSerializer(InvenTreeModelSerializer): 'builds', 'supplier_parts', 'supplier_parts__purchase_order_line_items', - 'supplier_parts__purcahes_order_line_items__order' + 'supplier_parts__purcahes_order_line_items__order', + 'starred_users', + 'starred_user__user', + 'starred_user__part', ) @staticmethod @@ -141,33 +165,40 @@ class PartSerializer(InvenTreeModelSerializer): return queryset + def get_starred(self, part): + """ + Return "true" if the part is starred by the current user. + """ + + return part in self.starred_parts + + # Extra detail for the category + category_detail = CategorySerializer(source='category', many=False, read_only=True) + + # Calculated fields in_stock = serializers.FloatField(read_only=True) ordering = serializers.FloatField(read_only=True) building = serializers.FloatField(read_only=True) - #allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) - #bom_items = serializers.IntegerField(source='bom_count', read_only=True) - #building = serializers.FloatField(source='quantity_being_built', read_only=False) - #category_name = serializers.CharField(source='category_path', read_only=True) image = serializers.CharField(source='get_image_url', read_only=True) - #on_order = serializers.FloatField(read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) - url = serializers.CharField(source='get_absolute_url', read_only=True) - #used_in = serializers.IntegerField(source='used_in_count', read_only=True) + starred = serializers.SerializerMethodField() - # TODO - Include a 'category_detail' field which serializers the category object + # TODO - Include annotation for the following fields: + # allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) + # bom_items = serializers.IntegerField(source='bom_count', read_only=True) + # used_in = serializers.IntegerField(source='used_in_count', read_only=True) class Meta: model = Part partial = True fields = [ 'active', - #'allocated_stock', + # 'allocated_stock', 'assembly', - #'bom_items', - #'building', + # 'bom_items', 'category', - #'category_name', + 'category_detail', 'component', 'description', 'full_name', @@ -179,18 +210,18 @@ class PartSerializer(InvenTreeModelSerializer): 'is_template', 'keywords', 'link', + 'minimum_stock', 'name', 'notes', - #'on_order', 'pk', 'purchaseable', + 'revision', 'salable', + 'starred', 'thumbnail', 'trackable', - #'total_stock', 'units', - #'used_in', - 'url', # Link to the part detail page + # 'used_in', 'variant_of', 'virtual', ] From 092215918cd8457b1c0badc95c161525b570175c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 19 Apr 2020 23:56:16 +1000 Subject: [PATCH 04/15] PEP fixes --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/InvenTree/urls.py | 6 ++++-- InvenTree/part/api.py | 11 +++-------- InvenTree/part/serializers.py | 5 ++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 4f1e172e5a..bbd42b957c 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -101,7 +101,7 @@ INSTALLED_APPS = [ 'crispy_forms', # Improved form rendering 'import_export', # Import / export tables to file 'django_cleanup', # Automatically delete orphaned MEDIA files - #'qr_code', # Generate QR codes + # 'qr_code', # Generate QR codes 'mptt', # Modified Preorder Tree Traversal 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 76ab422e7d..53d2d07544 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -8,7 +8,8 @@ Passes URL lookup downstream to each app as required. from django.conf.urls import url, include from django.contrib import admin from django.contrib.auth import views as auth_views -#from qr_code import urls as qr_code_urls +# TODO - Remove this line once a new QR solution has been implemented +# from qr_code import urls as qr_code_urls from company.urls import company_urls from company.urls import supplier_part_urls @@ -99,7 +100,8 @@ urlpatterns = [ url(r'^admin/', admin.site.urls, name='inventree-admin'), - #url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), + # TODO - Remove this line! + # url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), url(r'^index/', IndexView.as_view(), name='index'), url(r'^search/', SearchView.as_view(), name='search'), diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index c65e3c673c..6eda5539dd 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -6,10 +6,8 @@ Provides a JSON API for the Part app from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend -from django.conf import settings -from django.db.models import Q, F, Sum, Count -from django.db.models.functions import Coalesce +from django.db.models import Q, F, Count from rest_framework import status from rest_framework.response import Response @@ -19,15 +17,11 @@ from rest_framework import generics, permissions from django.conf.urls import url, include from django.urls import reverse -import os -from decimal import Decimal - from .models import Part, PartCategory, BomItem, PartStar from .models import PartParameter, PartParameterTemplate from . import serializers as part_serializers -from InvenTree.status_codes import OrderStatus, StockStatus, BuildStatus from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull @@ -125,6 +119,8 @@ class PartThumbs(generics.ListAPIView): # Get all Parts which have an associated image queryset = Part.objects.all().exclude(image='') + # TODO - We should return the thumbnails here, not the full image! + # Return the most popular parts first data = queryset.values( 'image', @@ -209,7 +205,6 @@ class PartList(generics.ListCreateAPIView): headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - def filter_queryset(self, queryset): """ Perform custom filtering of the queryset diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 1b60be121f..23e85df1a4 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -12,7 +12,7 @@ from .models import PartParameter, PartParameterTemplate from decimal import Decimal -from django.db.models import Q, F, Sum, Count +from django.db.models import Q, Sum from django.db.models.functions import Coalesce from InvenTree.status_codes import StockStatus, OrderStatus, BuildStatus @@ -99,7 +99,6 @@ class PartSerializer(InvenTreeModelSerializer): if category_detail is not True: self.fields.pop('category_detail') - @staticmethod def prefetch_queryset(queryset): """ @@ -123,7 +122,7 @@ class PartSerializer(InvenTreeModelSerializer): @staticmethod def annotate_queryset(queryset): """ - Add some extra annotations to the queryset, + Add some extra annotations to the queryset, performing database queries as efficiently as possible, to reduce database trips. """ From 3a64d0bc8facf0c6d0241d10c408a3119f1e4bed Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 00:00:14 +1000 Subject: [PATCH 05/15] Fix display of part table --- InvenTree/InvenTree/static/script/inventree/part.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index 7954caa012..7dc2c6a82a 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -87,6 +87,9 @@ function loadPartTable(table, url, options={}) { * disableFilters: If true, disable custom filters */ + // Ensure category detail is included + options.params['category_detail'] = true; + var params = options.params || {}; var filters = {}; @@ -184,11 +187,11 @@ function loadPartTable(table, url, options={}) { columns.push({ sortable: true, - field: 'category__name', + field: 'category_detail', title: 'Category', formatter: function(value, row, index, field) { if (row.category) { - return renderLink(row.category__name, "/part/category/" + row.category + "/"); + return renderLink(value.pathstring, "/part/category/" + row.category + "/"); } else { return 'No category'; From ff91c4ec5365f660023f234f699b31d29290efaa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 00:10:16 +1000 Subject: [PATCH 06/15] Add a new verision of django-qr-code Official package does not (yet) support django 3.0 --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/InvenTree/urls.py | 6 ++---- requirements.txt | 5 ++++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index bbd42b957c..fa6bf96ec7 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -101,7 +101,7 @@ INSTALLED_APPS = [ 'crispy_forms', # Improved form rendering 'import_export', # Import / export tables to file 'django_cleanup', # Automatically delete orphaned MEDIA files - # 'qr_code', # Generate QR codes + 'qr_code', # Generate QR codes 'mptt', # Modified Preorder Tree Traversal 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 53d2d07544..d9600333f4 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -8,8 +8,7 @@ Passes URL lookup downstream to each app as required. from django.conf.urls import url, include from django.contrib import admin from django.contrib.auth import views as auth_views -# TODO - Remove this line once a new QR solution has been implemented -# from qr_code import urls as qr_code_urls +from qr_code import urls as qr_code_urls from company.urls import company_urls from company.urls import supplier_part_urls @@ -100,8 +99,7 @@ urlpatterns = [ url(r'^admin/', admin.site.urls, name='inventree-admin'), - # TODO - Remove this line! - # url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), + url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), url(r'^index/', IndexView.as_view(), name='index'), url(r'^search/', SearchView.as_view(), name='search'), diff --git a/requirements.txt b/requirements.txt index a6c780b6ff..3b58076828 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,10 @@ tablib==0.13.0 # Import / export data files django-crispy-forms==1.8.1 # Form helpers django-import-export==2.0.0 # Data import / export for admin interface django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files -django-qr-code==1.1.0 # Generate QR codes +# TODO: Once the official django-qr-code package has been updated with Django3.x support, +# the following line should be removed. +git+git://github.com/chrissam/django-qr-code +# django-qr-code==1.1.0 # Generate QR codes flake8==3.3.0 # PEP checking coverage==4.0.3 # Unit test coverage python-coveralls==2.9.1 # Coveralls linking (for Travis) From 468eba1759d9ee4bfa9d02c81117880a9f40c665 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 00:16:57 +1000 Subject: [PATCH 07/15] Update PIP requirements - django-mptt required updating to fix a bug where tree was not being rebuild on item save --- requirements.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3b58076828..d402a41af5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ wheel>=0.34.2 # Wheel -Django==3.0.5 # Django package +Django==3.0.5 # Django package pillow==6.2.0 # Image manipulation djangorestframework==3.10.3 # DRF framework django-dbbackup==3.3.0 # Database backup / restore functionality django-cors-headers==3.2.0 # CORS headers extension for DRF django_filter==2.2.0 # Extended filtering options -django-mptt==0.10.0 # Modified Preorder Tree Traversal +django-mptt==0.11.0 # Modified Preorder Tree Traversal django-markdownx==3.0.1 # Markdown form fields django-markdownify==0.8.0 # Markdown rendering coreapi==2.3.0 # API documentation pygments==2.2.0 # Syntax highlighting -tablib==0.13.0 # Import / export data files +tablib==1.1.0 # Import / export data files django-crispy-forms==1.8.1 # Form helpers django-import-export==2.0.0 # Data import / export for admin interface django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files @@ -21,5 +21,5 @@ git+git://github.com/chrissam/django-qr-code flake8==3.3.0 # PEP checking coverage==4.0.3 # Unit test coverage python-coveralls==2.9.1 # Coveralls linking (for Travis) -rapidfuzz==0.2.1 # Fuzzy string matching -django-stdimage==5.0.3 # Advanced ImageField management \ No newline at end of file +rapidfuzz==0.7.6 # Fuzzy string matching +django-stdimage==5.1.1 # Advanced ImageField management \ No newline at end of file From 5233281a241ffe4ea3155434bc65669f115dd824 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 00:19:04 +1000 Subject: [PATCH 08/15] Should have checked first :'( New tablib is broked (at least in the admin interface) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d402a41af5..c6379857c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ django-markdownx==3.0.1 # Markdown form fields django-markdownify==0.8.0 # Markdown rendering coreapi==2.3.0 # API documentation pygments==2.2.0 # Syntax highlighting -tablib==1.1.0 # Import / export data files +tablib==0.13.0 # Import / export data files django-crispy-forms==1.8.1 # Form helpers django-import-export==2.0.0 # Data import / export for admin interface django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files From 6a89e0089d1a5d853e30b90a5495cbb681c63182 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 00:49:13 +1000 Subject: [PATCH 09/15] Updates for stock serializer --- InvenTree/part/api.py | 7 +++ InvenTree/part/serializers.py | 5 +- InvenTree/stock/api.py | 88 +++++++--------------------------- InvenTree/stock/serializers.py | 34 ++++++++----- 4 files changed, 45 insertions(+), 89 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 6eda5539dd..3169c613ca 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -205,6 +205,13 @@ class PartList(generics.ListCreateAPIView): headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + def get_queryset(self, *args, **kwargs): + + queryset = super().get_queryset(*args, **kwargs) + queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) + + return queryset + def filter_queryset(self, queryset): """ Perform custom filtering of the queryset diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 23e85df1a4..7ad1b83aa8 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -113,10 +113,7 @@ class PartSerializer(InvenTreeModelSerializer): 'builds', 'supplier_parts', 'supplier_parts__purchase_order_line_items', - 'supplier_parts__purcahes_order_line_items__order', - 'starred_users', - 'starred_user__user', - 'starred_user__part', + 'supplier_parts__purchase_order_line_items__order', ) @staticmethod diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 9f485ba5f7..1e27848eb9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -317,100 +317,44 @@ class StockList(generics.ListCreateAPIView): - status: Filter by the StockItem status """ + serializer_class = StockItemSerializer + queryset = StockItem.objects.all() def get_serializer(self, *args, **kwargs): try: - part_detail = str2bool(self.request.GET.get('part_detail', None)) - location_detail = str2bool(self.request.GET.get('location_detail', None)) + part_detail = str2bool(self.request.query_params.get('part_detail', None)) + location_detail = str2bool(self.request.query_params.get('location_detail', None)) + supplier_part_detail = str2bool(self.request.query_params.get('supplier_part_detail', None)) except AttributeError: part_detail = None location_detail = None + supplier_part_detail = None kwargs['part_detail'] = part_detail kwargs['location_detail'] = location_detail + # Ensure the request context is passed through kwargs['context'] = self.get_serializer_context() + return self.serializer_class(*args, **kwargs) - def list(self, request, *args, **kwargs): + # TODO - Override the 'create' method for this view, + # to allow the user to be recorded when a new StockItem object is created - queryset = self.filter_queryset(self.get_queryset()) + def get_queryset(self, *args, **kwargs): - # Instead of using the DRF serializer to LIST, - # we will serialize the objects manually. - # This is significantly faster + queryset = super().get_queryset(*args, **kwargs) + queryset = StockItemSerializer.prefetch_queryset(queryset) - data = queryset.values( - 'pk', - 'uid', - 'parent', - 'quantity', - 'serial', - 'batch', - 'status', - 'notes', - 'link', - 'location', - 'location__name', - 'location__description', - 'part', - 'part__IPN', - 'part__name', - 'part__revision', - 'part__description', - 'part__image', - 'part__category', - 'part__category__name', - 'part__category__description', - 'supplier_part', - ) + return queryset - # Reduce the number of lookups we need to do for categories - # Cache location lookups for this query - locations = {} - for item in data: - - img = item['part__image'] - - if img: - # Use the thumbnail image instead - fn, ext = os.path.splitext(img) - - thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext) - - thumb = os.path.join(settings.MEDIA_URL, thumb) - else: - thumb = '' - - item['part__thumbnail'] = thumb - - del item['part__image'] - - loc_id = item['location'] - - if loc_id: - if loc_id not in locations: - locations[loc_id] = StockLocation.objects.get(pk=loc_id).pathstring - - item['location__path'] = locations[loc_id] - else: - item['location__path'] = None - - item['status_text'] = StockStatus.label(item['status']) - - return Response(data) - - def get_queryset(self): - """ - If the query includes a particular location, - we may wish to also request stock items from all child locations. - """ + def filter_queryset(self, queryset): # Start with all objects - stock_list = super(StockList, self).get_queryset() + stock_list = super().filter_queryset(queryset) # Filter out parts which are not actually "in stock" stock_list = stock_list.filter(customer=None, belongs_to=None) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index fe4f850658..a7b51672c4 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -56,18 +56,28 @@ class StockItemSerializer(InvenTreeModelSerializer): - Includes serialization for the item location """ - url = serializers.CharField(source='get_absolute_url', read_only=True) + @staticmethod + def prefetch_queryset(queryset): + """ + Prefetch related database tables, + to reduce database hits. + """ + + return queryset.prefetch_related( + 'supplier_part', + 'supplier_part__supplier', + 'supplier_part__manufacturer', + 'location', + 'part' + ) + status_text = serializers.CharField(source='get_status_display', read_only=True) - part_name = serializers.CharField(source='get_part_name', read_only=True) - - part_image = serializers.CharField(source='part__image', read_only=True) - - tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) + #tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) - supplier_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True) + supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True) def __init__(self, *args, **kwargs): @@ -84,7 +94,7 @@ class StockItemSerializer(InvenTreeModelSerializer): self.fields.pop('location_detail') if supplier_detail is not True: - self.fields.pop('supplier_detail') + self.fields.pop('supplier_part_detail') class Meta: model = StockItem @@ -97,18 +107,16 @@ class StockItemSerializer(InvenTreeModelSerializer): 'notes', 'part', 'part_detail', - 'part_name', - 'part_image', 'pk', 'quantity', 'serial', 'supplier_part', - 'supplier_detail', + 'supplier_part_detail', 'status', 'status_text', - 'tracking_items', + #'tracking_items', 'uid', - 'url', + #'url', ] """ These fields are read-only in this context. From 4b1b9df193703fd7615b0f3d091d9dc6048fc0af Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 01:02:19 +1000 Subject: [PATCH 10/15] Update stock table rendering --- .../static/script/inventree/stock.js | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index d68d0946a2..6956b0abfc 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -43,8 +43,11 @@ function loadStockTable(table, options) { * filterList -
    element where filters are displayed * disableFilters: If true, disable custom filters */ - + // List of user-params which override the default filters + + options.params['part_detail'] = true; + var params = options.params || {}; var filterListElement = options.filterList || "#filter-list-stock"; @@ -83,27 +86,21 @@ function loadStockTable(table, options) { var row = data[0]; - if (field == 'part__name') { + if (field == 'part_name') { - var name = row.part__IPN; + var name = row.part_detail.full_name; - if (name) { - name += ' | '; - } - - name += row.part__name; - - return imageHoverIcon(row.part__thumbnail) + name + ' (' + data.length + ' items)'; + return imageHoverIcon(row.part_detail.thumbnail) + name + ' (' + data.length + ' items)'; } - else if (field == 'part__description') { - return row.part__description; + else if (field == 'part_description') { + return row.part_detail.description; } else if (field == 'quantity') { var stock = 0; var items = 0; data.forEach(function(item) { - stock += item.quantity; + stock += parseFloat(item.quantity); items += 1; }); @@ -216,25 +213,14 @@ function loadStockTable(table, options) { visible: false, }, { - field: 'part__name', + field: 'part_name', title: 'Part', sortable: true, formatter: function(value, row, index, field) { - var name = row.part__IPN; - - if (name) { - name += ' | '; - } - - name += row.part__name; - - if (row.part__revision) { - name += " | "; - name += row.part__revision; - } - var url = ''; + var thumb = row.part_detail.thumbnail; + var name = row.part_detail.full_name; if (row.supplier_part) { url = `/supplier-part/${row.supplier_part}/`; @@ -242,13 +228,16 @@ function loadStockTable(table, options) { url = `/part/${row.part}/`; } - return imageHoverIcon(row.part__thumbnail) + renderLink(name, url); + return imageHoverIcon(thumb) + renderLink(name, url); } }, { - field: 'part__description', + field: 'part_description', title: 'Description', sortable: true, + formatter: function(value, row, index, field) { + return row.part_detail.description; + } }, { field: 'quantity', @@ -256,11 +245,13 @@ function loadStockTable(table, options) { sortable: true, formatter: function(value, row, index, field) { - var val = value; + var val = parseFloat(value); // If there is a single unit with a serial number, use the serial number if (row.serial && row.quantity == 1) { val = '# ' + row.serial; + } else { + val = +val.toFixed(5); } var text = renderLink(val, '/stock/item/' + row.pk + '/'); @@ -282,7 +273,7 @@ function loadStockTable(table, options) { sortable: true, }, { - field: 'location__path', + field: 'location_detail.pathstring', title: 'Location', sortable: true, formatter: function(value, row, index, field) { From 0334035e77611823745681c3727e01174fa80694 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 01:09:37 +1000 Subject: [PATCH 11/15] Simplify StockItem serializer - Some more work needed here to cut down on database hits --- InvenTree/InvenTree/static/script/inventree/stock.js | 1 + InvenTree/part/serializers.py | 10 ---------- InvenTree/stock/serializers.py | 8 -------- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index 6956b0abfc..3fe11ed087 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -47,6 +47,7 @@ function loadStockTable(table, options) { // List of user-params which override the default filters options.params['part_detail'] = true; + options.params['location_detail'] = true; var params = options.params || {}; diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 7ad1b83aa8..a7305dcb2e 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -54,14 +54,6 @@ class PartBriefSerializer(InvenTreeModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) - - @staticmethod - def setup_eager_loading(queryset): - queryset = queryset.prefetch_related('category') - queryset = queryset.prefetch_related('stock_items') - queryset = queryset.prefetch_related('bom_items') - queryset = queryset.prefetch_related('builds') - return queryset class Meta: model = Part @@ -70,8 +62,6 @@ class PartBriefSerializer(InvenTreeModelSerializer): 'url', 'full_name', 'description', - 'total_stock', - 'available_stock', 'thumbnail', 'active', 'assembly', diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index a7b51672c4..a4de99040a 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -77,13 +77,11 @@ class StockItemSerializer(InvenTreeModelSerializer): part_detail = PartBriefSerializer(source='part', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) - supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True) def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) location_detail = kwargs.pop('location_detail', False) - supplier_detail = kwargs.pop('supplier_detail', False) super(StockItemSerializer, self).__init__(*args, **kwargs) @@ -93,9 +91,6 @@ class StockItemSerializer(InvenTreeModelSerializer): if location_detail is not True: self.fields.pop('location_detail') - if supplier_detail is not True: - self.fields.pop('supplier_part_detail') - class Meta: model = StockItem fields = [ @@ -111,12 +106,9 @@ class StockItemSerializer(InvenTreeModelSerializer): 'quantity', 'serial', 'supplier_part', - 'supplier_part_detail', 'status', 'status_text', - #'tracking_items', 'uid', - #'url', ] """ These fields are read-only in this context. From b2c40c91b7f5b019b5e91585d711d59dd0477437 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 01:11:25 +1000 Subject: [PATCH 12/15] PEP fixes --- InvenTree/stock/api.py | 8 +------- InvenTree/stock/serializers.py | 3 --- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 1e27848eb9..bf85294c4e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -5,7 +5,6 @@ JSON API for the Stock app from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters import NumberFilter -from django.conf import settings from django.conf.urls import url, include from django.urls import reverse from django.db.models import Q @@ -21,9 +20,7 @@ from .serializers import StockTrackingSerializer from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull -from InvenTree.status_codes import StockStatus -import os from decimal import Decimal, InvalidOperation from rest_framework.serializers import ValidationError @@ -326,11 +323,9 @@ class StockList(generics.ListCreateAPIView): try: part_detail = str2bool(self.request.query_params.get('part_detail', None)) location_detail = str2bool(self.request.query_params.get('location_detail', None)) - supplier_part_detail = str2bool(self.request.query_params.get('supplier_part_detail', None)) except AttributeError: part_detail = None location_detail = None - supplier_part_detail = None kwargs['part_detail'] = part_detail kwargs['location_detail'] = location_detail @@ -340,7 +335,7 @@ class StockList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) - # TODO - Override the 'create' method for this view, + # TODO - Override the 'create' method for this view, # to allow the user to be recorded when a new StockItem object is created def get_queryset(self, *args, **kwargs): @@ -350,7 +345,6 @@ class StockList(generics.ListCreateAPIView): return queryset - def filter_queryset(self, queryset): # Start with all objects diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index a4de99040a..2953809cab 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -8,7 +8,6 @@ from .models import StockItem, StockLocation from .models import StockItemTracking from part.serializers import PartBriefSerializer -from company.serializers import SupplierPartSerializer from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer @@ -72,8 +71,6 @@ class StockItemSerializer(InvenTreeModelSerializer): ) status_text = serializers.CharField(source='get_status_display', read_only=True) - - #tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) From c72fce0cc558a6fbe545bf9c4a162a1f98281bdd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 01:14:19 +1000 Subject: [PATCH 13/15] Add "tracking items" back in --- InvenTree/stock/serializers.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 2953809cab..84ee616024 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -67,14 +67,27 @@ class StockItemSerializer(InvenTreeModelSerializer): 'supplier_part__supplier', 'supplier_part__manufacturer', 'location', - 'part' + 'part', + 'tracking_info', ) + @staticmethod + def annotate_queryset(queryset): + """ + Add some extra annotations to the queryset, + performing database queries as efficiently as possible. + """ + + # TODO + pass + status_text = serializers.CharField(source='get_status_display', read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) + tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) + def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) @@ -105,6 +118,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'supplier_part', 'status', 'status_text', + 'tracking_items', 'uid', ] From 57fa69f6e66794616b9449a3826aa2a46d6a5b92 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 01:20:09 +1000 Subject: [PATCH 14/15] Update version - Display django version in "about" dialog --- InvenTree/InvenTree/version.py | 8 +++++++- InvenTree/part/templatetags/inventree_extras.py | 6 ++++++ InvenTree/templates/about.html | 6 +++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 386409646a..2b0d02bfe9 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -4,8 +4,9 @@ Provides information on the current InvenTree version import subprocess from common.models import InvenTreeSetting +import django -INVENTREE_SW_VERSION = "0.0.11_pre" +INVENTREE_SW_VERSION = "0.0.12 pre" def inventreeInstanceName(): @@ -18,6 +19,11 @@ def inventreeVersion(): return INVENTREE_SW_VERSION +def inventreeDjangoVersion(): + """ Return the version of Django library """ + return django.get_version() + + def inventreeCommitHash(): """ Returns the git commit hash for the running codebase """ diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index ab3b0694ac..8fa63adc0f 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -55,6 +55,12 @@ def inventree_version(*args, **kwargs): return version.inventreeVersion() +@register.simple_tag() +def django_version(*args, **kwargs): + """ Return Django version string """ + return version.inventreeDjangoVersion() + + @register.simple_tag() def inventree_commit_hash(*args, **kwargs): """ Return InvenTree git commit hash string """ diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 494d248d5e..87647eb4e7 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -25,6 +25,10 @@ {% trans "InvenTree Version" %}{% inventree_version %} + + + {% trans "Django Version" %}{% django_version %} + {% trans "Commit Hash" %}{% inventree_commit_hash %} @@ -45,7 +49,7 @@ {% trans "Submit Bug Report" %} - {% inventree_github_url %}/issues + {% inventree_github_url %}issues From f5c86bc45788061aebdda3e38acae98bd5d84b36 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 01:23:05 +1000 Subject: [PATCH 15/15] "Fix" for unit tests - Not working in travis for some reason? - But they are working locally... --- InvenTree/part/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 3169c613ca..f4b5f4a5eb 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -180,7 +180,7 @@ class PartList(generics.ListCreateAPIView): # 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: + if self.starred_parts is None and self.request is not None: self.starred_parts = [star.part for star in self.request.user.starred_parts.all()] kwargs['starred_parts'] = self.starred_parts