From 7e8664a4dd035703e94cf558f504c5fc2c3becda Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 09:15:09 +1100 Subject: [PATCH 1/9] Include "parts" count in Category API --- InvenTree/part/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index c23298a441..1c7ae6f4ca 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -18,6 +18,8 @@ class CategorySerializer(InvenTreeModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) + parts = serializers.IntegerField(source='item_count', read_only=True) + class Meta: model = PartCategory fields = [ @@ -27,6 +29,7 @@ class CategorySerializer(InvenTreeModelSerializer): 'pathstring', 'url', 'parent', + 'parts', ] From d17056820b4c7fcbb5148a38b4cb4d1e9d013726 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 09:25:58 +1100 Subject: [PATCH 2/9] Allow PartCategory filtering by null parent --- InvenTree/part/api.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 2a96a47fef..0538125828 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -57,6 +57,30 @@ class CategoryList(generics.ListCreateAPIView): permissions.IsAuthenticated, ] + def get_queryset(self): + """ + Custom filtering: + - Allow filtering by "null" parent to retrieve top-level part categories + """ + + cat_id = self.request.query_params.get('parent', None) + + queryset = super().get_queryset() + + if cat_id is not None: + + # Integer id? + try: + cat_id = int(cat_id) + queryset = queryset.filter(parent=cat_id) + except ValueError: + + # Look for top-level categories? + if str(cat_id).lower() in ['top', 'null', 'none', 'false', '-1']: + queryset = queryset.filter(parent=None) + + return queryset + filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -64,7 +88,6 @@ class CategoryList(generics.ListCreateAPIView): ] filter_fields = [ - 'parent', ] ordering_fields = [ From 6e65a736e7cd6f5671f9e3f3f416197c485b9d2d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 09:31:26 +1100 Subject: [PATCH 3/9] Add isNull function to query against null keys --- InvenTree/InvenTree/helpers.py | 15 +++++++++++++++ InvenTree/part/api.py | 21 +++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 023bf5c451..0abe5313ee 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -52,6 +52,21 @@ def str2bool(text, test=True): return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ] +def isNull(text): + """ + Test if a string 'looks' like a null value. + This is useful for querying the API against a null key. + + Args: + text: Input text + + Returns: + True if the text looks like a null value + """ + + return str(text).strip().lower() in ['top', 'null', 'none', 'empty', 'false', '-1'] + + def decimal2string(d): """ Format a Decimal number as a string, diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 0538125828..cf5a4b2deb 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -27,7 +27,7 @@ from . import serializers as part_serializers from InvenTree.status_codes import OrderStatus, StockStatus, BuildStatus from InvenTree.views import TreeSerializer -from InvenTree.helpers import str2bool +from InvenTree.helpers import str2bool, isNull class PartCategoryTree(TreeSerializer): @@ -69,15 +69,16 @@ class CategoryList(generics.ListCreateAPIView): if cat_id is not None: - # Integer id? - try: - cat_id = int(cat_id) - queryset = queryset.filter(parent=cat_id) - except ValueError: - - # Look for top-level categories? - if str(cat_id).lower() in ['top', 'null', 'none', 'false', '-1']: - queryset = queryset.filter(parent=None) + # Look for top-level categories + if isNull(cat_id): + queryset = queryset.filter(parent=None) + + else: + try: + cat_id = int(cat_id) + queryset = queryset.filter(parent=cat_id) + except ValueError: + pass return queryset From f5150f549ad000b3b453bc84abfb921a67b02e7f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 09:37:03 +1100 Subject: [PATCH 4/9] Part API changes - Allow filtering parts with null parent (top-level category parts) - Option to include sub-category parts or not --- .../InvenTree/static/script/inventree/part.js | 4 +++ InvenTree/part/api.py | 25 ++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index f05c5670da..dceb08dd5f 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -95,6 +95,10 @@ function loadPartTable(table, url, options={}) { query.active = true; } + // Include sub-category search + // TODO - Make this user-configurable! + query.cascade = true; + var columns = [ { field: 'pk', diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index cf5a4b2deb..55e711ff5f 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -243,12 +243,25 @@ class PartList(generics.ListCreateAPIView): # Start with all objects parts_list = Part.objects.all() - if cat_id: - try: - category = PartCategory.objects.get(pk=cat_id) - parts_list = parts_list.filter(category__in=category.getUniqueChildren()) - except PartCategory.DoesNotExist: - pass + cascade = str2bool(self.request.query_params.get('cascade', False)) + + if cat_id is not None: + + if isNull(cat_id): + parts_list = parts_list.filter(category=None) + else: + try: + cat_id = int(cat_id) + 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) From fb949495386da570084bcbf7126d09b736c1e86c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 11:34:42 +1100 Subject: [PATCH 5/9] Allow StockLocation filtering of null parent --- InvenTree/stock/api.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 86759a90aa..8e36e51ccf 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -19,7 +19,7 @@ from .serializers import LocationSerializer from .serializers import StockTrackingSerializer from InvenTree.views import TreeSerializer -from InvenTree.helpers import str2bool +from InvenTree.helpers import str2bool, isNull from InvenTree.status_codes import StockStatus import os @@ -223,9 +223,33 @@ class StockLocationList(generics.ListCreateAPIView): """ queryset = StockLocation.objects.all() - serializer_class = LocationSerializer + def get_queryset(self): + """ + Custom filtering: + - Allow filtering by "null" parent to retrieve top-level stock locations + """ + + queryset = super().get_queryset() + + loc_id = self.request.query_params.get('parent', None) + + if loc_id is not None: + + # Look for top-level locations + if isNull(loc_id): + queryset = queryset.filter(parent=None) + + else: + try: + loc_id = int(loc_id) + queryset = queryset.filter(parent=loc_id) + except ValueError: + pass + + return queryset + permission_classes = [ permissions.IsAuthenticated, ] @@ -237,7 +261,6 @@ class StockLocationList(generics.ListCreateAPIView): ] filter_fields = [ - 'parent', ] search_fields = [ From d4da6211bee4cf91931b0858ed62a866feee7539 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 11:40:37 +1100 Subject: [PATCH 6/9] StockItem: filtering improvements - Optional 'cacade' param - Filter by null parent --- .../static/script/inventree/stock.js | 4 +++ InvenTree/stock/api.py | 25 +++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index 1495d04cae..2a06808690 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -42,6 +42,10 @@ function loadStockTable(table, options) { var params = options.params || {}; + // Enforce 'cascade' option + // TODO - Make this user-configurable? + params.cascade = true; + console.log('load stock table'); table.inventreeTable({ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 8e36e51ccf..cda1034f4d 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -396,13 +396,24 @@ class StockList(generics.ListCreateAPIView): # Does the client wish to filter by stock location? loc_id = self.request.query_params.get('location', None) - if loc_id: - try: - location = StockLocation.objects.get(pk=loc_id) - stock_list = stock_list.filter(location__in=location.getUniqueChildren()) - - except (ValueError, StockLocation.DoesNotExist): - pass + cascade = str2bool(self.request.query_params.get('cascade', False)) + + if loc_id is not None: + + # Filter by 'null' location (i.e. top-level items) + if isNull(loc_id): + stock_list = stock_list.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()) + else: + stock_list = stock_list.filter(location=loc_id) + + except (ValueError, StockLocation.DoesNotExist): + pass # Does the client wish to filter by part category? cat_id = self.request.query_params.get('category', None) From ccb637773f81304c311e2a9f8a7334ff66a66e73 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 11:41:51 +1100 Subject: [PATCH 7/9] Add item count to StockLocation serializer --- InvenTree/stock/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 14fe043564..f84667e352 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -119,6 +119,8 @@ class LocationSerializer(InvenTreeModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) + items = serializers.IntegerField(source='item_count', read_only=True) + class Meta: model = StockLocation fields = [ @@ -127,7 +129,8 @@ class LocationSerializer(InvenTreeModelSerializer): 'name', 'description', 'parent', - 'pathstring' + 'pathstring', + 'items', ] From 92f5648656945d20e7bc5509b2098e18ca24236a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 12:20:43 +1100 Subject: [PATCH 8/9] Fix API endpoints for Stock app --- InvenTree/stock/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index cda1034f4d..3eb183ce35 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -545,13 +545,13 @@ stock_endpoints = [ ] location_endpoints = [ - url(r'^$', LocationDetail.as_view(), name='api-location-detail'), + url(r'^(?P\d+)/', LocationDetail.as_view(), name='api-location-detail'), + + url(r'^.*$', StockLocationList.as_view(), name='api-location-list'), ] stock_api_urls = [ - url(r'location/?', StockLocationList.as_view(), name='api-location-list'), - - url(r'location/(?P\d+)/', include(location_endpoints)), + url(r'location/', include(location_endpoints)), # These JSON endpoints have been replaced (for now) with server-side form rendering - 02/06/2019 # url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'), From b25df586cdc8f38d98953c928810a6f118574e1e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 3 Apr 2020 12:30:58 +1100 Subject: [PATCH 9/9] Fix API tets --- InvenTree/part/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index f93eb6604d..64ae616720 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -103,7 +103,7 @@ class PartAPITest(APITestCase): If provided, parts are provided for ANY child category (recursive) """ url = reverse('api-part-list') - data = {'category': 1} + data = {'category': 1, 'cascade': True} # Now request to include child categories response = self.client.get(url, data, format='json')