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
*/
// 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';

View File

@ -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) {

View File

@ -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 """

View File

@ -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):

View File

@ -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',
]

View File

@ -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 """

View File

@ -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)

View File

@ -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.

View File

@ -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>

View File

@ -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