diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index e385a2e4e6..0c050f86cf 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 208 +INVENTREE_API_VERSION = 209 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v209 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7514 + - Add "top_level" filter to PartCategory API endpoint + - Add "top_level" filter to StockLocation API endpoint + v208 - 2024-06-19 : https://github.com/inventree/InvenTree/pull/7479 - Adds documentation for the user roles API endpoint (no functional changes) diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index d558542abe..777850d4c2 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -779,7 +779,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel): on_delete=models.DO_NOTHING, blank=True, null=True, - verbose_name=_('parent'), + verbose_name='parent', related_name='children', ) diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index acfe95eaca..9634e5071d 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -137,6 +137,21 @@ class CategoryFilter(rest_filters.FilterSet): return queryset + top_level = rest_filters.BooleanFilter( + label=_('Top Level'), + method='filter_top_level', + help_text=_('Filter by top-level categories'), + ) + + def filter_top_level(self, queryset, name, value): + """Filter by top-level categories.""" + cascade = str2bool(self.data.get('cascade', False)) + + if value and not cascade: + return queryset.filter(parent=None) + + return queryset + cascade = rest_filters.BooleanFilter( label=_('Cascade'), method='filter_cascade', @@ -148,10 +163,11 @@ class CategoryFilter(rest_filters.FilterSet): Note: If the "parent" filter is provided, we offload the logic to that method. """ - parent = self.data.get('parent', None) + parent = str2bool(self.data.get('parent', None)) + top_level = str2bool(self.data.get('top_level', None)) # If the parent is *not* provided, update the results based on the "cascade" value - if not parent: + if not parent or top_level: if not value: # If "cascade" is False, only return top-level categories queryset = queryset.filter(parent=None) diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 6bdc61cd6b..90af41890e 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -111,6 +111,14 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer): return queryset + parent = serializers.PrimaryKeyRelatedField( + queryset=PartCategory.objects.all(), + required=False, + allow_null=True, + label=_('Parent Category'), + help_text=_('Parent part category'), + ) + url = serializers.CharField(source='get_absolute_url', read_only=True) part_count = serializers.IntegerField(read_only=True, label=_('Parts')) diff --git a/src/backend/InvenTree/part/templates/part/category.html b/src/backend/InvenTree/part/templates/part/category.html index 734e3ae98b..5671ca44c5 100644 --- a/src/backend/InvenTree/part/templates/part/category.html +++ b/src/backend/InvenTree/part/templates/part/category.html @@ -317,6 +317,8 @@ params: { {% if category %} parent: {{ category.pk }}, + {% else %} + top_level: true, {% endif %} }, allowTreeView: true, diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 4b6903bf96..99ae920f39 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -326,6 +326,21 @@ class StockLocationFilter(rest_filters.FilterSet): return queryset + top_level = rest_filters.BooleanFilter( + label=_('Top Level'), + method='filter_top_level', + help_text=_('Filter by top-level locations'), + ) + + def filter_top_level(self, queryset, name, value): + """Filter by top-level locations.""" + cascade = str2bool(self.data.get('cascade', False)) + + if value and not cascade: + return queryset.filter(parent=None) + + return queryset + cascade = rest_filters.BooleanFilter( label=_('Cascade'), method='filter_cascade', @@ -338,9 +353,10 @@ class StockLocationFilter(rest_filters.FilterSet): Note: If the "parent" filter is provided, we offload the logic to that method. """ parent = self.data.get('parent', None) + top_level = str2bool(self.data.get('top_level', None)) # If the parent is *not* provided, update the results based on the "cascade" value - if not parent: + if not parent or top_level: if not value: # If "cascade" is False, only return top-level location queryset = queryset.filter(parent=None) diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index c727741895..a305bb90d2 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -1077,6 +1077,15 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): return queryset + parent = serializers.PrimaryKeyRelatedField( + queryset=StockLocation.objects.all(), + many=False, + allow_null=True, + required=False, + label=_('Parent Location'), + help_text=_('Parent stock location'), + ) + url = serializers.CharField(source='get_absolute_url', read_only=True) items = serializers.IntegerField(read_only=True, label=_('Stock Items')) diff --git a/src/backend/InvenTree/stock/templates/stock/location.html b/src/backend/InvenTree/stock/templates/stock/location.html index edd44c70f5..6c052e517b 100644 --- a/src/backend/InvenTree/stock/templates/stock/location.html +++ b/src/backend/InvenTree/stock/templates/stock/location.html @@ -280,6 +280,8 @@ params: { {% if location %} parent: {{ location.pk }}, + {% else %} + top_level: true, {% endif %} }, allowTreeView: true, diff --git a/src/frontend/src/tables/part/PartCategoryTable.tsx b/src/frontend/src/tables/part/PartCategoryTable.tsx index 90576ee729..7f4de4fd86 100644 --- a/src/frontend/src/tables/part/PartCategoryTable.tsx +++ b/src/frontend/src/tables/part/PartCategoryTable.tsx @@ -130,7 +130,8 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) { props={{ enableDownload: true, params: { - parent: parentId + parent: parentId, + top_level: parentId === undefined ? true : undefined }, tableFilters: tableFilters, tableActions: tableActions, diff --git a/src/frontend/src/tables/stock/StockLocationTable.tsx b/src/frontend/src/tables/stock/StockLocationTable.tsx index 0481dde938..ceaf7ad3d4 100644 --- a/src/frontend/src/tables/stock/StockLocationTable.tsx +++ b/src/frontend/src/tables/stock/StockLocationTable.tsx @@ -145,7 +145,8 @@ export function StockLocationTable({ parentId }: { parentId?: any }) { enableLabels: true, enableReports: true, params: { - parent: parentId + parent: parentId, + top_level: parentId === undefined ? true : undefined }, tableFilters: tableFilters, tableActions: tableActions,