diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
index d8a6b94100..692ec09480 100644
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -1,11 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 176
+INVENTREE_API_VERSION = 177
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v177 - 2024-02-27 : https://github.com/inventree/InvenTree/pull/6581
+ - Adds "subcategoies" count to PartCategoryTree serializer
+ - Adds "sublocations" count to StockLocationTree serializer
+
v176 - 2024-02-26 : https://github.com/inventree/InvenTree/pull/6535
- Adds the field "plugins_install_disabled" to the Server info API endpoint
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 258c16b940..68d606a551 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -308,9 +308,17 @@ class CategoryTree(ListAPI):
filter_backends = ORDER_FILTER
+ ordering_fields = ['level', 'name', 'subcategories']
+
# Order by tree level (top levels first) and then name
ordering = ['level', 'name']
+ def get_queryset(self, *args, **kwargs):
+ """Return an annotated queryset for the CategoryTree endpoint."""
+ queryset = super().get_queryset(*args, **kwargs)
+ queryset = part_serializers.CategoryTree.annotate_queryset(queryset)
+ return queryset
+
class CategoryParameterList(ListCreateAPI):
"""API endpoint for accessing a list of PartCategoryParameterTemplate objects.
diff --git a/InvenTree/part/filters.py b/InvenTree/part/filters.py
index e16280dbe3..f7303cc190 100644
--- a/InvenTree/part/filters.py
+++ b/InvenTree/part/filters.py
@@ -281,6 +281,28 @@ def annotate_category_parts():
)
+def annotate_sub_categories():
+ """Construct a queryset annotation which returns the number of subcategories for each provided category."""
+ subquery = part.models.PartCategory.objects.filter(
+ tree_id=OuterRef('tree_id'),
+ lft__gt=OuterRef('lft'),
+ rght__lt=OuterRef('rght'),
+ level__gt=OuterRef('level'),
+ )
+
+ return Coalesce(
+ Subquery(
+ subquery.annotate(
+ total=Func(F('pk'), function='COUNT', output_field=IntegerField())
+ )
+ .values('total')
+ .order_by()
+ ),
+ 0,
+ output_field=IntegerField(),
+ )
+
+
def filter_by_parameter(queryset, template_id: int, value: str, func: str = ''):
"""Filter the given queryset by a given template parameter.
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index bbc8cd7b03..175c1b5036 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -123,7 +123,14 @@ class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
"""Metaclass defining serializer fields."""
model = PartCategory
- fields = ['pk', 'name', 'parent', 'icon', 'structural']
+ fields = ['pk', 'name', 'parent', 'icon', 'structural', 'subcategories']
+
+ subcategories = serializers.IntegerField(label=_('Subcategories'), read_only=True)
+
+ @staticmethod
+ def annotate_queryset(queryset):
+ """Annotate the queryset with the number of subcategories."""
+ return queryset.annotate(subcategories=part.filters.annotate_sub_categories())
class PartAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 459c5bf1f5..bd5d59fc4e 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -394,9 +394,17 @@ class StockLocationTree(ListAPI):
filter_backends = ORDER_FILTER
+ ordering_fields = ['level', 'name', 'sublocations']
+
# Order by tree level (top levels first) and then name
ordering = ['level', 'name']
+ def get_queryset(self, *args, **kwargs):
+ """Return annotated queryset for the StockLocationTree endpoint."""
+ queryset = super().get_queryset(*args, **kwargs)
+ queryset = StockSerializers.LocationTreeSerializer.annotate_queryset(queryset)
+ return queryset
+
class StockLocationTypeList(ListCreateAPI):
"""API endpoint for a list of StockLocationType objects.
diff --git a/InvenTree/stock/filters.py b/InvenTree/stock/filters.py
index 8a214c0622..4257bd49b9 100644
--- a/InvenTree/stock/filters.py
+++ b/InvenTree/stock/filters.py
@@ -59,3 +59,25 @@ def annotate_child_items():
0,
output_field=IntegerField(),
)
+
+
+def annotate_sub_locations():
+ """Construct a queryset annotation which returns the number of sub-locations below a certain StockLocation node in a StockLocation tree."""
+ subquery = stock.models.StockLocation.objects.filter(
+ tree_id=OuterRef('tree_id'),
+ lft__gt=OuterRef('lft'),
+ rght__lt=OuterRef('rght'),
+ level__gt=OuterRef('level'),
+ )
+
+ return Coalesce(
+ Subquery(
+ subquery.annotate(
+ count=Func(F('pk'), function='COUNT', output_field=IntegerField())
+ )
+ .values('count')
+ .order_by()
+ ),
+ 0,
+ output_field=IntegerField(),
+ )
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index ffebd78801..4574c7728d 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -858,7 +858,14 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Metaclass options."""
model = StockLocation
- fields = ['pk', 'name', 'parent', 'icon', 'structural']
+ fields = ['pk', 'name', 'parent', 'icon', 'structural', 'sublocations']
+
+ sublocations = serializers.IntegerField(label=_('Sublocations'), read_only=True)
+
+ @staticmethod
+ def annotate_queryset(queryset):
+ """Annotate the queryset with the number of sublocations."""
+ return queryset.annotate(sublocations=Count('children'))
class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
diff --git a/src/frontend/src/components/nav/PartCategoryTree.tsx b/src/frontend/src/components/nav/PartCategoryTree.tsx
index b9f730bafa..a95cfa304f 100644
--- a/src/frontend/src/components/nav/PartCategoryTree.tsx
+++ b/src/frontend/src/components/nav/PartCategoryTree.tsx
@@ -8,7 +8,11 @@ import {
useMantineTheme
} from '@mantine/core';
import { ReactTree, ThemeSettings } from '@naisutech/react-tree';
-import { IconSitemap } from '@tabler/icons-react';
+import {
+ IconChevronDown,
+ IconChevronRight,
+ IconSitemap
+} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -40,7 +44,8 @@ export function PartCategoryTree({
return {
id: category.pk,
label: category.name,
- parentId: category.parent
+ parentId: category.parent,
+ children: category.subcategories
};
})
)
@@ -67,6 +72,14 @@ export function PartCategoryTree({
);
}
+ function renderIcon({ node, open }: { node: any; open?: boolean }) {
+ if (node.children == 0) {
+ return undefined;
+ }
+
+ return open ? : ;
+ }
+
const mantineTheme = useMantineTheme();
const themes: ThemeSettings = useMemo(() => {
@@ -146,6 +159,7 @@ export function PartCategoryTree({
: ;
+ }
+
return (