Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-04-20 01:28:31 +10:00
commit c49cd9ffde
10 changed files with 287 additions and 303 deletions

View File

@ -87,6 +87,9 @@ function loadPartTable(table, url, options={}) {
* disableFilters: If true, disable custom filters * disableFilters: If true, disable custom filters
*/ */
// Ensure category detail is included
options.params['category_detail'] = true;
var params = options.params || {}; var params = options.params || {};
var filters = {}; var filters = {};
@ -184,11 +187,11 @@ function loadPartTable(table, url, options={}) {
columns.push({ columns.push({
sortable: true, sortable: true,
field: 'category__name', field: 'category_detail',
title: 'Category', title: 'Category',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
if (row.category) { if (row.category) {
return renderLink(row.category__name, "/part/category/" + row.category + "/"); return renderLink(value.pathstring, "/part/category/" + row.category + "/");
} }
else { else {
return 'No category'; return 'No category';

View File

@ -45,6 +45,10 @@ function loadStockTable(table, options) {
*/ */
// List of user-params which override the default filters // List of user-params which override the default filters
options.params['part_detail'] = true;
options.params['location_detail'] = true;
var params = options.params || {}; var params = options.params || {};
var filterListElement = options.filterList || "#filter-list-stock"; var filterListElement = options.filterList || "#filter-list-stock";
@ -83,27 +87,21 @@ function loadStockTable(table, options) {
var row = data[0]; var row = data[0];
if (field == 'part__name') { if (field == 'part_name') {
var name = row.part__IPN; var name = row.part_detail.full_name;
if (name) { return imageHoverIcon(row.part_detail.thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
name += ' | ';
}
name += row.part__name;
return imageHoverIcon(row.part__thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
} }
else if (field == 'part__description') { else if (field == 'part_description') {
return row.part__description; return row.part_detail.description;
} }
else if (field == 'quantity') { else if (field == 'quantity') {
var stock = 0; var stock = 0;
var items = 0; var items = 0;
data.forEach(function(item) { data.forEach(function(item) {
stock += item.quantity; stock += parseFloat(item.quantity);
items += 1; items += 1;
}); });
@ -216,25 +214,14 @@ function loadStockTable(table, options) {
visible: false, visible: false,
}, },
{ {
field: 'part__name', field: 'part_name',
title: 'Part', title: 'Part',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var name = row.part__IPN;
if (name) {
name += ' | ';
}
name += row.part__name;
if (row.part__revision) {
name += " | ";
name += row.part__revision;
}
var url = ''; var url = '';
var thumb = row.part_detail.thumbnail;
var name = row.part_detail.full_name;
if (row.supplier_part) { if (row.supplier_part) {
url = `/supplier-part/${row.supplier_part}/`; url = `/supplier-part/${row.supplier_part}/`;
@ -242,13 +229,16 @@ function loadStockTable(table, options) {
url = `/part/${row.part}/`; url = `/part/${row.part}/`;
} }
return imageHoverIcon(row.part__thumbnail) + renderLink(name, url); return imageHoverIcon(thumb) + renderLink(name, url);
} }
}, },
{ {
field: 'part__description', field: 'part_description',
title: 'Description', title: 'Description',
sortable: true, sortable: true,
formatter: function(value, row, index, field) {
return row.part_detail.description;
}
}, },
{ {
field: 'quantity', field: 'quantity',
@ -256,11 +246,13 @@ function loadStockTable(table, options) {
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var val = value; var val = parseFloat(value);
// If there is a single unit with a serial number, use the serial number // If there is a single unit with a serial number, use the serial number
if (row.serial && row.quantity == 1) { if (row.serial && row.quantity == 1) {
val = '# ' + row.serial; val = '# ' + row.serial;
} else {
val = +val.toFixed(5);
} }
var text = renderLink(val, '/stock/item/' + row.pk + '/'); var text = renderLink(val, '/stock/item/' + row.pk + '/');
@ -282,7 +274,7 @@ function loadStockTable(table, options) {
sortable: true, sortable: true,
}, },
{ {
field: 'location__path', field: 'location_detail.pathstring',
title: 'Location', title: 'Location',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {

View File

@ -4,8 +4,9 @@ Provides information on the current InvenTree version
import subprocess import subprocess
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
import django
INVENTREE_SW_VERSION = "0.0.11_pre" INVENTREE_SW_VERSION = "0.0.12 pre"
def inventreeInstanceName(): def inventreeInstanceName():
@ -18,6 +19,11 @@ def inventreeVersion():
return INVENTREE_SW_VERSION return INVENTREE_SW_VERSION
def inventreeDjangoVersion():
""" Return the version of Django library """
return django.get_version()
def inventreeCommitHash(): def inventreeCommitHash():
""" Returns the git commit hash for the running codebase """ """ Returns the git commit hash for the running codebase """

View File

@ -6,10 +6,8 @@ Provides a JSON API for the Part app
from __future__ import unicode_literals from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.conf import settings
from django.db.models import Q, F, Sum, Count from django.db.models import Q, F, Count
from django.db.models.functions import Coalesce
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
@ -19,15 +17,11 @@ from rest_framework import generics, permissions
from django.conf.urls import url, include from django.conf.urls import url, include
from django.urls import reverse from django.urls import reverse
import os
from decimal import Decimal
from .models import Part, PartCategory, BomItem, PartStar from .models import Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate from .models import PartParameter, PartParameterTemplate
from . import serializers as part_serializers from . import serializers as part_serializers
from InvenTree.status_codes import OrderStatus, StockStatus, BuildStatus
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull from InvenTree.helpers import str2bool, isNull
@ -125,6 +119,8 @@ class PartThumbs(generics.ListAPIView):
# Get all Parts which have an associated image # Get all Parts which have an associated image
queryset = Part.objects.all().exclude(image='') queryset = Part.objects.all().exclude(image='')
# TODO - We should return the thumbnails here, not the full image!
# Return the most popular parts first # Return the most popular parts first
data = queryset.values( data = queryset.values(
'image', 'image',
@ -166,6 +162,31 @@ class PartList(generics.ListCreateAPIView):
serializer_class = part_serializers.PartSerializer serializer_class = part_serializers.PartSerializer
queryset = Part.objects.all()
starred_parts = None
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)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" Override the default 'create' behaviour: """ Override the default 'create' behaviour:
We wish to save the user who created this part! We wish to save the user who created this part!
@ -184,129 +205,20 @@ class PartList(generics.ListCreateAPIView):
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def list(self, request, *args, **kwargs): def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
return queryset
def filter_queryset(self, queryset):
""" """
Instead of using the DRF serialiser to LIST, Perform custom filtering of the queryset
we serialize the objects manually.
This turns out to be significantly faster.
""" """
queryset = self.filter_queryset(self.get_queryset()) # Perform basic filtering
queryset = super().filter_queryset(queryset)
# Filters for annotations
# "in_stock" count should only sum stock items which are "in stock"
stock_filter = Q(stock_items__status__in=StockStatus.AVAILABLE_CODES)
# "on_order" items should only sum orders which are currently outstanding
order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN)
# "building" should only reference builds which are active
build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES)
# Set of fields we wish to serialize
data = queryset.values(
'pk',
'category',
'image',
'name',
'IPN',
'revision',
'description',
'keywords',
'is_template',
'link',
'units',
'minimum_stock',
'trackable',
'assembly',
'component',
'salable',
'active',
).annotate(
# Quantity of items which are "in stock"
in_stock=Coalesce(Sum('stock_items__quantity', filter=stock_filter), Decimal(0)),
on_order=Coalesce(Sum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter), Decimal(0)),
building=Coalesce(Sum('builds__quantity', filter=build_filter), Decimal(0)),
)
# If we are filtering by 'has_stock' status
has_stock = self.request.query_params.get('has_stock', None)
if has_stock is not None:
has_stock = str2bool(has_stock)
if has_stock:
# Filter items which have a non-null 'in_stock' quantity above zero
data = data.filter(in_stock__gt=0)
else:
# Filter items which a null or zero 'in_stock' quantity
data = data.filter(Q(in_stock__lte=0))
# If we are filtering by 'low_stock' status
low_stock = self.request.query_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
data = data.exclude(minimum_stock=0)
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
data = data.filter(Q(in_stock__lt=F('minimum_stock')))
else:
# Filter items which have an 'in_stock' level higher than 'minimum_stock'
data = data.filter(Q(in_stock__gte=F('minimum_stock')))
# Get a list of the parts that this user has starred
starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]
# Reduce the number of lookups we need to do for the part categories
categories = {}
for item in data:
if item['image']:
# Is this part 'starred' for the current user?
item['starred'] = item['pk'] in starred_parts
img = item['image']
# Use the 'thumbnail' image here instead of the full-size image
# Note: The full-size image is used when requesting the /api/part/<x>/ endpoint
if img:
fn, ext = os.path.splitext(img)
thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext)
thumb = os.path.join(settings.MEDIA_URL, thumb)
else:
thumb = ''
item['thumbnail'] = thumb
del item['image']
cat_id = item['category']
if cat_id:
if cat_id not in categories:
categories[cat_id] = PartCategory.objects.get(pk=cat_id).pathstring
item['category__name'] = categories[cat_id]
else:
item['category__name'] = None
return Response(data)
def get_queryset(self):
"""
Implement custom filtering for the Part list API
"""
# Start with all objects
parts_list = Part.objects.all()
# Filter by 'starred' parts? # Filter by 'starred' parts?
starred = str2bool(self.request.query_params.get('starred', None)) starred = str2bool(self.request.query_params.get('starred', None))
@ -315,10 +227,11 @@ class PartList(generics.ListCreateAPIView):
starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()] starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]
if starred: if starred:
parts_list = parts_list.filter(pk__in=starred_parts) queryset = queryset.filter(pk__in=starred_parts)
else: else:
parts_list = parts_list.exclude(pk__in=starred_parts) queryset = queryset.exclude(pk__in=starred_parts)
# Cascade?
cascade = str2bool(self.request.query_params.get('cascade', None)) cascade = str2bool(self.request.query_params.get('cascade', None))
# Does the user wish to filter by category? # Does the user wish to filter by category?
@ -334,7 +247,7 @@ class PartList(generics.ListCreateAPIView):
# A 'null' category is the top-level category # A 'null' category is the top-level category
if cascade is False: if cascade is False:
# Do not cascade, only list parts in the top-level category # Do not cascade, only list parts in the top-level category
parts_list = parts_list.filter(category=None) queryset = queryset.filter(category=None)
else: else:
try: try:
@ -342,17 +255,43 @@ class PartList(generics.ListCreateAPIView):
# If '?cascade=true' then include parts which exist in sub-categories # If '?cascade=true' then include parts which exist in sub-categories
if cascade: if cascade:
parts_list = parts_list.filter(category__in=category.getUniqueChildren()) queryset = queryset.filter(category__in=category.getUniqueChildren())
# Just return parts directly in the requested category # Just return parts directly in the requested category
else: else:
parts_list = parts_list.filter(category=cat_id) queryset = queryset.filter(category=cat_id)
except (ValueError, PartCategory.DoesNotExist): except (ValueError, PartCategory.DoesNotExist):
pass pass
# Ensure that related models are pre-loaded to reduce DB trips # Annotate calculated data to the queryset
parts_list = self.get_serializer_class().setup_eager_loading(parts_list) # (This will be used for further filtering)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
return parts_list # Filter by whether the part has stock
has_stock = self.request.query_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 = self.request.query_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')))
return queryset
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
@ -379,6 +318,7 @@ class PartList(generics.ListCreateAPIView):
'name', 'name',
] ]
# Default ordering
ordering = 'name' ordering = 'name'
search_fields = [ search_fields = [
@ -507,7 +447,9 @@ class BomList(generics.ListCreateAPIView):
kwargs['part_detail'] = part_detail kwargs['part_detail'] = part_detail
kwargs['sub_part_detail'] = sub_part_detail kwargs['sub_part_detail'] = sub_part_detail
# Ensure the request context is passed through!
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
def get_queryset(self): def get_queryset(self):

View File

@ -10,6 +10,12 @@ from .models import PartCategory
from .models import BomItem from .models import BomItem
from .models import PartParameter, PartParameterTemplate from .models import PartParameter, PartParameterTemplate
from decimal import Decimal
from django.db.models import Q, Sum
from django.db.models.functions import Coalesce
from InvenTree.status_codes import StockStatus, OrderStatus, BuildStatus
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
@ -49,14 +55,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('category')
queryset = queryset.prefetch_related('stock_items')
queryset = queryset.prefetch_related('bom_items')
queryset = queryset.prefetch_related('builds')
return queryset
class Meta: class Meta:
model = Part model = Part
fields = [ fields = [
@ -64,8 +62,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
'url', 'url',
'full_name', 'full_name',
'description', 'description',
'total_stock',
'available_stock',
'thumbnail', 'thumbnail',
'active', 'active',
'assembly', 'assembly',
@ -78,57 +74,140 @@ class PartSerializer(InvenTreeModelSerializer):
Used when displaying all details of a single component. Used when displaying all details of a single component.
""" """
allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) def __init__(self, *args, **kwargs):
bom_items = serializers.IntegerField(source='bom_count', read_only=True) """
building = serializers.FloatField(source='quantity_being_built', read_only=False) Custom initialization method for PartSerializer,
category_name = serializers.CharField(source='category_path', read_only=True) so that we can optionally pass extra fields based on the query.
image = serializers.CharField(source='get_image_url', read_only=True) """
on_order = serializers.FloatField(read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) self.starred_parts = kwargs.pop('starred_parts', [])
url = serializers.CharField(source='get_absolute_url', read_only=True)
used_in = serializers.IntegerField(source='used_in_count', read_only=True) category_detail = kwargs.pop('category_detail', False)
super().__init__(*args, **kwargs)
if category_detail is not True:
self.fields.pop('category_detail')
@staticmethod @staticmethod
def setup_eager_loading(queryset): def prefetch_queryset(queryset):
queryset = queryset.prefetch_related('category') """
queryset = queryset.prefetch_related('stock_items') Prefetch related database tables,
queryset = queryset.prefetch_related('bom_items') to reduce database hits.
queryset = queryset.prefetch_related('builds') """
return queryset.prefetch_related(
'category',
'stock_items',
'bom_items',
'builds',
'supplier_parts',
'supplier_parts__purchase_order_line_items',
'supplier_parts__purchase_order_line_items__order',
)
@staticmethod
def annotate_queryset(queryset):
"""
Add some extra annotations to the queryset,
performing database queries as efficiently as possible,
to reduce database trips.
"""
# Filter to limit stock items to "available"
stock_filter = Q(stock_items__status__in=StockStatus.AVAILABLE_CODES)
# Filter to limit orders to "open"
order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN)
# Filter to limit builds to "active"
build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES)
# Annotate the number total stock count
queryset = queryset.annotate(
in_stock=Coalesce(Sum('stock_items__quantity', filter=stock_filter, distinct=True), Decimal(0))
)
# Annotate the number of parts "on order"
# Total "on order" parts = "Quantity" - "Received" for each active purchase order
queryset = queryset.annotate(
ordering=Coalesce(Sum(
'supplier_parts__purchase_order_line_items__quantity',
filter=order_filter,
distinct=True
), Decimal(0)) - Coalesce(Sum(
'supplier_parts__purchase_order_line_items__received',
filter=order_filter,
distinct=True
), Decimal(0))
)
# Annotate number of parts being build
queryset = queryset.annotate(
building=Coalesce(
Sum('builds__quantity', filter=build_filter, distinct=True), Decimal(0)
)
)
return queryset return queryset
# TODO - Include a 'category_detail' field which serializers the category object def get_starred(self, part):
"""
Return "true" if the part is starred by the current user.
"""
return part in self.starred_parts
# Extra detail for the category
category_detail = CategorySerializer(source='category', many=False, read_only=True)
# Calculated fields
in_stock = serializers.FloatField(read_only=True)
ordering = serializers.FloatField(read_only=True)
building = serializers.FloatField(read_only=True)
image = serializers.CharField(source='get_image_url', read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
starred = serializers.SerializerMethodField()
# TODO - Include annotation for the following fields:
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
# bom_items = serializers.IntegerField(source='bom_count', read_only=True)
# used_in = serializers.IntegerField(source='used_in_count', read_only=True)
class Meta: class Meta:
model = Part model = Part
partial = True partial = True
fields = [ fields = [
'active', 'active',
'allocated_stock', # 'allocated_stock',
'assembly', 'assembly',
'bom_items', # 'bom_items',
'building',
'category', 'category',
'category_name', 'category_detail',
'component', 'component',
'description', 'description',
'full_name', 'full_name',
'image', 'image',
'in_stock',
'ordering',
'building',
'IPN', 'IPN',
'is_template', 'is_template',
'keywords', 'keywords',
'link', 'link',
'minimum_stock',
'name', 'name',
'notes', 'notes',
'on_order',
'pk', 'pk',
'purchaseable', 'purchaseable',
'revision',
'salable', 'salable',
'starred',
'thumbnail', 'thumbnail',
'trackable', 'trackable',
'total_stock',
'units', 'units',
'used_in', # 'used_in',
'url', # Link to the part detail page
'variant_of', 'variant_of',
'virtual', 'virtual',
] ]

View File

@ -55,6 +55,12 @@ def inventree_version(*args, **kwargs):
return version.inventreeVersion() return version.inventreeVersion()
@register.simple_tag()
def django_version(*args, **kwargs):
""" Return Django version string """
return version.inventreeDjangoVersion()
@register.simple_tag() @register.simple_tag()
def inventree_commit_hash(*args, **kwargs): def inventree_commit_hash(*args, **kwargs):
""" Return InvenTree git commit hash string """ """ Return InvenTree git commit hash string """

View File

@ -5,7 +5,6 @@ JSON API for the Stock app
from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from django_filters import NumberFilter from django_filters import NumberFilter
from django.conf import settings
from django.conf.urls import url, include from django.conf.urls import url, include
from django.urls import reverse from django.urls import reverse
from django.db.models import Q from django.db.models import Q
@ -21,9 +20,7 @@ from .serializers import StockTrackingSerializer
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull from InvenTree.helpers import str2bool, isNull
from InvenTree.status_codes import StockStatus
import os
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
@ -317,13 +314,15 @@ class StockList(generics.ListCreateAPIView):
- status: Filter by the StockItem status - status: Filter by the StockItem status
""" """
serializer_class = StockItemSerializer
queryset = StockItem.objects.all() queryset = StockItem.objects.all()
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
try: try:
part_detail = str2bool(self.request.GET.get('part_detail', None)) part_detail = str2bool(self.request.query_params.get('part_detail', None))
location_detail = str2bool(self.request.GET.get('location_detail', None)) location_detail = str2bool(self.request.query_params.get('location_detail', None))
except AttributeError: except AttributeError:
part_detail = None part_detail = None
location_detail = None location_detail = None
@ -331,86 +330,25 @@ class StockList(generics.ListCreateAPIView):
kwargs['part_detail'] = part_detail kwargs['part_detail'] = part_detail
kwargs['location_detail'] = location_detail kwargs['location_detail'] = location_detail
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
def list(self, request, *args, **kwargs): # TODO - Override the 'create' method for this view,
# to allow the user to be recorded when a new StockItem object is created
queryset = self.filter_queryset(self.get_queryset()) def get_queryset(self, *args, **kwargs):
# Instead of using the DRF serializer to LIST, queryset = super().get_queryset(*args, **kwargs)
# we will serialize the objects manually. queryset = StockItemSerializer.prefetch_queryset(queryset)
# This is significantly faster
data = queryset.values( return queryset
'pk',
'uid',
'parent',
'quantity',
'serial',
'batch',
'status',
'notes',
'link',
'location',
'location__name',
'location__description',
'part',
'part__IPN',
'part__name',
'part__revision',
'part__description',
'part__image',
'part__category',
'part__category__name',
'part__category__description',
'supplier_part',
)
# Reduce the number of lookups we need to do for categories def filter_queryset(self, queryset):
# Cache location lookups for this query
locations = {}
for item in data:
img = item['part__image']
if img:
# Use the thumbnail image instead
fn, ext = os.path.splitext(img)
thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext)
thumb = os.path.join(settings.MEDIA_URL, thumb)
else:
thumb = ''
item['part__thumbnail'] = thumb
del item['part__image']
loc_id = item['location']
if loc_id:
if loc_id not in locations:
locations[loc_id] = StockLocation.objects.get(pk=loc_id).pathstring
item['location__path'] = locations[loc_id]
else:
item['location__path'] = None
item['status_text'] = StockStatus.label(item['status'])
return Response(data)
def get_queryset(self):
"""
If the query includes a particular location,
we may wish to also request stock items from all child locations.
"""
# Start with all objects # Start with all objects
stock_list = super(StockList, self).get_queryset() stock_list = super().filter_queryset(queryset)
# Filter out parts which are not actually "in stock" # Filter out parts which are not actually "in stock"
stock_list = stock_list.filter(customer=None, belongs_to=None) stock_list = stock_list.filter(customer=None, belongs_to=None)

View File

@ -8,7 +8,6 @@ from .models import StockItem, StockLocation
from .models import StockItemTracking from .models import StockItemTracking
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
from company.serializers import SupplierPartSerializer
from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer
@ -56,24 +55,43 @@ class StockItemSerializer(InvenTreeModelSerializer):
- Includes serialization for the item location - Includes serialization for the item location
""" """
url = serializers.CharField(source='get_absolute_url', read_only=True) @staticmethod
def prefetch_queryset(queryset):
"""
Prefetch related database tables,
to reduce database hits.
"""
return queryset.prefetch_related(
'supplier_part',
'supplier_part__supplier',
'supplier_part__manufacturer',
'location',
'part',
'tracking_info',
)
@staticmethod
def annotate_queryset(queryset):
"""
Add some extra annotations to the queryset,
performing database queries as efficiently as possible.
"""
# TODO
pass
status_text = serializers.CharField(source='get_status_display', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True)
part_name = serializers.CharField(source='get_part_name', read_only=True)
part_image = serializers.CharField(source='part__image', read_only=True)
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
supplier_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True)
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False) part_detail = kwargs.pop('part_detail', False)
location_detail = kwargs.pop('location_detail', False) location_detail = kwargs.pop('location_detail', False)
supplier_detail = kwargs.pop('supplier_detail', False)
super(StockItemSerializer, self).__init__(*args, **kwargs) super(StockItemSerializer, self).__init__(*args, **kwargs)
@ -83,9 +101,6 @@ class StockItemSerializer(InvenTreeModelSerializer):
if location_detail is not True: if location_detail is not True:
self.fields.pop('location_detail') self.fields.pop('location_detail')
if supplier_detail is not True:
self.fields.pop('supplier_detail')
class Meta: class Meta:
model = StockItem model = StockItem
fields = [ fields = [
@ -97,18 +112,14 @@ class StockItemSerializer(InvenTreeModelSerializer):
'notes', 'notes',
'part', 'part',
'part_detail', 'part_detail',
'part_name',
'part_image',
'pk', 'pk',
'quantity', 'quantity',
'serial', 'serial',
'supplier_part', 'supplier_part',
'supplier_detail',
'status', 'status',
'status_text', 'status_text',
'tracking_items', 'tracking_items',
'uid', 'uid',
'url',
] ]
""" These fields are read-only in this context. """ These fields are read-only in this context.

View File

@ -25,6 +25,10 @@
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "InvenTree Version" %}</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td> <td>{% trans "InvenTree Version" %}</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td>
</tr> </tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Django Version" %}</td><td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
</tr>
<tr> <tr>
<td><span class='fas fa-code-branch'></span></td> <td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Commit Hash" %}</td><td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit_hash %}">{% inventree_commit_hash %}</a></td> <td>{% trans "Commit Hash" %}</td><td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit_hash %}">{% inventree_commit_hash %}</a></td>
@ -45,7 +49,7 @@
<tr> <tr>
<td><span class='fas fa-exclamation-circle'></span></td> <td><span class='fas fa-exclamation-circle'></span></td>
<td>{% trans "Submit Bug Report" %}</td> <td>{% trans "Submit Bug Report" %}</td>
<td><a href='{% inventree_github_url %}/issues'>{% inventree_github_url %}/issues</a></td> <td><a href='{% inventree_github_url %}/issues'>{% inventree_github_url %}issues</a></td>
</tr> </tr>
</table> </table>

View File

@ -1,11 +1,11 @@
wheel>=0.34.2 # Wheel wheel>=0.34.2 # Wheel
Django==2.2.10 # Django package Django==3.0.5 # Django package
pillow==6.2.0 # Image manipulation pillow==6.2.0 # Image manipulation
djangorestframework==3.10.3 # DRF framework djangorestframework==3.10.3 # DRF framework
django-dbbackup==3.3.0 # Database backup / restore functionality
django-cors-headers==3.2.0 # CORS headers extension for DRF django-cors-headers==3.2.0 # CORS headers extension for DRF
django_filter==2.2.0 # Extended filtering options django_filter==2.2.0 # Extended filtering options
django-mptt==0.10.0 # Modified Preorder Tree Traversal django-mptt==0.11.0 # Modified Preorder Tree Traversal
django-dbbackup==3.2.0 # Database backup / restore functionality
django-markdownx==3.0.1 # Markdown form fields django-markdownx==3.0.1 # Markdown form fields
django-markdownify==0.8.0 # Markdown rendering django-markdownify==0.8.0 # Markdown rendering
coreapi==2.3.0 # API documentation coreapi==2.3.0 # API documentation
@ -14,9 +14,12 @@ tablib==0.13.0 # Import / export data files
django-crispy-forms==1.8.1 # Form helpers django-crispy-forms==1.8.1 # Form helpers
django-import-export==2.0.0 # Data import / export for admin interface django-import-export==2.0.0 # Data import / export for admin interface
django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files
django-qr-code==1.1.0 # Generate QR codes # TODO: Once the official django-qr-code package has been updated with Django3.x support,
# the following line should be removed.
git+git://github.com/chrissam/django-qr-code
# django-qr-code==1.1.0 # Generate QR codes
flake8==3.3.0 # PEP checking flake8==3.3.0 # PEP checking
coverage==4.0.3 # Unit test coverage coverage==4.0.3 # Unit test coverage
python-coveralls==2.9.1 # Coveralls linking (for Travis) python-coveralls==2.9.1 # Coveralls linking (for Travis)
rapidfuzz==0.2.1 # Fuzzy string matching rapidfuzz==0.7.6 # Fuzzy string matching
django-stdimage==5.0.3 # Advanced ImageField management django-stdimage==5.1.1 # Advanced ImageField management