diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index a7fc93bf3f..646515ce70 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -11,6 +11,8 @@ from rest_framework.exceptions import ValidationError
from django.db.models.signals import pre_delete
from django.dispatch import receiver
+from .validators import validate_tree_name
+
class InvenTreeTree(models.Model):
""" Provides an abstracted self-referencing tree model for data categories.
@@ -31,7 +33,8 @@ class InvenTreeTree(models.Model):
name = models.CharField(
blank=False,
max_length=100,
- unique=True
+ unique=True,
+ validators=[validate_tree_name]
)
description = models.CharField(
@@ -62,17 +65,22 @@ class InvenTreeTree(models.Model):
If any parents are repeated (which would be very bad!), the process is halted
"""
- if unique is None:
- unique = set()
- else:
- unique.add(self.id)
+ item = self
- if self.parent and self.parent.id not in unique:
- self.parent.getUniqueParents(unique)
+ # Prevent infinite regression
+ max_parents = 500
+
+ unique = set()
+
+ while item.parent and max_parents > 0:
+ max_parents -= 1
+
+ unique.add(item.parent.id)
+ item = item.parent
return unique
- def getUniqueChildren(self, unique=None):
+ def getUniqueChildren(self, unique=None, include_self=True):
""" Return a flat set of all child items that exist under this node.
If any child items are repeated, the repetitions are omitted.
"""
@@ -83,7 +91,8 @@ class InvenTreeTree(models.Model):
if self.id in unique:
return unique
- unique.add(self.id)
+ if include_self:
+ unique.add(self.id)
# Some magic to get around the limitations of abstract models
contents = ContentType.objects.get_for_model(type(self))
@@ -99,14 +108,6 @@ class InvenTreeTree(models.Model):
""" True if there are any children under this item """
return self.children.count() > 0
- @property
- def children(self):
- """ Return the children of this item """
- contents = ContentType.objects.get_for_model(type(self))
- childs = contents.get_all_objects_for_this_type(parent=self.id)
-
- return childs
-
def getAcceptableParents(self):
""" Returns a list of acceptable parent items within this model
Acceptable parents are ones which are not underneath this item.
@@ -159,8 +160,8 @@ class InvenTreeTree(models.Model):
"""
return '/'.join([item.name for item in self.path])
- def __setattr__(self, attrname, val):
- """ Custom Attribute Setting function
+ def clean(self):
+ """ Custom cleaning
Parent:
Setting the parent of an item to its own child results in an infinite loop.
@@ -172,28 +173,18 @@ class InvenTreeTree(models.Model):
Tree node names are limited to a reduced character set
"""
- if attrname == 'parent_id':
- # If current ID is None, continue
- # - This object is just being created
- if self.id is None:
- pass
- # Parent cannot be set to same ID (this would cause looping)
- elif val == self.id:
+ super().clean()
+
+ # Parent cannot be set to same ID (this would cause looping)
+ try:
+ if self.parent.id == self.id:
raise ValidationError("Category cannot set itself as parent")
- # Null parent is OK
- elif val is None:
- pass
- # Ensure that the new parent is not already a child
- else:
- kids = self.getUniqueChildren()
- if val in kids:
- raise ValidationError("Category cannot set a child as parent")
+ except:
+ pass
- # Prohibit certain characters from tree node names
- elif attrname == 'name':
- val = val.translate({ord(c): None for c in "!@#$%^&*'\"\\/[]{}<>,|+=~`"})
-
- super(InvenTreeTree, self).__setattr__(attrname, val)
+ # Ensure that the new parent is not already a child
+ if self.id in self.getUniqueChildren(include_self=False):
+ raise ValidationError("Category cannot set a child as parent")
def __str__(self):
""" String representation of a category is the full path to that category """
diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
index 0e1622a49a..36eda4d451 100644
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -17,6 +17,14 @@ def validate_part_name(value):
)
+def validate_tree_name(value):
+ """ Prevent illegal characters in tree item names """
+
+ for c in "!@#$%^&*'\"\\/[]{}<>,|+=~`\"":
+ if c in str(value):
+ raise ValidationError({'name': _('Illegal character in name')})
+
+
def validate_overage(value):
""" Validate that a BOM overage string is properly formatted.
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 15f93c9133..a564705ac2 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -59,19 +59,29 @@ class TreeSerializer(views.APIView):
return data
- def get(self, request, *args, **kwargs):
+ def get_items(self):
- top_items = self.model.objects.filter(parent=None).order_by('name')
+ return self.model.objects.all()
+
+ def generate_tree(self):
nodes = []
+ items = self.get_items()
+
+ # Construct the top-level items
+ top_items = [i for i in items if i.parent is None]
+
top_count = 0
+ # Construct the top-level items
+ top_items = [i for i in items if i.parent is None]
+
for item in top_items:
nodes.append(self.itemToJson(item))
top_count += item.item_count
- top = {
+ self.tree = {
'pk': None,
'text': self.title,
'href': self.root_url,
@@ -79,8 +89,13 @@ class TreeSerializer(views.APIView):
'tags': [top_count],
}
+ def get(self, request, *args, **kwargs):
+ """ Respond to a GET request for the Tree """
+
+ self.generate_tree()
+
response = {
- 'tree': [top]
+ 'tree': [self.tree]
}
return JsonResponse(response, safe=False)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index d0beee54ce..22f371c379 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -6,6 +6,9 @@ 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 Sum
from rest_framework import status
from rest_framework.response import Response
@@ -15,6 +18,8 @@ from rest_framework import generics, permissions
from django.conf.urls import url, include
from django.urls import reverse
+import os
+
from .models import Part, PartCategory, BomItem, PartStar
from .serializers import PartSerializer, BomItemSerializer
@@ -34,6 +39,9 @@ class PartCategoryTree(TreeSerializer):
def root_url(self):
return reverse('part-index')
+ def get_items(self):
+ return PartCategory.objects.all().prefetch_related('parts', 'children')
+
class CategoryList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartCategory objects.
@@ -96,6 +104,58 @@ class PartList(generics.ListCreateAPIView):
serializer_class = PartSerializer
+ def list(self, request, *args, **kwargs):
+ """
+ Instead of using the DRF serialiser to LIST,
+ we serialize the objects manuually.
+ This turns out to be significantly faster.
+ """
+
+ queryset = self.filter_queryset(self.get_queryset())
+
+ data = queryset.values(
+ 'pk',
+ 'category',
+ 'image',
+ 'name',
+ 'IPN',
+ 'description',
+ 'keywords',
+ 'is_template',
+ 'URL',
+ 'units',
+ 'trackable',
+ 'assembly',
+ 'component',
+ 'salable',
+ 'active',
+ ).annotate(
+ in_stock=Sum('stock_items__quantity'),
+ )
+
+ # TODO - Annotate total being built
+ # TODO - Annotate total on order
+
+ # Reduce the number of lookups we need to do for the part categories
+ categories = {}
+
+ for item in data:
+
+ if item['image']:
+ item['image'] = os.path.join(settings.MEDIA_URL, 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):
# Does the user wish to filter by category?
diff --git a/InvenTree/part/migrations/0008_auto_20190618_0042.py b/InvenTree/part/migrations/0008_auto_20190618_0042.py
new file mode 100644
index 0000000000..c9e0699f9b
--- /dev/null
+++ b/InvenTree/part/migrations/0008_auto_20190618_0042.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.2 on 2019-06-17 14:42
+
+import InvenTree.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0007_auto_20190602_1944'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='partcategory',
+ name='name',
+ field=models.CharField(max_length=100, unique=True, validators=[InvenTree.validators.validate_tree_name]),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 38acae3360..7d5476db5d 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -66,16 +66,24 @@ class PartCategory(InvenTreeTree):
@property
def item_count(self):
- return self.partcount
+ return self.partcount()
- @property
- def partcount(self):
+ def partcount(self, cascade=True, active=True):
""" Return the total part count under this category
(including children of child categories)
"""
- return len(Part.objects.filter(category__in=self.getUniqueChildren(),
- active=True))
+ cats = [self.id]
+
+ if cascade:
+ cats += [cat for cat in self.getUniqueChildren()]
+
+ query = Part.objects.filter(category__in=cats)
+
+ if active:
+ query = query.filter(active=True)
+
+ return query.count()
@property
def has_parts(self):
diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py
index ea52396342..3b3fe36b08 100644
--- a/InvenTree/part/test_category.py
+++ b/InvenTree/part/test_category.py
@@ -82,10 +82,10 @@ class CategoryTest(TestCase):
self.assertTrue(self.fasteners.has_parts)
self.assertFalse(self.transceivers.has_parts)
- self.assertEqual(self.fasteners.partcount, 2)
- self.assertEqual(self.capacitors.partcount, 1)
+ self.assertEqual(self.fasteners.partcount(), 2)
+ self.assertEqual(self.capacitors.partcount(), 1)
- self.assertEqual(self.electronics.partcount, 3)
+ self.assertEqual(self.electronics.partcount(), 3)
def test_delete(self):
""" Test that category deletion moves the children properly """
diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js
index d56a8d4769..83d21aa074 100644
--- a/InvenTree/static/script/inventree/part.js
+++ b/InvenTree/static/script/inventree/part.js
@@ -112,16 +112,25 @@ function loadPartTable(table, url, options={}) {
}
columns.push({
- field: 'full_name',
+ field: 'name',
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
- if (row.is_template) {
- value = '' + value + '';
+ var name = '';
+
+ if (row.IPN) {
+ name += row.IPN;
+ name += ' | ';
}
- var display = imageHoverIcon(row.image_url) + renderLink(value, row.url);
+ name += value;
+
+ if (row.is_template) {
+ name = '' + name + '';
+ }
+
+ var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/');
if (!row.active) {
display = display + "INACTIVE";
@@ -146,11 +155,11 @@ function loadPartTable(table, url, options={}) {
columns.push({
sortable: true,
- field: 'category_name',
+ field: 'category__name',
title: 'Category',
formatter: function(value, row, index, field) {
if (row.category) {
- return renderLink(row.category_name, "/part/category/" + row.category + "/");
+ return renderLink(row.category__name, "/part/category/" + row.category + "/");
}
else {
return '';
@@ -159,13 +168,13 @@ function loadPartTable(table, url, options={}) {
});
columns.push({
- field: 'total_stock',
+ field: 'in_stock',
title: 'Stock',
searchable: false,
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
- return renderLink(value, row.url + 'stock/');
+ return renderLink(value, '/part/' + row.pk + '/stock/');
}
else {
return "No Stock";
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 092724407f..8240c6b837 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -38,6 +38,9 @@ class StockCategoryTree(TreeSerializer):
def root_url(self):
return reverse('stock-index')
+ def get_items(self):
+ return StockLocation.objects.all().prefetch_related('stock_items', 'children')
+
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object
diff --git a/InvenTree/stock/migrations/0007_auto_20190618_0042.py b/InvenTree/stock/migrations/0007_auto_20190618_0042.py
new file mode 100644
index 0000000000..862ea94273
--- /dev/null
+++ b/InvenTree/stock/migrations/0007_auto_20190618_0042.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.2 on 2019-06-17 14:42
+
+import InvenTree.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0006_stockitem_purchase_order'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='stocklocation',
+ name='name',
+ field=models.CharField(max_length=100, unique=True, validators=[InvenTree.validators.validate_tree_name]),
+ ),
+ ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index a53e61f86d..e992b4eb9f 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -49,19 +49,23 @@ class StockLocation(InvenTreeTree):
}
)
- @property
- def stock_item_count(self):
+ def stock_item_count(self, cascade=True):
""" Return the number of StockItem objects which live in or under this category
"""
- return StockItem.objects.filter(location__in=self.getUniqueChildren()).count()
+ if cascade:
+ query = StockItem.objects.filter(location__in=self.getUniqueChildren())
+ else:
+ query = StockItem.objects.filter(location=self)
+
+ return query.count()
@property
def item_count(self):
""" Simply returns the number of stock items in this location.
Required for tree view serializer.
"""
- return self.stock_item_count
+ return self.stock_item_count()
@receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log')