mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Stock API filtering fix (#4350)
* Improve filtering options for StockItem list - Make use of StockFilter introspection * Remove outdated filter * remove outdated "max_results" parameter * Fix cascade issue for stocklist API * Add relationship filters to the StockItemFilter * Fix filtering by 'status' and 'allocated' * Refactor 'customer' and 'expired' filters * Cleanup * Adds unit test for top-level stock location filtering
This commit is contained in:
		| @@ -1342,15 +1342,11 @@ class PartList(APIDownloadMixin, ListCreateAPI): | |||||||
|         # Does the user wish to filter by category? |         # Does the user wish to filter by category? | ||||||
|         cat_id = params.get('category', None) |         cat_id = params.get('category', None) | ||||||
|  |  | ||||||
|         if cat_id is None: |         if cat_id is not None: | ||||||
|             # No category filtering if category is not specified |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         else: |  | ||||||
|             # Category has been specified! |             # Category has been specified! | ||||||
|             if isNull(cat_id): |             if isNull(cat_id): | ||||||
|                 # A 'null' category is the top-level category |                 # A 'null' category is the top-level category | ||||||
|                 if cascade is False: |                 if not cascade: | ||||||
|                     # Do not cascade, only list parts in the top-level category |                     # Do not cascade, only list parts in the top-level category | ||||||
|                     queryset = queryset.filter(category=None) |                     queryset = queryset.filter(category=None) | ||||||
|  |  | ||||||
| @@ -1393,20 +1389,6 @@ class PartList(APIDownloadMixin, ListCreateAPI): | |||||||
|  |  | ||||||
|             queryset = queryset.filter(pk__in=parts_needed_to_complete_builds) |             queryset = queryset.filter(pk__in=parts_needed_to_complete_builds) | ||||||
|  |  | ||||||
|         # Optionally limit the maximum number of returned results |  | ||||||
|         # e.g. for displaying "recent part" list |  | ||||||
|         max_results = params.get('max_results', None) |  | ||||||
|  |  | ||||||
|         if max_results is not None: |  | ||||||
|             try: |  | ||||||
|                 max_results = int(max_results) |  | ||||||
|  |  | ||||||
|                 if max_results > 0: |  | ||||||
|                     queryset = queryset[:max_results] |  | ||||||
|  |  | ||||||
|             except (ValueError): |  | ||||||
|                 pass |  | ||||||
|  |  | ||||||
|         return queryset |         return queryset | ||||||
|  |  | ||||||
|     filter_backends = [ |     filter_backends = [ | ||||||
|   | |||||||
| @@ -352,6 +352,25 @@ class StockLocationTree(ListAPI): | |||||||
| class StockFilter(rest_filters.FilterSet): | class StockFilter(rest_filters.FilterSet): | ||||||
|     """FilterSet for StockItem LIST API.""" |     """FilterSet for StockItem LIST API.""" | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         """Metaclass options for this filterset""" | ||||||
|  |  | ||||||
|  |         model = StockItem | ||||||
|  |  | ||||||
|  |         # Simple filter filters | ||||||
|  |         fields = [ | ||||||
|  |             'supplier_part', | ||||||
|  |             'belongs_to', | ||||||
|  |             'build', | ||||||
|  |             'customer', | ||||||
|  |             'sales_order', | ||||||
|  |             'purchase_order', | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |     # Relationship filters | ||||||
|  |     manufactuer = rest_filters.ModelChoiceFilter(label='Manufacturer', queryset=Company.objects.filter(is_manufacturer=True), field_name='manufacturer_part__manufacturer') | ||||||
|  |     supplier = rest_filters.ModelChoiceFilter(label='Supplier', queryset=Company.objects.filter(is_supplier=True), field_name='supplier_part__supplier') | ||||||
|  |  | ||||||
|     # Part name filters |     # Part name filters | ||||||
|     name = rest_filters.CharFilter(label='Part name (case insensitive)', field_name='part__name', lookup_expr='iexact') |     name = rest_filters.CharFilter(label='Part name (case insensitive)', field_name='part__name', lookup_expr='iexact') | ||||||
|     name_contains = rest_filters.CharFilter(label='Part name contains (case insensitive)', field_name='part__name', lookup_expr='icontains') |     name_contains = rest_filters.CharFilter(label='Part name contains (case insensitive)', field_name='part__name', lookup_expr='icontains') | ||||||
| @@ -369,16 +388,46 @@ class StockFilter(rest_filters.FilterSet): | |||||||
|     min_stock = rest_filters.NumberFilter(label='Minimum stock', field_name='quantity', lookup_expr='gte') |     min_stock = rest_filters.NumberFilter(label='Minimum stock', field_name='quantity', lookup_expr='gte') | ||||||
|     max_stock = rest_filters.NumberFilter(label='Maximum stock', field_name='quantity', lookup_expr='lte') |     max_stock = rest_filters.NumberFilter(label='Maximum stock', field_name='quantity', lookup_expr='lte') | ||||||
|  |  | ||||||
|  |     status = rest_filters.NumberFilter(label='Status Code', method='filter_status') | ||||||
|  |  | ||||||
|  |     def filter_status(self, queryset, name, value): | ||||||
|  |         """Filter by integer status code""" | ||||||
|  |  | ||||||
|  |         return queryset.filter(status=value) | ||||||
|  |  | ||||||
|  |     allocated = rest_filters.BooleanFilter(label='Is Allocated', method='filter_allocated') | ||||||
|  |  | ||||||
|  |     def filter_allocated(self, queryset, name, value): | ||||||
|  |         """Filter by whether or not the stock item is 'allocated'""" | ||||||
|  |  | ||||||
|  |         if str2bool(value): | ||||||
|  |             # Filter StockItem with either build allocations or sales order allocations | ||||||
|  |             return queryset.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)) | ||||||
|  |         else: | ||||||
|  |             # Filter StockItem without build allocations or sales order allocations | ||||||
|  |             return queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) | ||||||
|  |  | ||||||
|  |     expired = rest_filters.BooleanFilter(label='Expired', method='filter_expired') | ||||||
|  |  | ||||||
|  |     def filter_expired(self, queryset, name, value): | ||||||
|  |         """Filter by whether or not the stock item has expired""" | ||||||
|  |  | ||||||
|  |         if not common.settings.stock_expiry_enabled(): | ||||||
|  |             return queryset | ||||||
|  |  | ||||||
|  |         if str2bool(value): | ||||||
|  |             return queryset.filter(StockItem.EXPIRED_FILTER) | ||||||
|  |         else: | ||||||
|  |             return queryset.exclude(StockItem.EXPIRED_FILTER) | ||||||
|  |  | ||||||
|     in_stock = rest_filters.BooleanFilter(label='In Stock', method='filter_in_stock') |     in_stock = rest_filters.BooleanFilter(label='In Stock', method='filter_in_stock') | ||||||
|  |  | ||||||
|     def filter_in_stock(self, queryset, name, value): |     def filter_in_stock(self, queryset, name, value): | ||||||
|         """Filter by if item is in stock.""" |         """Filter by if item is in stock.""" | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.filter(StockItem.IN_STOCK_FILTER) |             return queryset.filter(StockItem.IN_STOCK_FILTER) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.exclude(StockItem.IN_STOCK_FILTER) |             return queryset.exclude(StockItem.IN_STOCK_FILTER) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     available = rest_filters.BooleanFilter(label='Available', method='filter_available') |     available = rest_filters.BooleanFilter(label='Available', method='filter_available') | ||||||
|  |  | ||||||
| @@ -389,12 +438,10 @@ class StockFilter(rest_filters.FilterSet): | |||||||
|         """ |         """ | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             # The 'quantity' field is greater than the calculated 'allocated' field |             # The 'quantity' field is greater than the calculated 'allocated' field | ||||||
|             queryset = queryset.filter(Q(quantity__gt=F('allocated'))) |             return queryset.filter(Q(quantity__gt=F('allocated'))) | ||||||
|         else: |         else: | ||||||
|             # The 'quantity' field is less than (or equal to) the calculated 'allocated' field |             # The 'quantity' field is less than (or equal to) the calculated 'allocated' field | ||||||
|             queryset = queryset.filter(Q(quantity__lte=F('allocated'))) |             return queryset.filter(Q(quantity__lte=F('allocated'))) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     batch = rest_filters.CharFilter(label="Batch code filter (case insensitive)", lookup_expr='iexact') |     batch = rest_filters.CharFilter(label="Batch code filter (case insensitive)", lookup_expr='iexact') | ||||||
|  |  | ||||||
| @@ -414,11 +461,9 @@ class StockFilter(rest_filters.FilterSet): | |||||||
|         q = Q(serial=None) | Q(serial='') |         q = Q(serial=None) | Q(serial='') | ||||||
|  |  | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.exclude(q) |             return queryset.exclude(q) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.filter(q) |             return queryset.filter(q) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch') |     has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch') | ||||||
|  |  | ||||||
| @@ -427,11 +472,9 @@ class StockFilter(rest_filters.FilterSet): | |||||||
|         q = Q(batch=None) | Q(batch='') |         q = Q(batch=None) | Q(batch='') | ||||||
|  |  | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.exclude(q) |             return queryset.exclude(q) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.filter(q) |             return queryset.filter(q) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked') |     tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked') | ||||||
|  |  | ||||||
| @@ -446,55 +489,45 @@ class StockFilter(rest_filters.FilterSet): | |||||||
|         q_serial = Q(serial=None) | Q(serial='') |         q_serial = Q(serial=None) | Q(serial='') | ||||||
|  |  | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.exclude(q_batch & q_serial) |             return queryset.exclude(q_batch & q_serial) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.filter(q_batch & q_serial) |             return queryset.filter(q_batch & q_serial) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed') |     installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed') | ||||||
|  |  | ||||||
|     def filter_installed(self, queryset, name, value): |     def filter_installed(self, queryset, name, value): | ||||||
|         """Filter stock items by "belongs_to" field being empty.""" |         """Filter stock items by "belongs_to" field being empty.""" | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.exclude(belongs_to=None) |             return queryset.exclude(belongs_to=None) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.filter(belongs_to=None) |             return queryset.filter(belongs_to=None) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     sent_to_customer = rest_filters.BooleanFilter(label='Sent to customer', method='filter_sent_to_customer') |     sent_to_customer = rest_filters.BooleanFilter(label='Sent to customer', method='filter_sent_to_customer') | ||||||
|  |  | ||||||
|     def filter_sent_to_customer(self, queryset, name, value): |     def filter_sent_to_customer(self, queryset, name, value): | ||||||
|         """Filter by sent to customer.""" |         """Filter by sent to customer.""" | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.exclude(customer=None) |             return queryset.exclude(customer=None) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.filter(customer=None) |             return queryset.filter(customer=None) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     depleted = rest_filters.BooleanFilter(label='Depleted', method='filter_depleted') |     depleted = rest_filters.BooleanFilter(label='Depleted', method='filter_depleted') | ||||||
|  |  | ||||||
|     def filter_depleted(self, queryset, name, value): |     def filter_depleted(self, queryset, name, value): | ||||||
|         """Filter by depleted items.""" |         """Filter by depleted items.""" | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.filter(quantity__lte=0) |             return queryset.filter(quantity__lte=0) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.exclude(quantity__lte=0) |             return queryset.exclude(quantity__lte=0) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     has_purchase_price = rest_filters.BooleanFilter(label='Has purchase price', method='filter_has_purchase_price') |     has_purchase_price = rest_filters.BooleanFilter(label='Has purchase price', method='filter_has_purchase_price') | ||||||
|  |  | ||||||
|     def filter_has_purchase_price(self, queryset, name, value): |     def filter_has_purchase_price(self, queryset, name, value): | ||||||
|         """Filter by having a purchase price.""" |         """Filter by having a purchase price.""" | ||||||
|         if str2bool(value): |         if str2bool(value): | ||||||
|             queryset = queryset.exclude(purchase_price=None) |             return queryset.exclude(purchase_price=None) | ||||||
|         else: |         else: | ||||||
|             queryset = queryset.filter(purchase_price=None) |             return queryset.filter(purchase_price=None) | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     # Update date filters |     # Update date filters | ||||||
|     updated_before = rest_filters.DateFilter(label='Updated before', field_name='updated', lookup_expr='lte') |     updated_before = rest_filters.DateFilter(label='Updated before', field_name='updated', lookup_expr='lte') | ||||||
| @@ -778,6 +811,13 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | |||||||
|  |  | ||||||
|         queryset = StockSerializers.StockItemSerializer.annotate_queryset(queryset) |         queryset = StockSerializers.StockItemSerializer.annotate_queryset(queryset) | ||||||
|  |  | ||||||
|  |         # Also ensure that we pre-fecth all the related items | ||||||
|  |         queryset = queryset.prefetch_related( | ||||||
|  |             'part', | ||||||
|  |             'part__category', | ||||||
|  |             'location' | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         return queryset |         return queryset | ||||||
|  |  | ||||||
|     def filter_queryset(self, queryset): |     def filter_queryset(self, queryset): | ||||||
| @@ -786,50 +826,8 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | |||||||
|  |  | ||||||
|         queryset = super().filter_queryset(queryset) |         queryset = super().filter_queryset(queryset) | ||||||
|  |  | ||||||
|         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) |  | ||||||
|  |  | ||||||
|         sales_order = params.get('sales_order', None) |  | ||||||
|  |  | ||||||
|         if sales_order: |  | ||||||
|             queryset = queryset.filter(sales_order=sales_order) |  | ||||||
|  |  | ||||||
|         purchase_order = params.get('purchase_order', None) |  | ||||||
|  |  | ||||||
|         if purchase_order is not None: |  | ||||||
|             queryset = queryset.filter(purchase_order=purchase_order) |  | ||||||
|  |  | ||||||
|         # Filter stock items which are installed in another (specific) stock item |  | ||||||
|         installed_in = params.get('installed_in', None) |  | ||||||
|  |  | ||||||
|         if installed_in: |  | ||||||
|             # Note: The "installed_in" field is called "belongs_to" |  | ||||||
|             queryset = queryset.filter(belongs_to=installed_in) |  | ||||||
|  |  | ||||||
|         if common.settings.stock_expiry_enabled(): |         if common.settings.stock_expiry_enabled(): | ||||||
|  |  | ||||||
|             # Filter by 'expired' status |  | ||||||
|             expired = params.get('expired', None) |  | ||||||
|  |  | ||||||
|             if expired is not None: |  | ||||||
|                 expired = str2bool(expired) |  | ||||||
|  |  | ||||||
|                 if expired: |  | ||||||
|                     queryset = queryset.filter(StockItem.EXPIRED_FILTER) |  | ||||||
|                 else: |  | ||||||
|                     queryset = queryset.exclude(StockItem.EXPIRED_FILTER) |  | ||||||
|             # Filter by 'expiry date' |             # Filter by 'expiry date' | ||||||
|             expired_date_lte = params.get('expiry_date_lte', None) |             expired_date_lte = params.get('expiry_date_lte', None) | ||||||
|             if expired_date_lte is not None: |             if expired_date_lte is not None: | ||||||
| @@ -846,6 +844,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | |||||||
|                     queryset = queryset.filter(expiry_date__gte=date_gte) |                     queryset = queryset.filter(expiry_date__gte=date_gte) | ||||||
|                 except (ValueError, TypeError): |                 except (ValueError, TypeError): | ||||||
|                     pass |                     pass | ||||||
|  |  | ||||||
|             # Filter by 'stale' status |             # Filter by 'stale' status | ||||||
|             stale = params.get('stale', None) |             stale = params.get('stale', None) | ||||||
|  |  | ||||||
| @@ -865,12 +864,6 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | |||||||
|                     else: |                     else: | ||||||
|                         queryset = queryset.exclude(stale_filter) |                         queryset = queryset.exclude(stale_filter) | ||||||
|  |  | ||||||
|         # Filter by customer |  | ||||||
|         customer = params.get('customer', None) |  | ||||||
|  |  | ||||||
|         if customer: |  | ||||||
|             queryset = queryset.filter(customer=customer) |  | ||||||
|  |  | ||||||
|         # Exclude stock item tree |         # Exclude stock item tree | ||||||
|         exclude_tree = params.get('exclude_tree', None) |         exclude_tree = params.get('exclude_tree', None) | ||||||
|  |  | ||||||
| @@ -897,19 +890,6 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | |||||||
|             except Exception: |             except Exception: | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|         # Filter by 'allocated' parts? |  | ||||||
|         allocated = params.get('allocated', None) |  | ||||||
|  |  | ||||||
|         if allocated is not None: |  | ||||||
|             allocated = str2bool(allocated) |  | ||||||
|  |  | ||||||
|             if allocated: |  | ||||||
|                 # Filter StockItem with either build allocations or sales order allocations |  | ||||||
|                 queryset = queryset.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)) |  | ||||||
|             else: |  | ||||||
|                 # Filter StockItem without build allocations or sales order allocations |  | ||||||
|                 queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) |  | ||||||
|  |  | ||||||
|         # Exclude StockItems which are already allocated to a particular SalesOrder |         # Exclude StockItems which are already allocated to a particular SalesOrder | ||||||
|         exclude_so_allocation = params.get('exclude_so_allocation', None) |         exclude_so_allocation = params.get('exclude_so_allocation', None) | ||||||
|  |  | ||||||
| @@ -978,8 +958,9 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | |||||||
|         if loc_id is not None: |         if loc_id is not None: | ||||||
|  |  | ||||||
|             # Filter by 'null' location (i.e. top-level items) |             # Filter by 'null' location (i.e. top-level items) | ||||||
|             if isNull(loc_id) and not cascade: |             if isNull(loc_id): | ||||||
|                 queryset = queryset.filter(location=None) |                 if not cascade: | ||||||
|  |                     queryset = queryset.filter(location=None) | ||||||
|             else: |             else: | ||||||
|                 try: |                 try: | ||||||
|                     # If '?cascade=true' then include items which exist in sub-locations |                     # If '?cascade=true' then include items which exist in sub-locations | ||||||
| @@ -1015,55 +996,12 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): | |||||||
|             except (ValueError, BomItem.DoesNotExist): |             except (ValueError, BomItem.DoesNotExist): | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|         # Filter by StockItem status |  | ||||||
|         status = params.get('status', None) |  | ||||||
|  |  | ||||||
|         if status: |  | ||||||
|             queryset = queryset.filter(status=status) |  | ||||||
|  |  | ||||||
|         # Filter by supplier_part ID |  | ||||||
|         supplier_part_id = params.get('supplier_part', None) |  | ||||||
|  |  | ||||||
|         if supplier_part_id: |  | ||||||
|             queryset = queryset.filter(supplier_part=supplier_part_id) |  | ||||||
|  |  | ||||||
|         # Filter by company (either manufacturer or supplier) |         # Filter by company (either manufacturer or supplier) | ||||||
|         company = params.get('company', None) |         company = params.get('company', None) | ||||||
|  |  | ||||||
|         if company is not None: |         if company is not None: | ||||||
|             queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company)) |             queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company)) | ||||||
|  |  | ||||||
|         # Filter by supplier |  | ||||||
|         supplier = params.get('supplier', None) |  | ||||||
|  |  | ||||||
|         if supplier is not None: |  | ||||||
|             queryset = queryset.filter(supplier_part__supplier=supplier) |  | ||||||
|  |  | ||||||
|         # Filter by manufacturer |  | ||||||
|         manufacturer = params.get('manufacturer', None) |  | ||||||
|  |  | ||||||
|         if manufacturer is not None: |  | ||||||
|             queryset = queryset.filter(supplier_part__manufacturer_part__manufacturer=manufacturer) |  | ||||||
|  |  | ||||||
|         # Optionally, limit the maximum number of returned results |  | ||||||
|         max_results = params.get('max_results', None) |  | ||||||
|  |  | ||||||
|         if max_results is not None: |  | ||||||
|             try: |  | ||||||
|                 max_results = int(max_results) |  | ||||||
|  |  | ||||||
|                 if max_results > 0: |  | ||||||
|                     queryset = queryset[:max_results] |  | ||||||
|             except (ValueError): |  | ||||||
|                 pass |  | ||||||
|  |  | ||||||
|         # Also ensure that we pre-fecth all the related items |  | ||||||
|         queryset = queryset.prefetch_related( |  | ||||||
|             'part', |  | ||||||
|             'part__category', |  | ||||||
|             'location' |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         return queryset |         return queryset | ||||||
|  |  | ||||||
|     filter_backends = [ |     filter_backends = [ | ||||||
|   | |||||||
| @@ -306,6 +306,29 @@ class StockItemListTest(StockAPITestCase): | |||||||
|         # Return JSON-ified data |         # Return JSON-ified data | ||||||
|         return response.data |         return response.data | ||||||
|  |  | ||||||
|  |     def test_top_level_filtering(self): | ||||||
|  |         """Test filtering against "top level" stock location""" | ||||||
|  |  | ||||||
|  |         # No filters, should return *all* items | ||||||
|  |         response = self.get(self.list_url, {}, expected_code=200) | ||||||
|  |         self.assertEqual(len(response.data), StockItem.objects.count()) | ||||||
|  |  | ||||||
|  |         # Filter with "cascade=False" (but no location specified) | ||||||
|  |         # Should not result in any actual filtering | ||||||
|  |         response = self.get(self.list_url, {'cascade': False}, expected_code=200) | ||||||
|  |         self.assertEqual(len(response.data), StockItem.objects.count()) | ||||||
|  |  | ||||||
|  |         # Filter with "cascade=False" for the top-level location | ||||||
|  |         response = self.get(self.list_url, {'location': 'null', 'cascade': False}, expected_code=200) | ||||||
|  |         self.assertTrue(len(response.data) < StockItem.objects.count()) | ||||||
|  |  | ||||||
|  |         for result in response.data: | ||||||
|  |             self.assertIsNone(result['location']) | ||||||
|  |  | ||||||
|  |         # Filter with "cascade=True" | ||||||
|  |         response = self.get(self.list_url, {'location': 'null', 'cascade': True}, expected_code=200) | ||||||
|  |         self.assertEqual(len(response.data), StockItem.objects.count()) | ||||||
|  |  | ||||||
|     def test_get_stock_list(self): |     def test_get_stock_list(self): | ||||||
|         """List *all* StockItem objects.""" |         """List *all* StockItem objects.""" | ||||||
|         response = self.get_stock() |         response = self.get_stock() | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ addHeaderAction('latest-parts', '{% trans "Latest Parts" %}', 'fa-newspaper'); | |||||||
| loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { | loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { | ||||||
|     params: { |     params: { | ||||||
|         ordering: "-creation_date", |         ordering: "-creation_date", | ||||||
|         max_results: {% settings_value "PART_RECENT_COUNT" user=request.user %}, |         limit: {% settings_value "PART_RECENT_COUNT" user=request.user %}, | ||||||
|     }, |     }, | ||||||
|     name: 'latest_parts', |     name: 'latest_parts', | ||||||
| }); | }); | ||||||
| @@ -147,7 +147,7 @@ loadStockTable($('#table-recently-updated-stock'), { | |||||||
|     params: { |     params: { | ||||||
|         part_detail: true, |         part_detail: true, | ||||||
|         ordering: "-updated", |         ordering: "-updated", | ||||||
|         max_results: {% settings_value "STOCK_RECENT_COUNT" user=request.user %}, |         limit: {% settings_value "STOCK_RECENT_COUNT" user=request.user %}, | ||||||
|     }, |     }, | ||||||
|     name: 'recently-updated-stock', |     name: 'recently-updated-stock', | ||||||
|     grouping: false, |     grouping: false, | ||||||
|   | |||||||
| @@ -2742,7 +2742,7 @@ function loadInstalledInTable(table, options) { | |||||||
|     table.inventreeTable({ |     table.inventreeTable({ | ||||||
|         url: '{% url "api-stock-list" %}', |         url: '{% url "api-stock-list" %}', | ||||||
|         queryParams: { |         queryParams: { | ||||||
|             installed_in: options.stock_item, |             belongs_to: options.stock_item, | ||||||
|             part_detail: true, |             part_detail: true, | ||||||
|         }, |         }, | ||||||
|         formatNoMatches: function() { |         formatNoMatches: function() { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user