mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
c49cd9ffde
@ -87,6 +87,9 @@ function loadPartTable(table, url, options={}) {
|
||||
* disableFilters: If true, disable custom filters
|
||||
*/
|
||||
|
||||
// Ensure category detail is included
|
||||
options.params['category_detail'] = true;
|
||||
|
||||
var params = options.params || {};
|
||||
|
||||
var filters = {};
|
||||
@ -184,11 +187,11 @@ function loadPartTable(table, url, options={}) {
|
||||
|
||||
columns.push({
|
||||
sortable: true,
|
||||
field: 'category__name',
|
||||
field: 'category_detail',
|
||||
title: 'Category',
|
||||
formatter: function(value, row, index, field) {
|
||||
if (row.category) {
|
||||
return renderLink(row.category__name, "/part/category/" + row.category + "/");
|
||||
return renderLink(value.pathstring, "/part/category/" + row.category + "/");
|
||||
}
|
||||
else {
|
||||
return 'No category';
|
||||
|
@ -43,8 +43,12 @@ function loadStockTable(table, options) {
|
||||
* filterList - <ul> element where filters are displayed
|
||||
* disableFilters: If true, disable custom 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 filterListElement = options.filterList || "#filter-list-stock";
|
||||
@ -83,27 +87,21 @@ function loadStockTable(table, options) {
|
||||
|
||||
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) {
|
||||
name += ' | ';
|
||||
}
|
||||
|
||||
name += row.part__name;
|
||||
|
||||
return imageHoverIcon(row.part__thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
|
||||
return imageHoverIcon(row.part_detail.thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
|
||||
}
|
||||
else if (field == 'part__description') {
|
||||
return row.part__description;
|
||||
else if (field == 'part_description') {
|
||||
return row.part_detail.description;
|
||||
}
|
||||
else if (field == 'quantity') {
|
||||
var stock = 0;
|
||||
var items = 0;
|
||||
|
||||
data.forEach(function(item) {
|
||||
stock += item.quantity;
|
||||
stock += parseFloat(item.quantity);
|
||||
items += 1;
|
||||
});
|
||||
|
||||
@ -216,25 +214,14 @@ function loadStockTable(table, options) {
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
field: 'part__name',
|
||||
field: 'part_name',
|
||||
title: 'Part',
|
||||
sortable: true,
|
||||
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 thumb = row.part_detail.thumbnail;
|
||||
var name = row.part_detail.full_name;
|
||||
|
||||
if (row.supplier_part) {
|
||||
url = `/supplier-part/${row.supplier_part}/`;
|
||||
@ -242,13 +229,16 @@ function loadStockTable(table, options) {
|
||||
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',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
return row.part_detail.description;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
@ -256,11 +246,13 @@ function loadStockTable(table, options) {
|
||||
sortable: true,
|
||||
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 (row.serial && row.quantity == 1) {
|
||||
val = '# ' + row.serial;
|
||||
} else {
|
||||
val = +val.toFixed(5);
|
||||
}
|
||||
|
||||
var text = renderLink(val, '/stock/item/' + row.pk + '/');
|
||||
@ -282,7 +274,7 @@ function loadStockTable(table, options) {
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'location__path',
|
||||
field: 'location_detail.pathstring',
|
||||
title: 'Location',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
|
@ -4,8 +4,9 @@ Provides information on the current InvenTree version
|
||||
|
||||
import subprocess
|
||||
from common.models import InvenTreeSetting
|
||||
import django
|
||||
|
||||
INVENTREE_SW_VERSION = "0.0.11_pre"
|
||||
INVENTREE_SW_VERSION = "0.0.12 pre"
|
||||
|
||||
|
||||
def inventreeInstanceName():
|
||||
@ -18,6 +19,11 @@ def inventreeVersion():
|
||||
return INVENTREE_SW_VERSION
|
||||
|
||||
|
||||
def inventreeDjangoVersion():
|
||||
""" Return the version of Django library """
|
||||
return django.get_version()
|
||||
|
||||
|
||||
def inventreeCommitHash():
|
||||
""" Returns the git commit hash for the running codebase """
|
||||
|
||||
|
@ -6,10 +6,8 @@ Provides a JSON API for the Part app
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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.functions import Coalesce
|
||||
from django.db.models import Q, F, Count
|
||||
|
||||
from rest_framework import status
|
||||
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.urls import reverse
|
||||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
from .models import Part, PartCategory, BomItem, PartStar
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
|
||||
from . import serializers as part_serializers
|
||||
|
||||
from InvenTree.status_codes import OrderStatus, StockStatus, BuildStatus
|
||||
from InvenTree.views import TreeSerializer
|
||||
from InvenTree.helpers import str2bool, isNull
|
||||
|
||||
@ -125,6 +119,8 @@ class PartThumbs(generics.ListAPIView):
|
||||
# Get all Parts which have an associated image
|
||||
queryset = Part.objects.all().exclude(image='')
|
||||
|
||||
# TODO - We should return the thumbnails here, not the full image!
|
||||
|
||||
# Return the most popular parts first
|
||||
data = queryset.values(
|
||||
'image',
|
||||
@ -166,6 +162,31 @@ class PartList(generics.ListCreateAPIView):
|
||||
|
||||
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):
|
||||
""" Override the default 'create' behaviour:
|
||||
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)
|
||||
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,
|
||||
we serialize the objects manually.
|
||||
This turns out to be significantly faster.
|
||||
Perform custom filtering of the queryset
|
||||
"""
|
||||
|
||||
queryset = self.filter_queryset(self.get_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()
|
||||
# Perform basic filtering
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
# Filter by 'starred' parts?
|
||||
starred = str2bool(self.request.query_params.get('starred', None))
|
||||
@ -315,12 +227,13 @@ class PartList(generics.ListCreateAPIView):
|
||||
starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]
|
||||
|
||||
if starred:
|
||||
parts_list = parts_list.filter(pk__in=starred_parts)
|
||||
queryset = queryset.filter(pk__in=starred_parts)
|
||||
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))
|
||||
|
||||
|
||||
# Does the user wish to filter by category?
|
||||
cat_id = self.request.query_params.get('category', None)
|
||||
|
||||
@ -334,7 +247,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
# A 'null' category is the top-level category
|
||||
if cascade is False:
|
||||
# Do not cascade, only list parts in the top-level category
|
||||
parts_list = parts_list.filter(category=None)
|
||||
queryset = queryset.filter(category=None)
|
||||
|
||||
else:
|
||||
try:
|
||||
@ -342,17 +255,43 @@ class PartList(generics.ListCreateAPIView):
|
||||
|
||||
# If '?cascade=true' then include parts which exist in sub-categories
|
||||
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
|
||||
else:
|
||||
parts_list = parts_list.filter(category=cat_id)
|
||||
queryset = queryset.filter(category=cat_id)
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Ensure that related models are pre-loaded to reduce DB trips
|
||||
parts_list = self.get_serializer_class().setup_eager_loading(parts_list)
|
||||
# Annotate calculated data to the queryset
|
||||
# (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 = [
|
||||
permissions.IsAuthenticated,
|
||||
@ -379,6 +318,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
'name',
|
||||
]
|
||||
|
||||
# Default ordering
|
||||
ordering = 'name'
|
||||
|
||||
search_fields = [
|
||||
@ -507,7 +447,9 @@ class BomList(generics.ListCreateAPIView):
|
||||
kwargs['part_detail'] = part_detail
|
||||
kwargs['sub_part_detail'] = sub_part_detail
|
||||
|
||||
# Ensure the request context is passed through!
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -10,6 +10,12 @@ from .models import PartCategory
|
||||
from .models import BomItem
|
||||
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
|
||||
|
||||
|
||||
@ -48,14 +54,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
|
||||
|
||||
url = serializers.CharField(source='get_absolute_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:
|
||||
model = Part
|
||||
@ -64,8 +62,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
|
||||
'url',
|
||||
'full_name',
|
||||
'description',
|
||||
'total_stock',
|
||||
'available_stock',
|
||||
'thumbnail',
|
||||
'active',
|
||||
'assembly',
|
||||
@ -78,57 +74,140 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
Used when displaying all details of a single component.
|
||||
"""
|
||||
|
||||
allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
|
||||
bom_items = serializers.IntegerField(source='bom_count', read_only=True)
|
||||
building = serializers.FloatField(source='quantity_being_built', read_only=False)
|
||||
category_name = serializers.CharField(source='category_path', read_only=True)
|
||||
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)
|
||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||
used_in = serializers.IntegerField(source='used_in_count', read_only=True)
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Custom initialization method for PartSerializer,
|
||||
so that we can optionally pass extra fields based on the query.
|
||||
"""
|
||||
|
||||
self.starred_parts = kwargs.pop('starred_parts', [])
|
||||
|
||||
category_detail = kwargs.pop('category_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if category_detail is not True:
|
||||
self.fields.pop('category_detail')
|
||||
|
||||
@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')
|
||||
def prefetch_queryset(queryset):
|
||||
"""
|
||||
Prefetch related database tables,
|
||||
to reduce database hits.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# 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:
|
||||
model = Part
|
||||
partial = True
|
||||
fields = [
|
||||
'active',
|
||||
'allocated_stock',
|
||||
# 'allocated_stock',
|
||||
'assembly',
|
||||
'bom_items',
|
||||
'building',
|
||||
# 'bom_items',
|
||||
'category',
|
||||
'category_name',
|
||||
'category_detail',
|
||||
'component',
|
||||
'description',
|
||||
'full_name',
|
||||
'image',
|
||||
'in_stock',
|
||||
'ordering',
|
||||
'building',
|
||||
'IPN',
|
||||
'is_template',
|
||||
'keywords',
|
||||
'link',
|
||||
'minimum_stock',
|
||||
'name',
|
||||
'notes',
|
||||
'on_order',
|
||||
'pk',
|
||||
'purchaseable',
|
||||
'revision',
|
||||
'salable',
|
||||
'starred',
|
||||
'thumbnail',
|
||||
'trackable',
|
||||
'total_stock',
|
||||
'units',
|
||||
'used_in',
|
||||
'url', # Link to the part detail page
|
||||
# 'used_in',
|
||||
'variant_of',
|
||||
'virtual',
|
||||
]
|
||||
|
@ -55,6 +55,12 @@ def inventree_version(*args, **kwargs):
|
||||
return version.inventreeVersion()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def django_version(*args, **kwargs):
|
||||
""" Return Django version string """
|
||||
return version.inventreeDjangoVersion()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_commit_hash(*args, **kwargs):
|
||||
""" Return InvenTree git commit hash string """
|
||||
|
@ -5,7 +5,6 @@ JSON API for the Stock app
|
||||
from django_filters.rest_framework import FilterSet, DjangoFilterBackend
|
||||
from django_filters import NumberFilter
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
@ -21,9 +20,7 @@ from .serializers import StockTrackingSerializer
|
||||
|
||||
from InvenTree.views import TreeSerializer
|
||||
from InvenTree.helpers import str2bool, isNull
|
||||
from InvenTree.status_codes import StockStatus
|
||||
|
||||
import os
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from rest_framework.serializers import ValidationError
|
||||
@ -317,13 +314,15 @@ class StockList(generics.ListCreateAPIView):
|
||||
- status: Filter by the StockItem status
|
||||
"""
|
||||
|
||||
serializer_class = StockItemSerializer
|
||||
|
||||
queryset = StockItem.objects.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
part_detail = str2bool(self.request.GET.get('part_detail', None))
|
||||
location_detail = str2bool(self.request.GET.get('location_detail', None))
|
||||
part_detail = str2bool(self.request.query_params.get('part_detail', None))
|
||||
location_detail = str2bool(self.request.query_params.get('location_detail', None))
|
||||
except AttributeError:
|
||||
part_detail = None
|
||||
location_detail = None
|
||||
@ -331,86 +330,25 @@ class StockList(generics.ListCreateAPIView):
|
||||
kwargs['part_detail'] = part_detail
|
||||
kwargs['location_detail'] = location_detail
|
||||
|
||||
# Ensure the request context is passed through
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
|
||||
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,
|
||||
# we will serialize the objects manually.
|
||||
# This is significantly faster
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
||||
|
||||
data = queryset.values(
|
||||
'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',
|
||||
)
|
||||
return queryset
|
||||
|
||||
# Reduce the number of lookups we need to do for categories
|
||||
# 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.
|
||||
"""
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
# 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"
|
||||
stock_list = stock_list.filter(customer=None, belongs_to=None)
|
||||
|
@ -8,7 +8,6 @@ from .models import StockItem, StockLocation
|
||||
from .models import StockItemTracking
|
||||
|
||||
from part.serializers import PartBriefSerializer
|
||||
from company.serializers import SupplierPartSerializer
|
||||
from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer
|
||||
|
||||
|
||||
@ -56,24 +55,43 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
- 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)
|
||||
|
||||
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)
|
||||
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):
|
||||
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
location_detail = kwargs.pop('location_detail', False)
|
||||
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||
|
||||
super(StockItemSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -83,9 +101,6 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
if location_detail is not True:
|
||||
self.fields.pop('location_detail')
|
||||
|
||||
if supplier_detail is not True:
|
||||
self.fields.pop('supplier_detail')
|
||||
|
||||
class Meta:
|
||||
model = StockItem
|
||||
fields = [
|
||||
@ -97,18 +112,14 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
'notes',
|
||||
'part',
|
||||
'part_detail',
|
||||
'part_name',
|
||||
'part_image',
|
||||
'pk',
|
||||
'quantity',
|
||||
'serial',
|
||||
'supplier_part',
|
||||
'supplier_detail',
|
||||
'status',
|
||||
'status_text',
|
||||
'tracking_items',
|
||||
'uid',
|
||||
'url',
|
||||
]
|
||||
|
||||
""" These fields are read-only in this context.
|
||||
|
@ -25,6 +25,10 @@
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
@ -45,7 +49,7 @@
|
||||
<tr>
|
||||
<td><span class='fas fa-exclamation-circle'></span></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>
|
||||
</table>
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
wheel>=0.34.2 # Wheel
|
||||
Django==2.2.10 # Django package
|
||||
Django==3.0.5 # Django package
|
||||
pillow==6.2.0 # Image manipulation
|
||||
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_filter==2.2.0 # Extended filtering options
|
||||
django-mptt==0.10.0 # Modified Preorder Tree Traversal
|
||||
django-dbbackup==3.2.0 # Database backup / restore functionality
|
||||
django-mptt==0.11.0 # Modified Preorder Tree Traversal
|
||||
django-markdownx==3.0.1 # Markdown form fields
|
||||
django-markdownify==0.8.0 # Markdown rendering
|
||||
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-import-export==2.0.0 # Data import / export for admin interface
|
||||
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
|
||||
coverage==4.0.3 # Unit test coverage
|
||||
python-coveralls==2.9.1 # Coveralls linking (for Travis)
|
||||
rapidfuzz==0.2.1 # Fuzzy string matching
|
||||
django-stdimage==5.0.3 # Advanced ImageField management
|
||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||
django-stdimage==5.1.1 # Advanced ImageField management
|
Loading…
Reference in New Issue
Block a user