diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py new file mode 100644 index 0000000000..cd1b769646 --- /dev/null +++ b/InvenTree/InvenTree/filters.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from rest_framework.filters import OrderingFilter + + +class InvenTreeOrderingFilter(OrderingFilter): + """ + Custom OrderingFilter class which allows aliased filtering of related fields. + + To use, simply specify this filter in the "filter_backends" section. + + filter_backends = [ + InvenTreeOrderingFilter, + ] + + Then, specify a ordering_field_aliases attribute: + + ordering_field_alises = { + 'name': 'part__part__name', + 'SKU': 'part__SKU', + } + """ + + def get_ordering(self, request, queryset, view): + + ordering = super().get_ordering(request, queryset, view) + + aliases = getattr(view, 'ordering_field_aliases', None) + + # Attempt to map ordering fields based on provided aliases + if ordering is not None and aliases is not None: + """ + Ordering fields should be mapped to separate fields + """ + + for idx, field in enumerate(ordering): + + reverse = False + + if field.startswith('-'): + field = field[1:] + reverse = True + + if field in aliases: + ordering[idx] = aliases[field] + + if reverse: + ordering[idx] = '-' + ordering[idx] + + return ordering diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index a834989fd9..eb8ba22ad0 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -7,11 +7,12 @@ from __future__ import unicode_literals from django.conf.urls import url, include -from django_filters.rest_framework import DjangoFilterBackend +from django_filters import rest_framework as rest_filters from rest_framework import generics from rest_framework import filters, status from rest_framework.response import Response +from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.helpers import str2bool from InvenTree.api import AttachmentMixin from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus @@ -144,7 +145,7 @@ class POList(generics.ListCreateAPIView): return queryset filter_backends = [ - DjangoFilterBackend, + rest_filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter, ] @@ -214,6 +215,14 @@ class POLineItemList(generics.ListCreateAPIView): queryset = PurchaseOrderLineItem.objects.all() serializer_class = POLineItemSerializer + def get_queryset(self, *args, **kwargs): + + queryset = super().get_queryset(*args, **kwargs) + + queryset = POLineItemSerializer.annotate_queryset(queryset) + + return queryset + def get_serializer(self, *args, **kwargs): try: @@ -226,18 +235,26 @@ class POLineItemList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) filter_backends = [ - DjangoFilterBackend, + rest_filters.DjangoFilterBackend, filters.SearchFilter, - filters.OrderingFilter + InvenTreeOrderingFilter ] + ordering_field_aliases = { + 'MPN': 'part__manufacturer_part__MPN', + 'SKU': 'part__SKU', + 'part_name': 'part__part__name', + } + ordering_fields = [ - 'part__part__name', - 'part__MPN', - 'part__SKU', - 'reference', + 'MPN', + 'part_name', + 'purchase_price', 'quantity', 'received', + 'reference', + 'SKU', + 'total_price', ] search_fields = [ @@ -262,6 +279,14 @@ class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView): queryset = PurchaseOrderLineItem.objects.all() serializer_class = POLineItemSerializer + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = POLineItemSerializer.annotate_queryset(queryset) + + return queryset + class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ @@ -272,7 +297,7 @@ class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): serializer_class = SOAttachmentSerializer filter_backends = [ - DjangoFilterBackend, + rest_filters.DjangoFilterBackend, ] filter_fields = [ @@ -396,7 +421,7 @@ class SOList(generics.ListCreateAPIView): return queryset filter_backends = [ - DjangoFilterBackend, + rest_filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter, ] @@ -495,7 +520,7 @@ class SOLineItemList(generics.ListCreateAPIView): return queryset filter_backends = [ - DjangoFilterBackend, + rest_filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter ] @@ -580,7 +605,7 @@ class SOAllocationList(generics.ListCreateAPIView): return queryset filter_backends = [ - DjangoFilterBackend, + rest_filters.DjangoFilterBackend, ] # Default filterable fields @@ -598,7 +623,7 @@ class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): serializer_class = POAttachmentSerializer filter_backends = [ - DjangoFilterBackend, + rest_filters.DjangoFilterBackend, ] filter_fields = [ diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index e97d19250a..fe23bd2a17 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -7,8 +7,9 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ +from django.db import models from django.db.models import Case, When, Value -from django.db.models import BooleanField +from django.db.models import BooleanField, ExpressionWrapper, F from rest_framework import serializers from sql_util.utils import SubqueryCount @@ -109,6 +110,23 @@ class POSerializer(InvenTreeModelSerializer): class POLineItemSerializer(InvenTreeModelSerializer): + @staticmethod + def annotate_queryset(queryset): + """ + Add some extra annotations to this queryset: + + - Total price = purchase_price * quantity + """ + + queryset = queryset.annotate( + total_price=ExpressionWrapper( + F('purchase_price') * F('quantity'), + output_field=models.DecimalField() + ) + ) + + return queryset + def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) @@ -123,6 +141,8 @@ class POLineItemSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField(default=1) received = serializers.FloatField(default=0) + total_price = serializers.FloatField(read_only=True) + part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) @@ -158,6 +178,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'purchase_price_string', 'destination', 'destination_detail', + 'total_price', ] diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 80d5513efa..8e7f77bad1 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -294,7 +294,7 @@ $("#po-line-table").inventreeTable({ { field: 'part', sortable: true, - sortName: 'part__part__name', + sortName: 'part_name', title: '{% trans "Part" %}', switchable: false, formatter: function(value, row, index, field) { @@ -314,7 +314,7 @@ $("#po-line-table").inventreeTable({ }, { sortable: true, - sortName: 'part__SKU', + sortName: 'SKU', field: 'supplier_part_detail.SKU', title: '{% trans "SKU" %}', formatter: function(value, row, index, field) { @@ -327,7 +327,7 @@ $("#po-line-table").inventreeTable({ }, { sortable: true, - sortName: 'part__MPN', + sortName: 'MPN', field: 'supplier_part_detail.manufacturer_part_detail.MPN', title: '{% trans "MPN" %}', formatter: function(value, row, index, field) { @@ -364,6 +364,7 @@ $("#po-line-table").inventreeTable({ } }, { + field: 'total_price', sortable: true, field: 'total_price', title: '{% trans "Total price" %}', @@ -384,7 +385,7 @@ $("#po-line-table").inventreeTable({ } }, { - sortable: true, + sortable: false, field: 'received', switchable: false, title: '{% trans "Received" %}', diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index cf58c7d4d9..7ff5c55bbe 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -43,6 +43,7 @@ from .serializers import StockItemTestResultSerializer from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull from InvenTree.api import AttachmentMixin +from InvenTree.filters import InvenTreeOrderingFilter from decimal import Decimal, InvalidOperation @@ -882,10 +883,16 @@ class StockList(generics.ListCreateAPIView): filter_backends = [ DjangoFilterBackend, filters.SearchFilter, - filters.OrderingFilter, + InvenTreeOrderingFilter, ] + ordering_field_aliases = { + 'SKU': 'supplier_part__SKU', + } + ordering_fields = [ + 'batch', + 'location', 'part__name', 'part__IPN', 'updated', @@ -893,10 +900,13 @@ class StockList(generics.ListCreateAPIView): 'expiry_date', 'quantity', 'status', + 'SKU', ] ordering = [ - 'part__name' + 'part__name', + 'quantity', + 'location', ] search_fields = [ diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 585cac1310..99c3824cac 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -921,8 +921,10 @@ function loadStockTable(table, options) { return renderLink(text, link); } - }, - { + }); + + col = { + field: 'supplier_part', title: '{% trans "Supplier Part" %}', visible: params['supplier_part_detail'] || false, @@ -944,15 +946,25 @@ function loadStockTable(table, options) { return renderLink(text, link); } - }); + }; + + if (!options.params.ordering) { + col.sortable = true; + col.sortName = 'SKU'; + } + + columns.push(col); col = { field: 'purchase_price_string', title: '{% trans "Purchase Price" %}', }; + if (!options.params.ordering) { - col['sortable'] = true; + col.sortable = true; + col.sortName = 'purchase_price'; }; + columns.push(col); columns.push({