From ef66a3b8f38f80d7226728bb3e7116e266ff54c5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 08:10:59 +1000 Subject: [PATCH 1/6] Make PartDetail view same as PartList view --- InvenTree/part/api.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index f4b5f4a5eb..a2e765796b 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -134,11 +134,40 @@ class PartDetail(generics.RetrieveUpdateAPIView): 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 permission_classes = [ permissions.IsAuthenticated, ] + def get_serializer(self, *args, **kwargs): + + try: + cat_detail = str2bool(self.request.query_params.get('category_detail', False)) + except AttributeError: + cat_detail = None + + # Ensure the request context is passed through + kwargs['context'] = self.get_serializer_context() + + kwargs['category_detail'] = cat_detail + + # 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) class PartList(generics.ListCreateAPIView): """ API endpoint for accessing a list of Part objects From 4ec5e9a907e15d219ff25e09e023ded8e74085f2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 08:24:43 +1000 Subject: [PATCH 2/6] Consolidate StockItem serializer --- InvenTree/stock/api.py | 31 ++++++++++++++++++++++--------- InvenTree/stock/serializers.py | 11 +++++++++-- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index bf85294c4e..77b35f45d0 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -58,20 +58,28 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = StockItemSerializer permission_classes = (permissions.IsAuthenticated,) + def get_queryset(self, *args, **kwargs): + + queryset = super().get_queryset(*args, **kwargs) + queryset = StockItemSerializer.prefetch_queryset(queryset) + queryset = StockItemSerializer.annotate_queryset(queryset) + + return queryset + def get_serializer(self, *args, **kwargs): try: - kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', False)) + kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False)) except AttributeError: pass try: - kwargs['location_detail'] = str2bool(self.request.GET.get('location_detail', False)) + kwargs['location_detail'] = str2bool(self.request.query_params.get('location_detail', False)) except AttributeError: pass try: - kwargs['supplier_detail'] = str2bool(self.request.GET.get('supplier_detail', False)) + kwargs['supplier_part_detail'] = str2bool(self.request.query_params.get('supplier_detail', False)) except AttributeError: pass @@ -321,14 +329,19 @@ class StockList(generics.ListCreateAPIView): def get_serializer(self, *args, **kwargs): try: - part_detail = str2bool(self.request.query_params.get('part_detail', None)) - location_detail = str2bool(self.request.query_params.get('location_detail', None)) + kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None)) except AttributeError: - part_detail = None - location_detail = None + pass - kwargs['part_detail'] = part_detail - kwargs['location_detail'] = location_detail + try: + kwargs['location_detail'] = str2bool(self.request.query_params.get('location_detail', None)) + except AttributeError: + pass + + try: + kwargs['supplier_part_detail'] = str2bool(self.request.query_params.get('supplier_part_detail', None)) + except AttributeError: + pass # Ensure the request context is passed through kwargs['context'] = self.get_serializer_context() diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 84ee616024..616755b11b 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -7,6 +7,7 @@ from rest_framework import serializers from .models import StockItem, StockLocation from .models import StockItemTracking +from company.serializers import SupplierPartSerializer from part.serializers import PartBriefSerializer from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer @@ -78,13 +79,14 @@ class StockItemSerializer(InvenTreeModelSerializer): performing database queries as efficiently as possible. """ - # TODO - pass + # TODO - Add custom annotated fields + return queryset status_text = serializers.CharField(source='get_status_display', read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) + supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True) tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True) @@ -92,6 +94,7 @@ class StockItemSerializer(InvenTreeModelSerializer): part_detail = kwargs.pop('part_detail', False) location_detail = kwargs.pop('location_detail', False) + supplier_part_detail = kwargs.pop('supplier_part_detail', False) super(StockItemSerializer, self).__init__(*args, **kwargs) @@ -101,6 +104,9 @@ class StockItemSerializer(InvenTreeModelSerializer): if location_detail is not True: self.fields.pop('location_detail') + if supplier_part_detail is not True: + self.fields.pop('supplier_part_detail') + class Meta: model = StockItem fields = [ @@ -116,6 +122,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'quantity', 'serial', 'supplier_part', + 'supplier_part_detail', 'status', 'status_text', 'tracking_items', From fee6246a8f0f6b75d66a818570d31b720a2a1b75 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 08:25:24 +1000 Subject: [PATCH 3/6] PEP fix --- InvenTree/part/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index a2e765796b..51a4a35938 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -169,6 +169,7 @@ class PartDetail(generics.RetrieveUpdateAPIView): return self.serializer_class(*args, **kwargs) + class PartList(generics.ListCreateAPIView): """ API endpoint for accessing a list of Part objects From 99fcbcc64657b20cdb5e4b691396a5647f605e42 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 09:41:21 +1000 Subject: [PATCH 4/6] Consolidation of PurchaseOrder API --- InvenTree/order/api.py | 109 ++++++++++++++++++++------------- InvenTree/order/serializers.py | 39 +++++++++++- 2 files changed, 104 insertions(+), 44 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 18ba890127..cf546bee21 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -14,6 +14,7 @@ from django.conf import settings from django.conf.urls import url from InvenTree.status_codes import OrderStatus +from InvenTree.helpers import str2bool import os @@ -34,66 +35,67 @@ class POList(generics.ListCreateAPIView): queryset = PurchaseOrder.objects.all() serializer_class = POSerializer - def list(self, request, *args, **kwargs): + def get_serializer(self, *args, **kwargs): - queryset = self.get_queryset().prefetch_related('supplier', 'lines') + try: + kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', False)) + except AttributeError: + pass - queryset = self.filter_queryset(queryset) + # 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 = super().get_queryset(*args, **kwargs) + + queryset = queryset.prefetch_related( + 'supplier', + 'lines', + ) + + queryset = POSerializer.annotate_queryset(queryset) + + return queryset + + + def filter_queryset(self, queryset): + + # Perform basic filtering + queryset = super().filter_queryset(queryset) + + params = self.request.query_params # Special filtering for 'status' field - if 'status' in request.GET: - status = request.GET['status'] + status = params.get('status', None) + if status is not None: # First attempt to filter by integer value - try: - status = int(status) - queryset = queryset.filter(status=status) - except ValueError: - try: - value = OrderStatus.value(status) - queryset = queryset.filter(status=value) - except ValueError: - pass + queryset = queryset.filter(status=status) # Attempt to filter by part - if 'part' in request.GET: + part = params.get('part', None) + + if part is not None: try: - part = Part.objects.get(pk=request.GET['part']) + part = Part.objects.get(pk=part) queryset = queryset.filter(id__in=[p.id for p in part.purchase_orders()]) except (Part.DoesNotExist, ValueError): pass # Attempt to filter by supplier part - if 'supplier_part' in request.GET: + supplier_part = params.get('supplier_part', None) + + if supplier_part is not None: try: - supplier_part = SupplierPart.objects.get(pk=request.GET['supplier_part']) + supplier_part = SupplierPart.objects.get(pk=supplier_part) queryset = queryset.filter(id__in=[p.id for p in supplier_part.purchase_orders()]) except (ValueError, SupplierPart.DoesNotExist): pass - data = queryset.values( - 'pk', - 'supplier', - 'supplier_reference', - 'supplier__name', - 'supplier__image', - 'reference', - 'description', - 'link', - 'status', - 'notes', - 'creation_date', - ) - - for item in data: - - order = queryset.get(pk=item['pk']) - - item['supplier__image'] = os.path.join(settings.MEDIA_URL, item['supplier__image']) - item['status_text'] = OrderStatus.label(item['status']) - item['lines'] = order.lines.count() - - return Response(data) + return queryset permission_classes = [ permissions.IsAuthenticated, @@ -123,6 +125,31 @@ class PODetail(generics.RetrieveUpdateAPIView): queryset = PurchaseOrder.objects.all() serializer_class = POSerializer + def get_serializer(self, *args, **kwargs): + + try: + kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', False)) + 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 = super().get_queryset(*args, **kwargs) + + queryset = queryset.prefetch_related( + 'supplier', + 'lines', + ) + + queryset = POSerializer.annotate_queryset(queryset) + + return queryset + permission_classes = [ permissions.IsAuthenticated ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 9a8f1afee5..ab1471cdc1 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -5,7 +5,12 @@ JSON serializers for the Order API # -*- coding: utf-8 -*- from __future__ import unicode_literals +from rest_framework import serializers + +from django.db.models import Count + from InvenTree.serializers import InvenTreeModelSerializer +from company.serializers import CompanyBriefSerializer from .models import PurchaseOrder, PurchaseOrderLineItem @@ -13,17 +18,45 @@ from .models import PurchaseOrder, PurchaseOrderLineItem class POSerializer(InvenTreeModelSerializer): """ Serializes an Order object """ + def __init__(self, *args, **kwargs): + + supplier_detail = kwargs.pop('supplier_detail', False) + + super().__init__(*args, **kwargs) + + if supplier_detail is not True: + self.fields.pop('supplier_detail') + + @staticmethod + def annotate_queryset(queryset): + """ + Add extra information to the queryset + """ + + return queryset.annotate( + line_items=Count('lines'), + ) + + supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) + + line_items = serializers.IntegerField(read_only=True) + + status_text = serializers.CharField(source='get_status_display', read_only=True) + class Meta: model = PurchaseOrder fields = [ 'pk', - 'supplier', - 'supplier_reference', - 'reference', 'description', + 'line_items', 'link', + 'reference', + 'supplier', + 'supplier_detail', + 'supplier_reference', 'status', + 'status_text', 'notes', ] From 746e9ab98358dedc18ce3bb9aa66a4a5fcee869e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 09:48:33 +1000 Subject: [PATCH 5/6] Fix rendering of PurchaseOrder table --- .../static/script/inventree/order.js | 20 ++++++++++--------- InvenTree/order/serializers.py | 3 +++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/order.js b/InvenTree/InvenTree/static/script/inventree/order.js index 12b2fd4333..d583ebcdc4 100644 --- a/InvenTree/InvenTree/static/script/inventree/order.js +++ b/InvenTree/InvenTree/static/script/inventree/order.js @@ -104,21 +104,23 @@ function removePurchaseOrderLineItem(e) { function loadPurchaseOrderTable(table, options) { /* Create a purchase-order table */ - var params = options.params || {}; + options.params = options.params || {}; + + options.params['supplier_detail'] = true; var filters = loadTableFilters("order"); - for (var key in params) { - filters[key] = params[key]; + for (var key in options.params) { + filters[key] = options.params[key]; } - setupFilterList("order", table); + setupFilterList("order", $(table)); $(table).inventreeTable({ url: options.url, queryParams: filters, groupBy: false, - original: params, + original: options.params, formatNoMatches: function() { return "No purchase orders found"; }, columns: [ { @@ -131,15 +133,15 @@ function loadPurchaseOrderTable(table, options) { field: 'reference', title: 'Purchase Order', formatter: function(value, row, index, field) { - return renderLink(value, "/order/purchase-order/" + row.pk + "/"); + return renderLink(value, `/order/purchase-order/${row.pk}/`); } }, { sortable: true, - field: 'supplier', + field: 'supplier_detail', title: 'Supplier', formatter: function(value, row, index, field) { - return imageHoverIcon(row.supplier__image) + renderLink(row.supplier__name, '/company/' + value + '/purchase-orders/'); + return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`); } }, { @@ -162,7 +164,7 @@ function loadPurchaseOrderTable(table, options) { }, { sortable: true, - field: 'lines', + field: 'line_items', title: 'Items' }, ], diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index ab1471cdc1..935166e82a 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -48,6 +48,9 @@ class POSerializer(InvenTreeModelSerializer): fields = [ 'pk', + 'issue_date', + 'complete_date', + 'creation_date', 'description', 'line_items', 'link', From ff3cc96e0e4a816f0371e2fdd6f35c6c198530c4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 20 Apr 2020 09:50:41 +1000 Subject: [PATCH 6/6] PEP fixes --- InvenTree/order/api.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index cf546bee21..1dd0657930 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -8,16 +8,11 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, permissions from rest_framework import filters -from rest_framework.response import Response -from django.conf import settings from django.conf.urls import url -from InvenTree.status_codes import OrderStatus from InvenTree.helpers import str2bool -import os - from part.models import Part from company.models import SupplierPart @@ -60,7 +55,6 @@ class POList(generics.ListCreateAPIView): return queryset - def filter_queryset(self, queryset): # Perform basic filtering