mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Merge pull request #1773 from SchrodingersGat/ipn-filtering
API Filtering improvements
This commit is contained in:
		@@ -5,7 +5,8 @@ Provides a JSON API for the Part app
 | 
				
			|||||||
# -*- coding: utf-8 -*-
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
from __future__ import unicode_literals
 | 
					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.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
 | 
				
			||||||
from django.utils.translation import ugettext_lazy as _
 | 
					from django.utils.translation import ugettext_lazy as _
 | 
				
			||||||
@@ -15,12 +16,13 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework import filters, serializers
 | 
					from rest_framework import filters, serializers
 | 
				
			||||||
from rest_framework import generics
 | 
					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.money import Money
 | 
				
			||||||
from djmoney.contrib.exchange.models import convert_money
 | 
					from djmoney.contrib.exchange.models import convert_money
 | 
				
			||||||
from djmoney.contrib.exchange.exceptions import MissingRate
 | 
					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 Part, PartCategory, BomItem
 | 
				
			||||||
from .models import PartParameter, PartParameterTemplate
 | 
					from .models import PartParameter, PartParameterTemplate
 | 
				
			||||||
@@ -405,6 +407,87 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
 | 
				
			|||||||
        return response
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PartFilter(rest_filters.FilterSet):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Custom filters for the PartList endpoint.
 | 
				
			||||||
 | 
					    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)',
 | 
				
			||||||
 | 
					        field_name='IPN',
 | 
				
			||||||
 | 
					        lookup_expr="iexact"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # 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(label='Low stock', 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(label='Has stock', 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.BooleanFilter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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):
 | 
					class PartList(generics.ListCreateAPIView):
 | 
				
			||||||
    """ API endpoint for accessing a list of Part objects
 | 
					    """ API endpoint for accessing a list of Part objects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -427,8 +510,8 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    serializer_class = part_serializers.PartSerializer
 | 
					    serializer_class = part_serializers.PartSerializer
 | 
				
			||||||
 | 
					 | 
				
			||||||
    queryset = Part.objects.all()
 | 
					    queryset = Part.objects.all()
 | 
				
			||||||
 | 
					    filterset_class = PartFilter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    starred_parts = None
 | 
					    starred_parts = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -469,7 +552,7 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
        # Do we wish to include PartCategory detail?
 | 
					        # Do we wish to include PartCategory detail?
 | 
				
			||||||
        if str2bool(request.query_params.get('category_detail', False)):
 | 
					        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()
 | 
					            category_ids = set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for part in data:
 | 
					            for part in data:
 | 
				
			||||||
@@ -541,6 +624,10 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        params = self.request.query_params
 | 
					        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)
 | 
					        queryset = super().filter_queryset(queryset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Filter by "uses" query - Limit to parts which use the provided part
 | 
					        # Filter by "uses" query - Limit to parts which use the provided part
 | 
				
			||||||
@@ -567,17 +654,6 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
            except (ValueError, Part.DoesNotExist):
 | 
					            except (ValueError, Part.DoesNotExist):
 | 
				
			||||||
                pass
 | 
					                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 whether the BOM has been validated (or not)
 | 
					        # Filter by whether the BOM has been validated (or not)
 | 
				
			||||||
        bom_valid = params.get('bom_valid', None)
 | 
					        bom_valid = params.get('bom_valid', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -643,36 +719,6 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
                except (ValueError, PartCategory.DoesNotExist):
 | 
					                except (ValueError, PartCategory.DoesNotExist):
 | 
				
			||||||
                    pass
 | 
					                    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
 | 
					        # Filer by 'depleted_stock' status -> has no stock and stock items
 | 
				
			||||||
        depleted_stock = params.get('depleted_stock', None)
 | 
					        depleted_stock = params.get('depleted_stock', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -722,14 +768,7 @@ class PartList(generics.ListCreateAPIView):
 | 
				
			|||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    filter_fields = [
 | 
					    filter_fields = [
 | 
				
			||||||
        'is_template',
 | 
					 | 
				
			||||||
        'variant_of',
 | 
					        'variant_of',
 | 
				
			||||||
        'assembly',
 | 
					 | 
				
			||||||
        'component',
 | 
					 | 
				
			||||||
        'trackable',
 | 
					 | 
				
			||||||
        'purchaseable',
 | 
					 | 
				
			||||||
        'salable',
 | 
					 | 
				
			||||||
        'active',
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ordering_fields = [
 | 
					    ordering_fields = [
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								InvenTree/part/migrations/0070_alter_part_variant_of.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								InvenTree/part/migrations/0070_alter_part_variant_of.py
									
									
									
									
									
										Normal file
									
								
							@@ -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'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -692,7 +692,6 @@ class Part(MPTTModel):
 | 
				
			|||||||
        null=True, blank=True,
 | 
					        null=True, blank=True,
 | 
				
			||||||
        limit_choices_to={
 | 
					        limit_choices_to={
 | 
				
			||||||
            'is_template': True,
 | 
					            'is_template': True,
 | 
				
			||||||
            'active': True,
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        on_delete=models.SET_NULL,
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
        help_text=_('Is this part a variant of another part?'),
 | 
					        help_text=_('Is this part a variant of another part?'),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,8 +14,8 @@ from rest_framework.views import APIView
 | 
				
			|||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework import generics, filters, permissions
 | 
					from rest_framework import generics, filters, permissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import FilterSet, DjangoFilterBackend
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
from django_filters import NumberFilter
 | 
					from django_filters import rest_framework as rest_filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import StockLocation, StockItem
 | 
					from .models import StockLocation, StockItem
 | 
				
			||||||
from .models import StockItemTracking
 | 
					from .models import StockItemTracking
 | 
				
			||||||
@@ -110,20 +110,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
 | 
				
			|||||||
        return super().update(request, *args, **kwargs)
 | 
					        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):
 | 
					class StockAdjust(APIView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    A generic class for handling stocktake actions.
 | 
					    A generic class for handling stocktake actions.
 | 
				
			||||||
@@ -356,6 +342,113 @@ class StockLocationList(generics.ListCreateAPIView):
 | 
				
			|||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockFilter(rest_filters.FilterSet):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    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_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
 | 
				
			||||||
 | 
					    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):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if str2bool(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):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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):
 | 
					class StockList(generics.ListCreateAPIView):
 | 
				
			||||||
    """ API endpoint for list view of Stock objects
 | 
					    """ API endpoint for list view of Stock objects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -372,6 +465,7 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    serializer_class = StockItemSerializer
 | 
					    serializer_class = StockItemSerializer
 | 
				
			||||||
    queryset = StockItem.objects.all()
 | 
					    queryset = StockItem.objects.all()
 | 
				
			||||||
 | 
					    filterset_class = StockFilter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, request, *args, **kwargs):
 | 
					    def create(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -542,24 +636,11 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
        if belongs_to:
 | 
					        if belongs_to:
 | 
				
			||||||
            queryset = queryset.filter(belongs_to=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)
 | 
					        build = params.get('build', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if build:
 | 
					        if build:
 | 
				
			||||||
            queryset = queryset.filter(build=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)
 | 
					        sales_order = params.get('sales_order', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if sales_order:
 | 
					        if sales_order:
 | 
				
			||||||
@@ -577,19 +658,6 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
            # Note: The "installed_in" field is called "belongs_to"
 | 
					            # Note: The "installed_in" field is called "belongs_to"
 | 
				
			||||||
            queryset = queryset.filter(belongs_to=installed_in)
 | 
					            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():
 | 
					        if common.settings.stock_expiry_enabled():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Filter by 'expired' status
 | 
					            # Filter by 'expired' status
 | 
				
			||||||
@@ -628,61 +696,7 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
        if customer:
 | 
					        if customer:
 | 
				
			||||||
            queryset = queryset.filter(customer=customer)
 | 
					            queryset = queryset.filter(customer=customer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Filter if items have been sent to a customer (any customer)
 | 
					        # Filter by 'allocated' parts?
 | 
				
			||||||
        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 "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?
 | 
					 | 
				
			||||||
        allocated = params.get('allocated', None)
 | 
					        allocated = params.get('allocated', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if allocated is not None:
 | 
					        if allocated is not None:
 | 
				
			||||||
@@ -695,37 +709,6 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
                # Filter StockItem without build allocations or sales order allocations
 | 
					                # Filter StockItem without build allocations or sales order allocations
 | 
				
			||||||
                queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True))
 | 
					                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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if depleted is not None:
 | 
					 | 
				
			||||||
            depleted = str2bool(depleted)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if depleted:
 | 
					 | 
				
			||||||
                queryset = queryset.filter(quantity__lte=0)
 | 
					 | 
				
			||||||
            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?
 | 
					        # Does the client wish to filter by the Part ID?
 | 
				
			||||||
        part_id = params.get('part', None)
 | 
					        part_id = params.get('part', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -824,50 +807,6 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
        if manufacturer is not None:
 | 
					        if manufacturer is not None:
 | 
				
			||||||
            queryset = queryset.filter(supplier_part__manufacturer_part__manufacturer=manufacturer)
 | 
					            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
 | 
					        # Optionally, limit the maximum number of returned results
 | 
				
			||||||
        max_results = params.get('max_results', None)
 | 
					        max_results = params.get('max_results', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -895,9 +834,6 @@ class StockList(generics.ListCreateAPIView):
 | 
				
			|||||||
        filters.OrderingFilter,
 | 
					        filters.OrderingFilter,
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    filter_fields = [
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ordering_fields = [
 | 
					    ordering_fields = [
 | 
				
			||||||
        'part__name',
 | 
					        'part__name',
 | 
				
			||||||
        'part__IPN',
 | 
					        'part__IPN',
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user