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?'),