From 376428b80b333a345e2c881fa4f2f67aa70c92a7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 17:02:45 +1000 Subject: [PATCH 1/8] Add regex IPN filter for Part API --- InvenTree/part/api.py | 132 ++++++++++++------ .../migrations/0070_alter_part_variant_of.py | 19 +++ InvenTree/part/models.py | 1 - 3 files changed, 109 insertions(+), 43 deletions(-) create mode 100644 InvenTree/part/migrations/0070_alter_part_variant_of.py diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 337201c95f..3199e96d63 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -5,9 +5,10 @@ Provides a JSON API for the Part app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django_filters.rest_framework import DjangoFilterBackend +from django.conf.urls import url, include +from django.urls import reverse from django.http import JsonResponse -from django.db.models import Q, F, Count, Min, Max, Avg +from django.db.models import Q, F, Count, Min, Max, Avg, query from django.utils.translation import ugettext_lazy as _ from rest_framework import status @@ -15,12 +16,13 @@ from rest_framework.response import Response from rest_framework import filters, serializers from rest_framework import generics +from django_filters.rest_framework import DjangoFilterBackend +from django_filters import rest_framework as rest_filters + from djmoney.money import Money from djmoney.contrib.exchange.models import convert_money from djmoney.contrib.exchange.exceptions import MissingRate -from django.conf.urls import url, include -from django.urls import reverse from .models import Part, PartCategory, BomItem from .models import PartParameter, PartParameterTemplate @@ -405,6 +407,74 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): return response +class PartFilter(rest_filters.FilterSet): + """ + Custom filters for the PartList endpoint. + Uses the django_filters extension framework + """ + + # Exact match for IPN + ipn = rest_filters.CharFilter( + label='Filter by exact IPN (internal part number)', + field_name='IPN', + lookup_expr="iexact" + ) + + # Regex match for IPN + ipn_regex = rest_filters.CharFilter( + field_name='IPN', lookup_expr='iregex' + ) + + # low_stock filter + low_stock = rest_filters.BooleanFilter(method='filter_low_stock') + + def filter_low_stock(self, queryset, name, value): + """ + Filter by "low stock" status + """ + + value = str2bool(value) + + if value: + # 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 + + # has_stock filter + has_stock = rest_filters.BooleanFilter(method='filter_has_stock') + + def filter_has_stock(self, queryset, name, value): + + value = str2bool(value) + + if value: + queryset = queryset.filter(Q(in_stock__gt=0)) + else: + queryset = queryset.filter(Q(in_stock__lte=0)) + + return queryset + + is_template = rest_filters.CharFilter() + + assembly = rest_filters.BooleanFilter() + + component = rest_filters.BooleanFilter() + + trackable = rest_filters.BooleanFilter() + + purchaseable = rest_filters.BooleanFilter() + + salable = rest_filters.BooleanFilter() + + active = rest_filters.BooleanFilter() + + class PartList(generics.ListCreateAPIView): """ API endpoint for accessing a list of Part objects @@ -427,8 +497,8 @@ class PartList(generics.ListCreateAPIView): """ serializer_class = part_serializers.PartSerializer - queryset = Part.objects.all() + filterset_class = PartFilter starred_parts = None @@ -541,6 +611,10 @@ class PartList(generics.ListCreateAPIView): params = self.request.query_params + # Annotate calculated data to the queryset + # (This will be used for further filtering) + queryset = part_serializers.PartSerializer.annotate_queryset(queryset) + queryset = super().filter_queryset(queryset) # Filter by "uses" query - Limit to parts which use the provided part @@ -578,6 +652,17 @@ class PartList(generics.ListCreateAPIView): else: queryset = queryset.filter(IPN='') + # Filter by IPN + """ + ipn = params.get('ipn', None) + + if ipn is not None: + + queryset = queryset.filter(IPN=ipn) + + """ + # Filter by IPN (regex support) + # Filter by whether the BOM has been validated (or not) bom_valid = params.get('bom_valid', None) @@ -643,36 +728,6 @@ class PartList(generics.ListCreateAPIView): 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 = 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 = 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'))) - # Filer by 'depleted_stock' status -> has no stock and stock items depleted_stock = params.get('depleted_stock', None) @@ -722,14 +777,7 @@ class PartList(generics.ListCreateAPIView): ] filter_fields = [ - 'is_template', 'variant_of', - 'assembly', - 'component', - 'trackable', - 'purchaseable', - 'salable', - 'active', ] ordering_fields = [ diff --git a/InvenTree/part/migrations/0070_alter_part_variant_of.py b/InvenTree/part/migrations/0070_alter_part_variant_of.py new file mode 100644 index 0000000000..a2b2f7ec18 --- /dev/null +++ b/InvenTree/part/migrations/0070_alter_part_variant_of.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.4 on 2021-07-08 07:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0069_auto_20210701_0509'), + ] + + operations = [ + migrations.AlterField( + model_name='part', + name='variant_of', + field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'is_template': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.part', verbose_name='Variant Of'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b69177c05b..cc533177d9 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -692,7 +692,6 @@ class Part(MPTTModel): null=True, blank=True, limit_choices_to={ 'is_template': True, - 'active': True, }, on_delete=models.SET_NULL, help_text=_('Is this part a variant of another part?'), From ba0a13443fd1a812cdcc69941be72c819e3aff12 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 17:02:55 +1000 Subject: [PATCH 2/8] PEP fixes --- 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 3199e96d63..2ee82bf20b 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from django.conf.urls import url, include from django.urls import reverse from django.http import JsonResponse -from django.db.models import Q, F, Count, Min, Max, Avg, query +from django.db.models import Q, F, Count, Min, Max, Avg from django.utils.translation import ugettext_lazy as _ from rest_framework import status From a8a21f7c9d14fc246a09d36ac560ef415f8a48d8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 17:16:02 +1000 Subject: [PATCH 3/8] Transition "has IPN" filter to django-filters approach --- InvenTree/part/api.py | 45 +++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 2ee82bf20b..2fd5babe0a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from django.conf.urls import url, include from django.urls import reverse from django.http import JsonResponse -from django.db.models import Q, F, Count, Min, Max, Avg +from django.db.models import Q, F, Count, Min, Max, Avg, query from django.utils.translation import ugettext_lazy as _ from rest_framework import status @@ -413,6 +413,18 @@ class PartFilter(rest_filters.FilterSet): Uses the django_filters extension framework """ + # Filter by parts which have (or not) an IPN value + has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn') + + def filter_has_ipn(self, queryset, name, value): + + value = str2bool(value) + + if value: + queryset = queryset.exclude(IPN='') + else: + queryset = queryset.filter(IPN='') + # Exact match for IPN ipn = rest_filters.CharFilter( label='Filter by exact IPN (internal part number)', @@ -422,11 +434,12 @@ class PartFilter(rest_filters.FilterSet): # Regex match for IPN ipn_regex = rest_filters.CharFilter( + label='Filter by regex on IPN (internal part number) field', field_name='IPN', lookup_expr='iregex' ) # low_stock filter - low_stock = rest_filters.BooleanFilter(method='filter_low_stock') + low_stock = rest_filters.BooleanFilter(label='Low stock', method='filter_low_stock') def filter_low_stock(self, queryset, name, value): """ @@ -447,7 +460,7 @@ class PartFilter(rest_filters.FilterSet): return queryset # has_stock filter - has_stock = rest_filters.BooleanFilter(method='filter_has_stock') + has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock') def filter_has_stock(self, queryset, name, value): @@ -460,7 +473,7 @@ class PartFilter(rest_filters.FilterSet): return queryset - is_template = rest_filters.CharFilter() + is_template = rest_filters.BooleanFilter() assembly = rest_filters.BooleanFilter() @@ -539,7 +552,7 @@ class PartList(generics.ListCreateAPIView): # Do we wish to include PartCategory detail? if str2bool(request.query_params.get('category_detail', False)): - # Work out which part categorie we need to query + # Work out which part categories we need to query category_ids = set() for part in data: @@ -641,28 +654,6 @@ class PartList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): pass - # Filter by whether the part has an IPN (internal part number) defined - has_ipn = params.get('has_ipn', None) - - if has_ipn is not None: - has_ipn = str2bool(has_ipn) - - if has_ipn: - queryset = queryset.exclude(IPN='') - else: - queryset = queryset.filter(IPN='') - - # Filter by IPN - """ - ipn = params.get('ipn', None) - - if ipn is not None: - - queryset = queryset.filter(IPN=ipn) - - """ - # Filter by IPN (regex support) - # Filter by whether the BOM has been validated (or not) bom_valid = params.get('bom_valid', None) From 81010994e75e55bf6f663c9a678a356fe682b299 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 17:26:55 +1000 Subject: [PATCH 4/8] Adds regex filtering for "batch" code on StockItem --- InvenTree/stock/api.py | 48 +++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index cf13811c8c..15dd6f6994 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -14,8 +14,8 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import generics, filters, permissions -from django_filters.rest_framework import FilterSet, DjangoFilterBackend -from django_filters import NumberFilter +from django_filters.rest_framework import DjangoFilterBackend +from django_filters import rest_framework as rest_filters from .models import StockLocation, StockItem from .models import StockItemTracking @@ -110,20 +110,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): return super().update(request, *args, **kwargs) -class StockFilter(FilterSet): - """ FilterSet for advanced stock filtering. - - Allows greater-than / less-than filtering for stock quantity - """ - - min_stock = NumberFilter(name='quantity', lookup_expr='gte') - max_stock = NumberFilter(name='quantity', lookup_expr='lte') - - class Meta: - model = StockItem - fields = ['quantity', 'part', 'location'] - - class StockAdjust(APIView): """ A generic class for handling stocktake actions. @@ -356,6 +342,22 @@ class StockLocationList(generics.ListCreateAPIView): ] +class StockFilter(rest_filters.FilterSet): + """ FilterSet for advanced stock filtering. + + Allows greater-than / less-than filtering for stock quantity + """ + + 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') + + batch = rest_filters.CharFilter(label="Batch code filter (case insensitive)", lookup_expr='iexact') + + batch_regex = rest_filters.CharFilter(label="Batch code filter (regex)", field_name='batch', lookup_expr='iregex') + + is_building = rest_filters.BooleanFilter(label="In production") + + class StockList(generics.ListCreateAPIView): """ API endpoint for list view of Stock objects @@ -372,6 +374,7 @@ class StockList(generics.ListCreateAPIView): serializer_class = StockItemSerializer queryset = StockItem.objects.all() + filterset_class = StockFilter def create(self, request, *args, **kwargs): """ @@ -542,24 +545,11 @@ class StockList(generics.ListCreateAPIView): if belongs_to: queryset = queryset.filter(belongs_to=belongs_to) - # Filter by batch code - batch = params.get('batch', None) - - if batch is not None: - queryset = queryset.filter(batch=batch) - build = params.get('build', None) if build: queryset = queryset.filter(build=build) - # Filter by 'is building' status - is_building = params.get('is_building', None) - - if is_building: - is_building = str2bool(is_building) - queryset = queryset.filter(is_building=is_building) - sales_order = params.get('sales_order', None) if sales_order: From f0e7826fdc53e9798175e4514291720b34c45131 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 17:44:52 +1000 Subject: [PATCH 5/8] Adds some more API filters for the StockItem endpoint --- InvenTree/part/api.py | 2 +- InvenTree/stock/api.py | 113 +++++++++++++++++------------------------ 2 files changed, 47 insertions(+), 68 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 2fd5babe0a..7303f06787 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from django.conf.urls import url, include from django.urls import reverse from django.http import JsonResponse -from django.db.models import Q, F, Count, Min, Max, Avg, query +from django.db.models import Q, F, Count, Min, Max, Avg from django.utils.translation import ugettext_lazy as _ from rest_framework import status diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 15dd6f6994..fc18ee7731 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -343,20 +343,62 @@ class StockLocationList(generics.ListCreateAPIView): class StockFilter(rest_filters.FilterSet): - """ FilterSet for advanced stock filtering. - - Allows greater-than / less-than filtering for stock quantity """ + FilterSet for StockItem LIST API + """ + + # Part name filters + name = rest_filters.CharFilter(label='Part name (case insensitive)', field_name='part__name', lookup_expr='iexact') + name_regex = rest_filters.CharFilter(label='Part name (regex)', field_name='part__name', lookup_expr='iregex') + + # Part IPN filters + ipn = rest_filters.CharFilter(label='Part IPN (case insensitive)', field_name='part__IPN', lookup_expr='iexact') + ipn_regex = rest_filters.CharFilter(label='Part IPN (regex)', field_name='part__IPN', lookup_expr='iregex') + + # Part attribute filters + assembly = rest_filters.BooleanFilter(label="Assembly", field_name='part__assembly') + active = rest_filters.BooleanFilter(label="Active", field_name='part__active') 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') + in_stock = rest_filters.BooleanFilter(label='In Stock', method='filter_in_stock') + + def filter_in_stock(self, queryset, name, value): + + value = str2bool(value) + + if value: + queryset = queryset.filter(StockItem.IN_STOCK_FILTER) + else: + queryset = queryset.exclude(StockItem.IN_STOCK_FILTER) + + return queryset + batch = rest_filters.CharFilter(label="Batch code filter (case insensitive)", lookup_expr='iexact') batch_regex = rest_filters.CharFilter(label="Batch code filter (regex)", field_name='batch', lookup_expr='iregex') is_building = rest_filters.BooleanFilter(label="In production") + # Serial number filtering + serial_gte = rest_filters.NumberFilter(label='Serial number GTE', field_name='serial', lookup_expr='gte') + serial_lte = rest_filters.NumberFilter(label='Serial number LTE', field_name='serial', lookup_expr='lte') + serial = rest_filters.NumberFilter(label='Serial number', field_name='serial', lookup_expr='exact') + + serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized') + + def filter_serialized(self, queryset, name, value): + + value = str2bool(value) + + if value: + queryset = queryset.exclude(serial=None) + else: + queryset = queryset.filter(serial=None) + + return queryset + class StockList(generics.ListCreateAPIView): """ API endpoint for list view of Stock objects @@ -629,50 +671,7 @@ class StockList(generics.ListCreateAPIView): else: queryset = queryset.filter(customer=None) - # Filter by "serialized" status? - serialized = params.get('serialized', None) - - if serialized is not None: - serialized = str2bool(serialized) - - if serialized: - queryset = queryset.exclude(serial=None) - else: - queryset = queryset.filter(serial=None) - - # Filter by serial number? - serial_number = params.get('serial', None) - - if serial_number is not None: - queryset = queryset.filter(serial=serial_number) - - # Filter by range of serial numbers? - serial_number_gte = params.get('serial_gte', None) - serial_number_lte = params.get('serial_lte', None) - - if serial_number_gte is not None or serial_number_lte is not None: - queryset = queryset.exclude(serial=None) - - if serial_number_gte is not None: - queryset = queryset.filter(serial__gte=serial_number_gte) - - if serial_number_lte is not None: - queryset = queryset.filter(serial__lte=serial_number_lte) - - # Filter by "in_stock" status - in_stock = params.get('in_stock', None) - - if in_stock is not None: - in_stock = str2bool(in_stock) - - if in_stock: - # Filter out parts which are not actually "in stock" - queryset = queryset.filter(StockItem.IN_STOCK_FILTER) - else: - # Only show parts which are not in stock - queryset = queryset.exclude(StockItem.IN_STOCK_FILTER) - - # Filter by 'allocated' patrs? + # Filter by 'allocated' parts? allocated = params.get('allocated', None) if allocated is not None: @@ -685,20 +684,6 @@ class StockList(generics.ListCreateAPIView): # Filter StockItem without build allocations or sales order allocations queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) - # Do we wish to filter by "active parts" - active = params.get('active', None) - - if active is not None: - active = str2bool(active) - queryset = queryset.filter(part__active=active) - - # Do we wish to filter by "assembly parts" - assembly = params.get('assembly', None) - - if assembly is not None: - assembly = str2bool(assembly) - queryset = queryset.filter(part__assembly=assembly) - # Filter by 'depleted' status depleted = params.get('depleted', None) @@ -710,12 +695,6 @@ class StockList(generics.ListCreateAPIView): else: queryset = queryset.exclude(quantity__lte=0) - # Filter by internal part number - ipn = params.get('IPN', None) - - if ipn is not None: - queryset = queryset.filter(part__IPN=ipn) - # Does the client wish to filter by the Part ID? part_id = params.get('part', None) From 79d90b1c4a375848b78d1a7667ebca4b3e318581 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 17:46:57 +1000 Subject: [PATCH 6/8] Additional filtering options for name and IPN fields --- InvenTree/stock/api.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index fc18ee7731..9fb3e356a5 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -349,10 +349,12 @@ class StockFilter(rest_filters.FilterSet): # Part name filters 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_regex = rest_filters.CharFilter(label='Part name (regex)', field_name='part__name', lookup_expr='iregex') # Part IPN filters ipn = rest_filters.CharFilter(label='Part IPN (case insensitive)', field_name='part__IPN', lookup_expr='iexact') + ipn_contains = rest_filters.CharFilter(label='Part IPN contains (case insensitive)', field_name='part__IPN', lookup_expr='icontains') ipn_regex = rest_filters.CharFilter(label='Part IPN (regex)', field_name='part__IPN', lookup_expr='iregex') # Part attribute filters @@ -864,9 +866,6 @@ class StockList(generics.ListCreateAPIView): filters.OrderingFilter, ] - filter_fields = [ - ] - ordering_fields = [ 'part__name', 'part__IPN', From c7f79a5a08d9d567765ad092276a9443e702cf94 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 19:23:01 +1000 Subject: [PATCH 7/8] Fixes --- InvenTree/stock/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 9fb3e356a5..20b4a2e36a 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -353,9 +353,9 @@ class StockFilter(rest_filters.FilterSet): name_regex = rest_filters.CharFilter(label='Part name (regex)', field_name='part__name', lookup_expr='iregex') # Part IPN filters - ipn = rest_filters.CharFilter(label='Part IPN (case insensitive)', field_name='part__IPN', lookup_expr='iexact') - ipn_contains = rest_filters.CharFilter(label='Part IPN contains (case insensitive)', field_name='part__IPN', lookup_expr='icontains') - ipn_regex = rest_filters.CharFilter(label='Part IPN (regex)', field_name='part__IPN', lookup_expr='iregex') + IPN = rest_filters.CharFilter(label='Part IPN (case insensitive)', field_name='part__IPN', lookup_expr='iexact') + IPN_contains = rest_filters.CharFilter(label='Part IPN contains (case insensitive)', field_name='part__IPN', lookup_expr='icontains') + IPN_regex = rest_filters.CharFilter(label='Part IPN (regex)', field_name='part__IPN', lookup_expr='iregex') # Part attribute filters assembly = rest_filters.BooleanFilter(label="Assembly", field_name='part__assembly') From a985e11aa8c88337fbf212f7addaba4592f317a9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 20:10:22 +1000 Subject: [PATCH 8/8] Simplify and add filters for StockList API endpoint --- InvenTree/stock/api.py | 138 ++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 85 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 20b4a2e36a..4a6e7111e8 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -368,9 +368,7 @@ class StockFilter(rest_filters.FilterSet): def filter_in_stock(self, queryset, name, value): - value = str2bool(value) - - if value: + if str2bool(value): queryset = queryset.filter(StockItem.IN_STOCK_FILTER) else: queryset = queryset.exclude(StockItem.IN_STOCK_FILTER) @@ -392,15 +390,64 @@ class StockFilter(rest_filters.FilterSet): def filter_serialized(self, queryset, name, value): - value = str2bool(value) - - if value: + if str2bool(value): queryset = queryset.exclude(serial=None) else: queryset = queryset.filter(serial=None) return queryset + installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed') + + def filter_installed(self, queryset, name, value): + """ + Filter stock items by "belongs_to" field being empty + """ + + if str2bool(value): + queryset = queryset.exclude(belongs_to=None) + else: + queryset = queryset.filter(belongs_to=None) + + return queryset + + sent_to_customer = rest_filters.BooleanFilter(label='Sent to customer', method='filter_sent_to_customer') + + def filter_sent_to_customer(self, queryset, name, value): + + if str2bool(value): + queryset = queryset.exclude(customer=None) + else: + queryset = queryset.filter(customer=None) + + return queryset + + depleted = rest_filters.BooleanFilter(label='Depleted', method='filter_depleted') + + def filter_depleted(self, queryset, name, value): + + if str2bool(value): + queryset = queryset.filter(quantity__lte=0) + else: + queryset = queryset.exclude(quantity__lte=0) + + return queryset + + has_purchase_price = rest_filters.BooleanFilter(label='Has purchase price', method='filter_has_purchase_price') + + def filter_has_purchase_price(self, queryset, name, value): + + if str2bool(value): + queryset = queryset.exclude(purcahse_price=None) + else: + queryset = queryset.filter(purchase_price=None) + + return queryset + + # Update date filters + updated_before = rest_filters.DateFilter(label='Updated before', field_name='updated', lookup_expr='lte') + updated_after = rest_filters.DateFilter(label='Updated after', field_name='updated', lookup_expr='gte') + class StockList(generics.ListCreateAPIView): """ API endpoint for list view of Stock objects @@ -611,19 +658,6 @@ class StockList(generics.ListCreateAPIView): # Note: The "installed_in" field is called "belongs_to" queryset = queryset.filter(belongs_to=installed_in) - # Filter stock items which are installed in another stock item - installed = params.get('installed', None) - - if installed is not None: - installed = str2bool(installed) - - if installed: - # Exclude items which are *not* installed in another item - queryset = queryset.exclude(belongs_to=None) - else: - # Exclude items which are instaled in another item - queryset = queryset.filter(belongs_to=None) - if common.settings.stock_expiry_enabled(): # Filter by 'expired' status @@ -662,17 +696,6 @@ class StockList(generics.ListCreateAPIView): if customer: queryset = queryset.filter(customer=customer) - # Filter if items have been sent to a customer (any customer) - sent_to_customer = params.get('sent_to_customer', None) - - if sent_to_customer is not None: - sent_to_customer = str2bool(sent_to_customer) - - if sent_to_customer: - queryset = queryset.exclude(customer=None) - else: - queryset = queryset.filter(customer=None) - # Filter by 'allocated' parts? allocated = params.get('allocated', None) @@ -686,17 +709,6 @@ class StockList(generics.ListCreateAPIView): # Filter StockItem without build allocations or sales order allocations queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) - # Filter by 'depleted' status - depleted = params.get('depleted', None) - - if depleted is not None: - depleted = str2bool(depleted) - - if depleted: - queryset = queryset.filter(quantity__lte=0) - else: - queryset = queryset.exclude(quantity__lte=0) - # Does the client wish to filter by the Part ID? part_id = params.get('part', None) @@ -795,50 +807,6 @@ class StockList(generics.ListCreateAPIView): if manufacturer is not None: queryset = queryset.filter(supplier_part__manufacturer_part__manufacturer=manufacturer) - """ - Filter by the 'last updated' date of the stock item(s): - - - updated_before=? : Filter stock items which were last updated *before* the provided date - - updated_after=? : Filter stock items which were last updated *after* the provided date - """ - - date_fmt = '%Y-%m-%d' # ISO format date string - - updated_before = params.get('updated_before', None) - updated_after = params.get('updated_after', None) - - if updated_before: - try: - updated_before = datetime.strptime(str(updated_before), date_fmt).date() - queryset = queryset.filter(updated__lte=updated_before) - - print("Before:", updated_before.isoformat()) - except (ValueError, TypeError): - # Account for improperly formatted date string - print("After before:", str(updated_before)) - pass - - if updated_after: - try: - updated_after = datetime.strptime(str(updated_after), date_fmt).date() - queryset = queryset.filter(updated__gte=updated_after) - print("After:", updated_after.isoformat()) - except (ValueError, TypeError): - # Account for improperly formatted date string - print("After error:", str(updated_after)) - pass - - # Filter stock items which have a purchase price set - has_purchase_price = params.get('has_purchase_price', None) - - if has_purchase_price is not None: - has_purchase_price = str2bool(has_purchase_price) - - if has_purchase_price: - queryset = queryset.exclude(purchase_price=None) - else: - queryset = queryset.filter(purchase_price=None) - # Optionally, limit the maximum number of returned results max_results = params.get('max_results', None)