"""
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.http import JsonResponse
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 rest_framework.response import Response
from rest_framework import filters, serializers
from rest_framework import generics

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
from .models import PartAttachment, PartTestTemplate
from .models import PartSellPriceBreak, PartInternalPriceBreak
from .models import PartCategoryParameterTemplate

from common.models import InvenTreeSetting
from build.models import Build

from . import serializers as part_serializers

from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin

from InvenTree.status_codes import BuildStatus


class PartCategoryTree(TreeSerializer):

    title = _("Parts")
    model = PartCategory

    queryset = PartCategory.objects.all()

    @property
    def root_url(self):
        return reverse('part-index')

    def get_items(self):
        return PartCategory.objects.all().prefetch_related('parts', 'children')


class CategoryList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of PartCategory objects.

    - GET: Return a list of PartCategory objects
    - POST: Create a new PartCategory object
    """

    queryset = PartCategory.objects.all()
    serializer_class = part_serializers.CategorySerializer

    def filter_queryset(self, queryset):
        """
        Custom filtering:
        - Allow filtering by "null" parent to retrieve top-level part categories
        """

        queryset = super().filter_queryset(queryset)

        params = self.request.query_params

        cat_id = params.get('parent', None)

        cascade = str2bool(params.get('cascade', False))

        # Do not filter by category
        if cat_id is None:
            pass
        # Look for top-level categories
        elif isNull(cat_id):

            if not cascade:
                queryset = queryset.filter(parent=None)

        else:
            try:
                category = PartCategory.objects.get(pk=cat_id)

                if cascade:
                    parents = category.get_descendants(include_self=True)
                    parent_ids = [p.id for p in parents]

                    queryset = queryset.filter(parent__in=parent_ids)
                else:
                    queryset = queryset.filter(parent=category)

            except (ValueError, PartCategory.DoesNotExist):
                pass

        return queryset

    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]

    filter_fields = [
    ]

    ordering_fields = [
        'name',
    ]

    ordering = 'name'

    search_fields = [
        'name',
        'description',
    ]


class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API endpoint for detail view of a single PartCategory object """
    serializer_class = part_serializers.CategorySerializer
    queryset = PartCategory.objects.all()


class CategoryParameters(generics.ListAPIView):
    """ API endpoint for accessing a list of PartCategoryParameterTemplate objects.

    - GET: Return a list of PartCategoryParameterTemplate objects
    """

    queryset = PartCategoryParameterTemplate.objects.all()
    serializer_class = part_serializers.CategoryParameterTemplateSerializer

    def get_queryset(self):
        """
        Custom filtering:
        - Allow filtering by "null" parent to retrieve all categories parameter templates
        - Allow filtering by category
        - Allow traversing all parent categories
        """

        try:
            cat_id = int(self.kwargs.get('pk', None))
        except TypeError:
            cat_id = None
        fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true'))

        queryset = super().get_queryset()

        if isinstance(cat_id, int):

            try:
                category = PartCategory.objects.get(pk=cat_id)
            except PartCategory.DoesNotExist:
                # Return empty queryset
                return PartCategoryParameterTemplate.objects.none()

            category_list = [cat_id]

            if fetch_parent:
                parent_categories = category.get_ancestors()
                for parent in parent_categories:
                    category_list.append(parent.pk)

            queryset = queryset.filter(category__in=category_list)

        return queryset


class PartSalePriceList(generics.ListCreateAPIView):
    """
    API endpoint for list view of PartSalePriceBreak model
    """

    queryset = PartSellPriceBreak.objects.all()
    serializer_class = part_serializers.PartSalePriceSerializer

    filter_backends = [
        DjangoFilterBackend
    ]

    filter_fields = [
        'part',
    ]


class PartInternalPriceList(generics.ListCreateAPIView):
    """
    API endpoint for list view of PartInternalPriceBreak model
    """

    queryset = PartInternalPriceBreak.objects.all()
    serializer_class = part_serializers.PartInternalPriceSerializer
    permission_required = 'roles.sales_order.show'

    filter_backends = [
        DjangoFilterBackend
    ]

    filter_fields = [
        'part',
    ]


class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
    """
    API endpoint for listing (and creating) a PartAttachment (file upload).
    """

    queryset = PartAttachment.objects.all()
    serializer_class = part_serializers.PartAttachmentSerializer

    filter_backends = [
        DjangoFilterBackend,
    ]

    filter_fields = [
        'part',
    ]


class PartTestTemplateList(generics.ListCreateAPIView):
    """
    API endpoint for listing (and creating) a PartTestTemplate.
    """

    queryset = PartTestTemplate.objects.all()
    serializer_class = part_serializers.PartTestTemplateSerializer

    def filter_queryset(self, queryset):
        """
        Filter the test list queryset.

        If filtering by 'part', we include results for any parts "above" the specified part.
        """

        queryset = super().filter_queryset(queryset)

        params = self.request.query_params

        part = params.get('part', None)

        # Filter by part
        if part:
            try:
                part = Part.objects.get(pk=part)
                queryset = queryset.filter(part__in=part.get_ancestors(include_self=True))
            except (ValueError, Part.DoesNotExist):
                pass

        # Filter by 'required' status
        required = params.get('required', None)

        if required is not None:
            queryset = queryset.filter(required=required)

        return queryset

    filter_backends = [
        DjangoFilterBackend,
        filters.OrderingFilter,
        filters.SearchFilter,
    ]


class PartThumbs(generics.ListAPIView):
    """
    API endpoint for retrieving information on available Part thumbnails
    """

    queryset = Part.objects.all()
    serializer_class = part_serializers.PartThumbSerializer

    def get_queryset(self):

        queryset = super().get_queryset()

        # Get all Parts which have an associated image
        queryset = queryset.exclude(image='')

        return queryset

    def list(self, request, *args, **kwargs):
        """
        Serialize the available Part images.
        - Images may be used for multiple parts!
        """

        queryset = self.get_queryset()

        # TODO - We should return the thumbnails here, not the full image!

        # Return the most popular parts first
        data = queryset.values(
            'image',
        ).annotate(count=Count('image')).order_by('-count')

        return Response(data)


class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
    """ API endpoint for updating Part thumbnails"""

    queryset = Part.objects.all()
    serializer_class = part_serializers.PartThumbSerializerUpdate

    filter_backends = [
        DjangoFilterBackend
    ]


class PartDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API endpoint for detail view of a single Part object """

    queryset = Part.objects.all()
    serializer_class = part_serializers.PartSerializer

    starred_parts = None

    def get_queryset(self, *args, **kwargs):
        queryset = super().get_queryset(*args, **kwargs)

        queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
        queryset = part_serializers.PartSerializer.annotate_queryset(queryset)

        return queryset

    def get_serializer(self, *args, **kwargs):

        try:
            kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', False))
        except AttributeError:
            pass

        # Ensure the request context is passed through
        kwargs['context'] = self.get_serializer_context()

        # Pass a list of "starred" parts fo the current user to the serializer
        # We do this to reduce the number of database queries required!
        if self.starred_parts is None and self.request is not None:
            self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]

        kwargs['starred_parts'] = self.starred_parts

        return self.serializer_class(*args, **kwargs)

    def destroy(self, request, *args, **kwargs):
        # Retrieve part
        part = Part.objects.get(pk=int(kwargs['pk']))
        # Check if inactive
        if not part.active:
            # Delete
            return super(PartDetail, self).destroy(request, *args, **kwargs)
        else:
            # Return 405 error
            message = f'Part \'{part.name}\' (pk = {part.pk}) is active: cannot delete'
            return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data=message)

    def update(self, request, *args, **kwargs):
        """
        Custom update functionality for Part instance.

        - If the 'starred' field is provided, update the 'starred' status against current user
        """

        if 'starred' in request.data:
            starred = str2bool(request.data.get('starred', None))

            self.get_object().setStarred(request.user, starred)

        response = super().update(request, *args, **kwargs)

        return response


class PartList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of Part objects

    - GET: Return list of objects
    - POST: Create a new Part object

    The Part object list can be filtered by:
        - category: Filter by PartCategory reference
        - cascade: If true, include parts from sub-categories
        - starred: Is the part "starred" by the current user?
        - is_template: Is the part a template part?
        - variant_of: Filter by variant_of Part reference
        - assembly: Filter by assembly field
        - component: Filter by component field
        - trackable: Filter by trackable field
        - purchaseable: Filter by purcahseable field
        - salable: Filter by salable field
        - active: Filter by active field
        - ancestor: Filter parts by 'ancestor' (template / variant tree)
    """

    serializer_class = part_serializers.PartSerializer

    queryset = Part.objects.all()

    starred_parts = None

    def get_serializer(self, *args, **kwargs):

        # Ensure the request context is passed through
        kwargs['context'] = self.get_serializer_context()

        # Pass a list of "starred" parts fo the current user to the serializer
        # We do this to reduce the number of database queries required!
        if self.starred_parts is None and self.request is not None:
            self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]

        kwargs['starred_parts'] = self.starred_parts

        return self.serializer_class(*args, **kwargs)

    def list(self, request, *args, **kwargs):
        """
        Overide the 'list' method, as the PartCategory objects are
        very expensive to serialize!

        So we will serialize them first, and keep them in memory,
        so that they do not have to be serialized multiple times...
        """

        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)

        if page is not None:
            serializer = self.get_serializer(page, many=True)
        else:
            serializer = self.get_serializer(queryset, many=True)

        data = serializer.data

        # 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
            category_ids = set()

            for part in data:
                cat_id = part['category']

                if cat_id is not None:
                    category_ids.add(cat_id)

            # Fetch only the required PartCategory objects from the database
            categories = PartCategory.objects.filter(pk__in=category_ids).prefetch_related(
                'parts',
                'parent',
                'children',
            )

            category_map = {}

            # Serialize each PartCategory object
            for category in categories:
                category_map[category.pk] = part_serializers.CategorySerializer(category).data

            for part in data:
                cat_id = part['category']

                if cat_id is not None and cat_id in category_map.keys():
                    detail = category_map[cat_id]
                else:
                    detail = None

                part['category_detail'] = detail

        """
        Determine the response type based on the request.
        a) For HTTP requests (e.g. via the browseable API) return a DRF response
        b) For AJAX requests, simply return a JSON rendered response.
        """
        if page is not None:
            return self.get_paginated_response(data)
        elif request.is_ajax():
            return JsonResponse(data, safe=False)
        else:
            return Response(data)

    def perform_create(self, serializer):
        """
        We wish to save the user who created this part!

        Note: Implementation copied from DRF class CreateModelMixin
        """

        part = serializer.save()
        part.creation_user = self.request.user
        part.save()

    def get_queryset(self, *args, **kwargs):

        queryset = super().get_queryset(*args, **kwargs)

        queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
        queryset = part_serializers.PartSerializer.annotate_queryset(queryset)

        return queryset

    def filter_queryset(self, queryset):
        """
        Perform custom filtering of the queryset.
        We overide the DRF filter_fields here because
        """

        params = self.request.query_params

        queryset = super().filter_queryset(queryset)

        # Filter by "uses" query - Limit to parts which use the provided part
        uses = params.get('uses', None)

        if uses:
            try:
                uses = Part.objects.get(pk=uses)

                queryset = queryset.filter(uses.get_used_in_filter())

            except (ValueError, Part.DoesNotExist):
                pass

        # Filter by 'ancestor'?
        ancestor = params.get('ancestor', None)

        if ancestor is not None:
            # If an 'ancestor' part is provided, filter to match only children
            try:
                ancestor = Part.objects.get(pk=ancestor)
                descendants = ancestor.get_descendants(include_self=False)
                queryset = queryset.filter(pk__in=[d.pk for d in descendants])
            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 whether the BOM has been validated (or not)
        bom_valid = params.get('bom_valid', None)

        # TODO: Querying bom_valid status may be quite expensive
        # TODO: (It needs to be profiled!)
        # TODO: It might be worth caching the bom_valid status to a database column

        if bom_valid is not None:

            bom_valid = str2bool(bom_valid)

            # Limit queryset to active assemblies
            queryset = queryset.filter(active=True, assembly=True)

            pks = []

            for part in queryset:
                if part.is_bom_valid() == bom_valid:
                    pks.append(part.pk)

            queryset = queryset.filter(pk__in=pks)

        # Filter by 'starred' parts?
        starred = params.get('starred', None)

        if starred is not None:
            starred = str2bool(starred)
            starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]

            if starred:
                queryset = queryset.filter(pk__in=starred_parts)
            else:
                queryset = queryset.exclude(pk__in=starred_parts)

        # Cascade? (Default = True)
        cascade = str2bool(params.get('cascade', True))

        # Does the user wish to filter by category?
        cat_id = params.get('category', None)

        if cat_id is None:
            # No category filtering if category is not specified
            pass

        else:
            # Category has been specified!
            if isNull(cat_id):
                # A 'null' category is the top-level category
                if cascade is False:
                    # Do not cascade, only list parts in the top-level category
                    queryset = queryset.filter(category=None)

            else:
                try:
                    category = PartCategory.objects.get(pk=cat_id)

                    # If '?cascade=true' then include parts which exist in sub-categories
                    if cascade:
                        queryset = queryset.filter(category__in=category.getUniqueChildren())
                    # Just return parts directly in the requested category
                    else:
                        queryset = queryset.filter(category=cat_id)
                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')))

        # Filter by "parts which need stock to complete build"
        stock_to_build = params.get('stock_to_build', None)

        # TODO: This is super expensive, database query wise...
        # TODO: Need to figure out a cheaper way of making this filter query

        if stock_to_build is not None:
            # Get active builds
            builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
            # Store parts with builds needing stock
            parts_needed_to_complete_builds = []
            # Filter required parts
            for build in builds:
                parts_needed_to_complete_builds += [part.pk for part in build.required_parts_to_complete_build]

            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

    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]

    filter_fields = [
        'is_template',
        'variant_of',
        'assembly',
        'component',
        'trackable',
        'purchaseable',
        'salable',
        'active',
    ]

    ordering_fields = [
        'name',
        'creation_date',
        'IPN',
        'in_stock',
    ]

    # Default ordering
    ordering = 'name'

    search_fields = [
        'name',
        'description',
        'IPN',
        'revision',
        'keywords',
        'category__name',
    ]


class PartParameterTemplateList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of PartParameterTemplate objects.

    - GET: Return list of PartParameterTemplate objects
    - POST: Create a new PartParameterTemplate object
    """

    queryset = PartParameterTemplate.objects.all()
    serializer_class = part_serializers.PartParameterTemplateSerializer

    filter_backends = [
        filters.OrderingFilter,
    ]

    filter_fields = [
        'name',
    ]


class PartParameterList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of PartParameter objects

    - GET: Return list of PartParameter objects
    - POST: Create a new PartParameter object
    """

    queryset = PartParameter.objects.all()
    serializer_class = part_serializers.PartParameterSerializer

    filter_backends = [
        DjangoFilterBackend
    ]

    filter_fields = [
        'part',
        'template',
    ]


class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    API endpoint for detail view of a single PartParameter object
    """

    queryset = PartParameter.objects.all()
    serializer_class = part_serializers.PartParameterSerializer


class BomList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of BomItem objects.

    - GET: Return list of BomItem objects
    - POST: Create a new BomItem object
    """

    serializer_class = part_serializers.BomItemSerializer

    def list(self, request, *args, **kwargs):

        queryset = self.filter_queryset(self.get_queryset())

        serializer = self.get_serializer(queryset, many=True)

        data = serializer.data

        if request.is_ajax():
            return JsonResponse(data, safe=False)
        else:
            return Response(data)

    def get_serializer(self, *args, **kwargs):

        # Do we wish to include extra detail?
        try:
            kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', None))
        except AttributeError:
            pass

        try:
            kwargs['sub_part_detail'] = str2bool(self.request.GET.get('sub_part_detail', None))
        except AttributeError:
            pass

        # Ensure the request context is passed through!
        kwargs['context'] = self.get_serializer_context()

        return self.serializer_class(*args, **kwargs)

    def get_queryset(self, *args, **kwargs):

        queryset = BomItem.objects.all()

        queryset = self.get_serializer_class().setup_eager_loading(queryset)

        return queryset

    def filter_queryset(self, queryset):

        queryset = super().filter_queryset(queryset)

        params = self.request.query_params

        # Filter by "optional" status?
        optional = params.get('optional', None)

        if optional is not None:
            optional = str2bool(optional)

            queryset = queryset.filter(optional=optional)

        # Filter by "inherited" status
        inherited = params.get('inherited', None)

        if inherited is not None:
            inherited = str2bool(inherited)

            queryset = queryset.filter(inherited=inherited)

        # Filter by "allow_variants"
        variants = params.get("allow_variants", None)

        if variants is not None:
            variants = str2bool(variants)

            queryset = queryset.filter(allow_variants=variants)

        # Filter by part?
        part = params.get('part', None)

        if part is not None:
            """
            If we are filtering by "part", there are two cases to consider:

            a) Bom items which are defined for *this* part
            b) Inherited parts which are defined for a *parent* part

            So we need to construct two queries!
            """

            # First, check that the part is actually valid!
            try:
                part = Part.objects.get(pk=part)

                queryset = queryset.filter(part.get_bom_item_filter())

            except (ValueError, Part.DoesNotExist):
                pass

        # Filter by "active" status of the part
        part_active = params.get('part_active', None)

        if part_active is not None:
            part_active = str2bool(part_active)
            queryset = queryset.filter(part__active=part_active)

        # Filter by "trackable" status of the part
        part_trackable = params.get('part_trackable', None)

        if part_trackable is not None:
            part_trackable = str2bool(part_trackable)
            queryset = queryset.filter(part__trackable=part_trackable)

        # Filter by "trackable" status of the sub-part
        sub_part_trackable = params.get('sub_part_trackable', None)

        if sub_part_trackable is not None:
            sub_part_trackable = str2bool(sub_part_trackable)
            queryset = queryset.filter(sub_part__trackable=sub_part_trackable)

        # Filter by whether the BOM line has been validated
        validated = params.get('validated', None)

        if validated is not None:
            validated = str2bool(validated)

            # Work out which lines have actually been validated
            pks = []

            for bom_item in queryset.all():
                if bom_item.is_line_valid:
                    pks.append(bom_item.pk)

            if validated:
                queryset = queryset.filter(pk__in=pks)
            else:
                queryset = queryset.exclude(pk__in=pks)

        # Annotate with purchase prices
        queryset = queryset.annotate(
            purchase_price_min=Min('sub_part__stock_items__purchase_price'),
            purchase_price_max=Max('sub_part__stock_items__purchase_price'),
            purchase_price_avg=Avg('sub_part__stock_items__purchase_price'),
        )

        # Get values for currencies
        currencies = queryset.annotate(
            purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'),
        ).values('pk', 'sub_part', 'purchase_price_currency')

        def convert_price(price, currency, decimal_places=4):
            """ Convert price field, returns Money field """

            price_adjusted = None

            # Get default currency from settings
            default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
            
            if price:
                if currency and default_currency:
                    try:
                        # Get adjusted price
                        price_adjusted = convert_money(Money(price, currency), default_currency)
                    except MissingRate:
                        # No conversion rate set
                        price_adjusted = Money(price, currency)
                else:
                    # Currency exists
                    if currency:
                        price_adjusted = Money(price, currency)
                    # Default currency exists
                    if default_currency:
                        price_adjusted = Money(price, default_currency)

            if price_adjusted and decimal_places:
                price_adjusted.decimal_places = decimal_places

            return price_adjusted

        # Convert prices to default currency (using backend conversion rates)
        for bom_item in queryset:
            # Find associated currency (select first found)
            purchase_price_currency = None
            for currency_item in currencies:
                if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part.pk:
                    purchase_price_currency = currency_item['purchase_price_currency']
                    break
            # Convert prices
            bom_item.purchase_price_min = convert_price(bom_item.purchase_price_min, purchase_price_currency)
            bom_item.purchase_price_max = convert_price(bom_item.purchase_price_max, purchase_price_currency)
            bom_item.purchase_price_avg = convert_price(bom_item.purchase_price_avg, purchase_price_currency)

        return queryset

    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]

    filter_fields = [
    ]


class BomDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API endpoint for detail view of a single BomItem object """

    queryset = BomItem.objects.all()
    serializer_class = part_serializers.BomItemSerializer


class BomItemValidate(generics.UpdateAPIView):
    """ API endpoint for validating a BomItem """

    # Very simple serializers
    class BomItemValidationSerializer(serializers.Serializer):

        valid = serializers.BooleanField(default=False)

    queryset = BomItem.objects.all()
    serializer_class = BomItemValidationSerializer

    def update(self, request, *args, **kwargs):
        """ Perform update request """

        partial = kwargs.pop('partial', False)

        valid = request.data.get('valid', False)

        instance = self.get_object()

        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)

        if type(instance) == BomItem:
            instance.validate_hash(valid)

        return Response(serializer.data)


part_api_urls = [
    url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),

    # Base URL for PartCategory API endpoints
    url(r'^category/', include([
        url(r'^(?P<pk>\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
        url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
        url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
    ])),

    # Base URL for PartTestTemplate API endpoints
    url(r'^test-template/', include([
        url(r'^$', PartTestTemplateList.as_view(), name='api-part-test-template-list'),
    ])),

    # Base URL for PartAttachment API endpoints
    url(r'^attachment/', include([
        url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
    ])),

    # Base URL for part sale pricing
    url(r'^sale-price/', include([
        url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
    ])),

    # Base URL for part internal pricing
    url(r'^internal-price/', include([
        url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
    ])),

    # Base URL for PartParameter API endpoints
    url(r'^parameter/', include([
        url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),

        url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'),
        url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
    ])),

    url(r'^thumbs/', include([
        url(r'^$', PartThumbs.as_view(), name='api-part-thumbs'),
        url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
    ])),

    url(r'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),

    url(r'^.*$', PartList.as_view(), name='api-part-list'),
]

bom_api_urls = [
    # BOM Item Detail
    url(r'^(?P<pk>\d+)/', include([
        url(r'^validate/?', BomItemValidate.as_view(), name='api-bom-item-validate'),
        url(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'),
    ])),

    # Catch-all
    url(r'^.*$', BomList.as_view(), name='api-bom-list'),
]

InvenTree/part/api.py
"""
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.http import JsonResponse
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 rest_framework.response import Response
from rest_framework import filters, serializers
from rest_framework import generics

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
from .models import PartAttachment, PartTestTemplate
from .models import PartSellPriceBreak, PartInternalPriceBreak
from .models import PartCategoryParameterTemplate

from common.models import InvenTreeSetting
from build.models import Build

from . import serializers as part_serializers

from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin

from InvenTree.status_codes import BuildStatus


class PartCategoryTree(TreeSerializer):

    title = _("Parts")
    model = PartCategory

    queryset = PartCategory.objects.all()

    @property
    def root_url(self):
        return reverse('part-index')

    def get_items(self):
        return PartCategory.objects.all().prefetch_related('parts', 'children')


class CategoryList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of PartCategory objects.

    - GET: Return a list of PartCategory objects
    - POST: Create a new PartCategory object
    """

    queryset = PartCategory.objects.all()
    serializer_class = part_serializers.CategorySerializer

    def filter_queryset(self, queryset):
        """
        Custom filtering:
        - Allow filtering by "null" parent to retrieve top-level part categories
        """

        queryset = super().filter_queryset(queryset)

        params = self.request.query_params

        cat_id = params.get('parent', None)

        cascade = str2bool(params.get('cascade', False))

        # Do not filter by category
        if cat_id is None:
            pass
        # Look for top-level categories
        elif isNull(cat_id):

            if not cascade:
                queryset = queryset.filter(parent=None)

        else:
            try:
                category = PartCategory.objects.get(pk=cat_id)

                if cascade:
                    parents = category.get_descendants(include_self=True)
                    parent_ids = [p.id for p in parents]

                    queryset = queryset.filter(parent__in=parent_ids)
                else:
                    queryset = queryset.filter(parent=category)

            except (ValueError, PartCategory.DoesNotExist):
                pass

        return queryset

    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]

    filter_fields = [
    ]

    ordering_fields = [
        'name',
    ]

    ordering = 'name'

    search_fields = [
        'name',
        'description',
    ]


class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API endpoint for detail view of a single PartCategory object """
    serializer_class = part_serializers.CategorySerializer
    queryset = PartCategory.objects.all()


class CategoryParameters(generics.ListAPIView):
    """ API endpoint for accessing a list of PartCategoryParameterTemplate objects.

    - GET: Return a list of PartCategoryParameterTemplate objects
    """

    queryset = PartCategoryParameterTemplate.objects.all()
    serializer_class = part_serializers.CategoryParameterTemplateSerializer

    def get_queryset(self):
        """
        Custom filtering:
        - Allow filtering by "null" parent to retrieve all categories parameter templates
        - Allow filtering by category
        - Allow traversing all parent categories
        """

        try:
            cat_id = int(self.kwargs.get('pk', None))
        except TypeError:
            cat_id = None
        fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true'))

        queryset = super().get_queryset()

        if isinstance(cat_id, int):

            try:
                category = PartCategory.objects.get(pk=cat_id)
            except PartCategory.DoesNotExist:
                # Return empty queryset
                return PartCategoryParameterTemplate.objects.none()

            category_list = [cat_id]

            if fetch_parent:
                parent_categories = category.get_ancestors()
                for parent in parent_categories:
                    category_list.append(parent.pk)

            queryset = queryset.filter(category__in=category_list)

        return queryset


class PartSalePriceList(generics.ListCreateAPIView):
    """
    API endpoint for list view of PartSalePriceBreak model
    """

    queryset = PartSellPriceBreak.objects.all()
    serializer_class = part_serializers.PartSalePriceSerializer

    filter_backends = [
        DjangoFilterBackend
    ]

    filter_fields = [
        'part',
    ]


class PartInternalPriceList(generics.ListCreateAPIView):
    """
    API endpoint for list view of PartInternalPriceBreak model
    """

    queryset = PartInternalPriceBreak.objects.all()
    serializer_class = part_serializers.PartInternalPriceSerializer
    permission_required = 'roles.sales_order.show'

    filter_backends = [
        DjangoFilterBackend
    ]

    filter_fields = [
        'part',
    ]


class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
    """
    API endpoint for listing (and creating) a PartAttachment (file upload).
    """

    queryset = PartAttachment.objects.all()
    serializer_class = part_serializers.PartAttachmentSerializer

    filter_backends = [
        DjangoFilterBackend,
    ]

    filter_fields = [
        'part',
    ]


class PartTestTemplateList(generics.ListCreateAPIView):
    """
    API endpoint for listing (and creating) a PartTestTemplate.
    """

    queryset = PartTestTemplate.objects.all()
    serializer_class = part_serializers.PartTestTemplateSerializer

    def filter_queryset(self, queryset):
        """
        Filter the test list queryset.

        If filtering by 'part', we include results for any parts "above" the specified part.
        """

        queryset = super().filter_queryset(queryset)

        params = self.request.query_params

        part = params.get('part', None)

        # Filter by part
        if part:
            try:
                part = Part.objects.get(pk=part)
                queryset = queryset.filter(part__in=part.get_ancestors(include_self=True))
            except (ValueError, Part.DoesNotExist):
                pass

        # Filter by 'required' status
        required = params.get('required', None)

        if required is not None:
            queryset = queryset.filter(required=required)

        return queryset

    filter_backends = [
        DjangoFilterBackend,
        filters.OrderingFilter,
        filters.SearchFilter,
    ]


class PartThumbs(generics.ListAPIView):
    """
    API endpoint for retrieving information on available Part thumbnails
    """

    queryset = Part.objects.all()
    serializer_class = part_serializers.PartThumbSerializer

    def get_queryset(self):

        queryset = super().get_queryset()

        # Get all Parts which have an associated image
        queryset = queryset.exclude(image='')

        return queryset

    def list(self, request, *args, **kwargs):
        """
        Serialize the available Part images.
        - Images may be used for multiple parts!
        """

        queryset = self.get_queryset()

        # TODO - We should return the thumbnails here, not the full image!

        # Return the most popular parts first
        data = queryset.values(
            'image',
        ).annotate(count=Count('image')).order_by('-count')

        return Response(data)


class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
    """ API endpoint for updating Part thumbnails"""

    queryset = Part.objects.all()
    serializer_class = part_serializers.PartThumbSerializerUpdate

    filter_backends = [
        DjangoFilterBackend
    ]


class PartDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API endpoint for detail view of a single Part object """

    queryset = Part.objects.all()
    serializer_class = part_serializers.PartSerializer

    starred_parts = None

    def get_queryset(self, *args, **kwargs):
        queryset = super().get_queryset(*args, **kwargs)

        queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
        queryset = part_serializers.PartSerializer.annotate_queryset(queryset)

        return queryset

    def get_serializer(self, *args, **kwargs):

        try:
            kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', False))
        except AttributeError:
            pass

        # Ensure the request context is passed through
        kwargs['context'] = self.get_serializer_context()

        # Pass a list of "starred" parts fo the current user to the serializer
        # We do this to reduce the number of database queries required!
        if self.starred_parts is None and self.request is not None:
            self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]

        kwargs['starred_parts'] = self.starred_parts

        return self.serializer_class(*args, **kwargs)

    def destroy(self, request, *args, **kwargs):
        # Retrieve part
        part = Part.objects.get(pk=int(kwargs['pk']))
        # Check if inactive
        if not part.active:
            # Delete
            return super(PartDetail, self).destroy(request, *args, **kwargs)
        else:
            # Return 405 error
            message = f'Part \'{part.name}\' (pk = {part.pk}) is active: cannot delete'
            return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data=message)

    def update(self, request, *args, **kwargs):
        """
        Custom update functionality for Part instance.

        - If the 'starred' field is provided, update the 'starred' status against current user
        """

        if 'starred' in request.data:
            starred = str2bool(request.data.get('starred', None))

            self.get_object().setStarred(request.user, starred)

        response = super().update(request, *args, **kwargs)

        return response


class PartList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of Part objects

    - GET: Return list of objects
    - POST: Create a new Part object

    The Part object list can be filtered by:
        - category: Filter by PartCategory reference
        - cascade: If true, include parts from sub-categories
        - starred: Is the part "starred" by the current user?
        - is_template: Is the part a template part?
        - variant_of: Filter by variant_of Part reference
        - assembly: Filter by assembly field
        - component: Filter by component field
        - trackable: Filter by trackable field
        - purchaseable: Filter by purcahseable field
        - salable: Filter by salable field
        - active: Filter by active field
        - ancestor: Filter parts by 'ancestor' (template / variant tree)
    """

    serializer_class = part_serializers.PartSerializer

    queryset = Part.objects.all()

    starred_parts = None

    def get_serializer(self, *args, **kwargs):

        # Ensure the request context is passed through
        kwargs['context'] = self.get_serializer_context()

        # Pass a list of "starred" parts fo the current user to the serializer
        # We do this to reduce the number of database queries required!
        if self.starred_parts is None and self.request is not None:
            self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]

        kwargs['starred_parts'] = self.starred_parts

        return self.serializer_class(*args, **kwargs)

    def list(self, request, *args, **kwargs):
        """
        Overide the 'list' method, as the PartCategory objects are
        very expensive to serialize!

        So we will serialize them first, and keep them in memory,
        so that they do not have to be serialized multiple times...
        """

        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)

        if page is not None:
            serializer = self.get_serializer(page, many=True)
        else:
            serializer = self.get_serializer(queryset, many=True)

        data = serializer.data

        # 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
            category_ids = set()

            for part in data:
                cat_id = part['category']

                if cat_id is not None:
                    category_ids.add(cat_id)

            # Fetch only the required PartCategory objects from the database
            categories = PartCategory.objects.filter(pk__in=category_ids).prefetch_related(
                'parts',
                'parent',
                'children',
            )

            category_map = {}

            # Serialize each PartCategory object
            for category in categories:
                category_map[category.pk] = part_serializers.CategorySerializer(category).data

            for part in data:
                cat_id = part['category']

                if cat_id is not None and cat_id in category_map.keys():
                    detail = category_map[cat_id]
                else:
                    detail = None

                part['category_detail'] = detail

        """
        Determine the response type based on the request.
        a) For HTTP requests (e.g. via the browseable API) return a DRF response
        b) For AJAX requests, simply return a JSON rendered response.
        """
        if page is not None:
            return self.get_paginated_response(data)
        elif request.is_ajax():
            return JsonResponse(data, safe=False)
        else:
            return Response(data)

    def perform_create(self, serializer):
        """
        We wish to save the user who created this part!

        Note: Implementation copied from DRF class CreateModelMixin
        """

        part = serializer.save()
        part.creation_user = self.request.user
        part.save()

    def get_queryset(self, *args, **kwargs):

        queryset = super().get_queryset(*args, **kwargs)

        queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
        queryset = part_serializers.PartSerializer.annotate_queryset(queryset)

        return queryset

    def filter_queryset(self, queryset):
        """
        Perform custom filtering of the queryset.
        We overide the DRF filter_fields here because
        """

        params = self.request.query_params

        queryset = super().filter_queryset(queryset)

        # Filter by "uses" query - Limit to parts which use the provided part
        uses = params.get('uses', None)

        if uses:
            try:
                uses = Part.objects.get(pk=uses)

                queryset = queryset.filter(uses.get_used_in_filter())

            except (ValueError, Part.DoesNotExist):
                pass

        # Filter by 'ancestor'?
        ancestor = params.get('ancestor', None)

        if ancestor is not None:
            # If an 'ancestor' part is provided, filter to match only children
            try:
                ancestor = Part.objects.get(pk=ancestor)
                descendants = ancestor.get_descendants(include_self=False)
                queryset = queryset.filter(pk__in=[d.pk for d in descendants])
            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 whether the BOM has been validated (or not)
        bom_valid = params.get('bom_valid', None)

        # TODO: Querying bom_valid status may be quite expensive
        # TODO: (It needs to be profiled!)
        # TODO: It might be worth caching the bom_valid status to a database column

        if bom_valid is not None:

            bom_valid = str2bool(bom_valid)

            # Limit queryset to active assemblies
            queryset = queryset.filter(active=True, assembly=True)

            pks = []

            for part in queryset:
                if part.is_bom_valid() == bom_valid:
                    pks.append(part.pk)

            queryset = queryset.filter(pk__in=pks)

        # Filter by 'starred' parts?
        starred = params.get('starred', None)

        if starred is not None:
            starred = str2bool(starred)
            starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]

            if starred:
                queryset = queryset.filter(pk__in=starred_parts)
            else:
                queryset = queryset.exclude(pk__in=starred_parts)

        # Cascade? (Default = True)
        cascade = str2bool(params.get('cascade', True))

        # Does the user wish to filter by category?
        cat_id = params.get('category', None)

        if cat_id is None:
            # No category filtering if category is not specified
            pass

        else:
            # Category has been specified!
            if isNull(cat_id):
                # A 'null' category is the top-level category
                if cascade is False:
                    # Do not cascade, only list parts in the top-level category
                    queryset = queryset.filter(category=None)

            else:
                try:
                    category = PartCategory.objects.get(pk=cat_id)

                    # If '?cascade=true' then include parts which exist in sub-categories
                    if cascade:
                        queryset = queryset.filter(category__in=category.getUniqueChildren())
                    # Just return parts directly in the requested category
                    else:
                        queryset = queryset.filter(category=cat_id)
                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')))

        # Filter by "parts which need stock to complete build"
        stock_to_build = params.get('stock_to_build', None)

        # TODO: This is super expensive, database query wise...
        # TODO: Need to figure out a cheaper way of making this filter query

        if stock_to_build is not None:
            # Get active builds
            builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
            # Store parts with builds needing stock
            parts_needed_to_complete_builds = []
            # Filter required parts
            for build in builds:
                parts_needed_to_complete_builds += [part.pk for part in build.required_parts_to_complete_build]

            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

    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]

    filter_fields = [
        'is_template',
        'variant_of',
        'assembly',
        'component',
        'trackable',
        'purchaseable',
        'salable',
        'active',
    ]

    ordering_fields = [
        'name',
        'creation_date',
        'IPN',
        'in_stock',
        'category',
    ]

    # Default ordering
    ordering = 'name'

    search_fields = [
        'name',
        'description',
        'IPN',
        'revision',
        'keywords',
        'category__name',
    ]


class PartParameterTemplateList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of PartParameterTemplate objects.

    - GET: Return list of PartParameterTemplate objects
    - POST: Create a new PartParameterTemplate object
    """

    queryset = PartParameterTemplate.objects.all()
    serializer_class = part_serializers.PartParameterTemplateSerializer

    filter_backends = [
        filters.OrderingFilter,
    ]

    filter_fields = [
        'name',
    ]


class PartParameterList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of PartParameter objects

    - GET: Return list of PartParameter objects
    - POST: Create a new PartParameter object
    """

    queryset = PartParameter.objects.all()
    serializer_class = part_serializers.PartParameterSerializer

    filter_backends = [
        DjangoFilterBackend
    ]

    filter_fields = [
        'part',
        'template',
    ]


class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
    """
    API endpoint for detail view of a single PartParameter object
    """

    queryset = PartParameter.objects.all()
    serializer_class = part_serializers.PartParameterSerializer


class BomList(generics.ListCreateAPIView):
    """ API endpoint for accessing a list of BomItem objects.

    - GET: Return list of BomItem objects
    - POST: Create a new BomItem object
    """

    serializer_class = part_serializers.BomItemSerializer

    def list(self, request, *args, **kwargs):

        queryset = self.filter_queryset(self.get_queryset())

        serializer = self.get_serializer(queryset, many=True)

        data = serializer.data

        if request.is_ajax():
            return JsonResponse(data, safe=False)
        else:
            return Response(data)

    def get_serializer(self, *args, **kwargs):

        # Do we wish to include extra detail?
        try:
            kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', None))
        except AttributeError:
            pass

        try:
            kwargs['sub_part_detail'] = str2bool(self.request.GET.get('sub_part_detail', None))
        except AttributeError:
            pass

        # Ensure the request context is passed through!
        kwargs['context'] = self.get_serializer_context()

        return self.serializer_class(*args, **kwargs)

    def get_queryset(self, *args, **kwargs):

        queryset = BomItem.objects.all()

        queryset = self.get_serializer_class().setup_eager_loading(queryset)

        return queryset

    def filter_queryset(self, queryset):

        queryset = super().filter_queryset(queryset)

        params = self.request.query_params

        # Filter by "optional" status?
        optional = params.get('optional', None)

        if optional is not None:
            optional = str2bool(optional)

            queryset = queryset.filter(optional=optional)

        # Filter by "inherited" status
        inherited = params.get('inherited', None)

        if inherited is not None:
            inherited = str2bool(inherited)

            queryset = queryset.filter(inherited=inherited)

        # Filter by "allow_variants"
        variants = params.get("allow_variants", None)

        if variants is not None:
            variants = str2bool(variants)

            queryset = queryset.filter(allow_variants=variants)

        # Filter by part?
        part = params.get('part', None)

        if part is not None:
            """
            If we are filtering by "part", there are two cases to consider:

            a) Bom items which are defined for *this* part
            b) Inherited parts which are defined for a *parent* part

            So we need to construct two queries!
            """

            # First, check that the part is actually valid!
            try:
                part = Part.objects.get(pk=part)

                queryset = queryset.filter(part.get_bom_item_filter())

            except (ValueError, Part.DoesNotExist):
                pass

        # Filter by "active" status of the part
        part_active = params.get('part_active', None)

        if part_active is not None:
            part_active = str2bool(part_active)
            queryset = queryset.filter(part__active=part_active)

        # Filter by "trackable" status of the part
        part_trackable = params.get('part_trackable', None)

        if part_trackable is not None:
            part_trackable = str2bool(part_trackable)
            queryset = queryset.filter(part__trackable=part_trackable)

        # Filter by "trackable" status of the sub-part
        sub_part_trackable = params.get('sub_part_trackable', None)

        if sub_part_trackable is not None:
            sub_part_trackable = str2bool(sub_part_trackable)
            queryset = queryset.filter(sub_part__trackable=sub_part_trackable)

        # Filter by whether the BOM line has been validated
        validated = params.get('validated', None)

        if validated is not None:
            validated = str2bool(validated)

            # Work out which lines have actually been validated
            pks = []

            for bom_item in queryset.all():
                if bom_item.is_line_valid:
                    pks.append(bom_item.pk)

            if validated:
                queryset = queryset.filter(pk__in=pks)
            else:
                queryset = queryset.exclude(pk__in=pks)

        # Annotate with purchase prices
        queryset = queryset.annotate(
            purchase_price_min=Min('sub_part__stock_items__purchase_price'),
            purchase_price_max=Max('sub_part__stock_items__purchase_price'),
            purchase_price_avg=Avg('sub_part__stock_items__purchase_price'),
        )

        # Get values for currencies
        currencies = queryset.annotate(
            purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'),
        ).values('pk', 'sub_part', 'purchase_price_currency')

        def convert_price(price, currency, decimal_places=4):
            """ Convert price field, returns Money field """

            price_adjusted = None

            # Get default currency from settings
            default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
            
            if price:
                if currency and default_currency:
                    try:
                        # Get adjusted price
                        price_adjusted = convert_money(Money(price, currency), default_currency)
                    except MissingRate:
                        # No conversion rate set
                        price_adjusted = Money(price, currency)
                else:
                    # Currency exists
                    if currency:
                        price_adjusted = Money(price, currency)
                    # Default currency exists
                    if default_currency:
                        price_adjusted = Money(price, default_currency)

            if price_adjusted and decimal_places:
                price_adjusted.decimal_places = decimal_places

            return price_adjusted

        # Convert prices to default currency (using backend conversion rates)
        for bom_item in queryset:
            # Find associated currency (select first found)
            purchase_price_currency = None
            for currency_item in currencies:
                if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part.pk:
                    purchase_price_currency = currency_item['purchase_price_currency']
                    break
            # Convert prices
            bom_item.purchase_price_min = convert_price(bom_item.purchase_price_min, purchase_price_currency)
            bom_item.purchase_price_max = convert_price(bom_item.purchase_price_max, purchase_price_currency)
            bom_item.purchase_price_avg = convert_price(bom_item.purchase_price_avg, purchase_price_currency)

        return queryset

    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]

    filter_fields = [
    ]


class BomDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API endpoint for detail view of a single BomItem object """

    queryset = BomItem.objects.all()
    serializer_class = part_serializers.BomItemSerializer


class BomItemValidate(generics.UpdateAPIView):
    """ API endpoint for validating a BomItem """

    # Very simple serializers
    class BomItemValidationSerializer(serializers.Serializer):

        valid = serializers.BooleanField(default=False)

    queryset = BomItem.objects.all()
    serializer_class = BomItemValidationSerializer

    def update(self, request, *args, **kwargs):
        """ Perform update request """

        partial = kwargs.pop('partial', False)

        valid = request.data.get('valid', False)

        instance = self.get_object()

        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)

        if type(instance) == BomItem:
            instance.validate_hash(valid)

        return Response(serializer.data)


part_api_urls = [
    url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),

    # Base URL for PartCategory API endpoints
    url(r'^category/', include([
        url(r'^(?P<pk>\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
        url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
        url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
    ])),

    # Base URL for PartTestTemplate API endpoints
    url(r'^test-template/', include([
        url(r'^$', PartTestTemplateList.as_view(), name='api-part-test-template-list'),
    ])),

    # Base URL for PartAttachment API endpoints
    url(r'^attachment/', include([
        url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
    ])),

    # Base URL for part sale pricing
    url(r'^sale-price/', include([
        url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'),
    ])),

    # Base URL for part internal pricing
    url(r'^internal-price/', include([
        url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
    ])),

    # Base URL for PartParameter API endpoints
    url(r'^parameter/', include([
        url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),

        url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'),
        url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
    ])),

    url(r'^thumbs/', include([
        url(r'^$', PartThumbs.as_view(), name='api-part-thumbs'),
        url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
    ])),

    url(r'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),

    url(r'^.*$', PartList.as_view(), name='api-part-list'),
]

bom_api_urls = [
    # BOM Item Detail
    url(r'^(?P<pk>\d+)/', include([
        url(r'^validate/?', BomItemValidate.as_view(), name='api-bom-item-validate'),
        url(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'),
    ])),

    # Catch-all
    url(r'^.*$', BomList.as_view(), name='api-bom-list'),
]

InvenTree/part/api.py
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index c2785b666f0d..60cea121a72c 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -706,6 +706,7 @@ def filter_queryset(self, queryset):
         'creation_date',
         'IPN',
         'in_stock',
+        'category',
     ]
 
     # Default ordering
diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js
index 7331ec25e0d7..66174e2f1500 100644
--- a/InvenTree/templates/js/part.js
+++ b/InvenTree/templates/js/part.js
@@ -447,6 +447,7 @@ function loadPartTable(table, url, options={}) {
 
     columns.push({
         sortable: true,
+        sortName: 'category',
         field: 'category_detail',
         title: '{% trans "Category" %}',
         formatter: function(value, row, index, field) {

Table ordering not working for any parameter
![image](https://user-images.githubusercontent.com/15521303/122682048-35817e80-d1ef-11eb-92d5-8b41370cd088.png)

Table ordering not working for any parameter
![image](https://user-images.githubusercontent.com/15521303/122682048-35817e80-d1ef-11eb-92d5-8b41370cd088.png)

