2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 06:01:35 +00:00

Refactor (backend): Improve BuildItemList API filters (#10279)

* refactor(stock): improve StockList api filters

* update PR numver in api_version

* Update src/backend/InvenTree/InvenTree/api_version.py

Co-authored-by: Matthias Mair <code@mjmair.com>

* Fix MySQL test failure caused by self-referential FK constraint in StockItem

* Data import fix (#10298)

* Data import fix

- Improved error handling

* Tweak frontend display of errors

* chore(deps-dev): bump vite from 6.3.5 to 6.3.6 in /src/frontend (#10297)

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Reza
2025-09-11 01:55:30 +03:30
committed by GitHub
parent 5099494180
commit 8adfa234bb
5 changed files with 85 additions and 56 deletions

View File

@@ -1,11 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 390
INVENTREE_API_VERSION = 391
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v391 -> 2025-09-06 : https://github.com/inventree/InvenTree/pull/10279
- Refactors 'exclude_tree', 'cascade', and 'location' filters in StockList API endpoint
v390 -> 2025-09-03 : https://github.com/inventree/InvenTree/pull/10257
- Fixes limitation on adding virtual parts to a SalesOrder
- Additional query filter options for BomItem API endpoint

View File

@@ -174,8 +174,8 @@ class StaleStockNotificationTests(InvenTreeTestCase):
def test_check_stale_stock_no_stale_items(self):
"""Test check_stale_stock when no stale items exist."""
# Clear all existing stock items
stock.models.StockItem.objects.update(parent=None)
stock.models.StockItem.objects.all().delete()
# Create only future expiry items
today = helpers.current_date()
stock.models.StockItem.objects.create(
@@ -194,6 +194,7 @@ class StaleStockNotificationTests(InvenTreeTestCase):
def test_check_stale_stock_with_stale_items(self, mock_offload):
"""Test check_stale_stock when stale items exist."""
# Clear existing stock items
stock.models.StockItem.objects.update(parent=None)
stock.models.StockItem.objects.all().delete()
self.create_stock_items_with_expiry()
@@ -229,6 +230,7 @@ class StaleStockNotificationTests(InvenTreeTestCase):
def test_check_stale_stock_filtering(self):
"""Test that check_stale_stock properly filters stock items."""
# Clear all existing stock items first
stock.models.StockItem.objects.update(parent=None)
stock.models.StockItem.objects.all().delete()
today = helpers.current_date()

View File

@@ -39,8 +39,9 @@ from InvenTree.filters import (
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
InvenTreeDateFilter,
NumberOrNullFilter,
)
from InvenTree.helpers import extract_serial_numbers, generateTestKey, isNull, str2bool
from InvenTree.helpers import extract_serial_numbers, generateTestKey, str2bool
from InvenTree.mixins import (
CreateAPI,
CustomRetrieveUpdateDestroyAPI,
@@ -933,6 +934,66 @@ class StockFilter(rest_filters.FilterSet):
else:
return queryset.exclude(stale_filter)
exclude_tree = rest_filters.NumberFilter(
method='filter_exclude_tree',
label=_('Exclude Tree'),
help_text=_(
'Provide a StockItem PK to exclude that item and all its descendants'
),
)
def filter_exclude_tree(self, queryset, name, value):
"""Exclude a StockItem and all of its descendants from the queryset."""
try:
root = StockItem.objects.get(pk=value)
pks_to_exclude = [
item.pk for item in root.get_descendants(include_self=True)
]
return queryset.exclude(pk__in=pks_to_exclude)
except (ValueError, StockItem.DoesNotExist):
# If the value is invalid or the object doesn't exist, do nothing.
return queryset
cascade = rest_filters.BooleanFilter(
method='filter_cascade',
label=_('Cascade Locations'),
help_text=_('If true, include items in child locations of the given location'),
)
location = NumberOrNullFilter(
method='filter_location',
label=_('Location'),
help_text=_("Filter by numeric Location ID or the literal 'null'"),
)
def filter_cascade(self, queryset, name, value):
"""Dummy filter method for 'cascade'.
- Ensures 'cascade' appears in API documentation
- Does NOT actually filter the queryset directly
"""
return queryset
def filter_location(self, queryset, name, value):
"""Filter for location that also applies cascade logic."""
cascade = str2bool(self.data.get('cascade', True))
if value == 'null':
if not cascade:
return queryset.filter(location=None)
return queryset
if not cascade:
return queryset.filter(location=value)
try:
loc_obj = StockLocation.objects.get(pk=value)
except StockLocation.DoesNotExist:
return queryset
children = loc_obj.getUniqueChildren()
return queryset.filter(location__in=children)
class StockApiMixin:
"""Mixin class for StockItem API endpoints."""
@@ -1191,52 +1252,6 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView):
headers=self.get_success_headers(serializer.data),
)
def filter_queryset(self, queryset):
"""Custom filtering for the StockItem queryset."""
params = self.request.query_params
queryset = super().filter_queryset(queryset)
# Exclude stock item tree
exclude_tree = params.get('exclude_tree', None)
if exclude_tree is not None:
try:
item = StockItem.objects.get(pk=exclude_tree)
queryset = queryset.exclude(
pk__in=[it.pk for it in item.get_descendants(include_self=True)]
)
except (ValueError, StockItem.DoesNotExist): # pragma: no cover
pass
# Does the client wish to filter by stock location?
loc_id = params.get('location', None)
cascade = str2bool(params.get('cascade', True))
if loc_id is not None:
# Filter by 'null' location (i.e. top-level items)
if isNull(loc_id):
if not cascade:
queryset = queryset.filter(location=None)
else:
try:
# If '?cascade=true' then include items which exist in sub-locations
if cascade:
location = StockLocation.objects.get(pk=loc_id)
queryset = queryset.filter(
location__in=location.getUniqueChildren()
)
else:
queryset = queryset.filter(location=loc_id)
except (ValueError, StockLocation.DoesNotExist): # pragma: no cover
pass
return queryset
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_field_aliases = {

View File

@@ -274,18 +274,19 @@
tree_id: 9
level: 0
lft: 1
rght: 2
rght: 4
- model: stock.stockitem
pk: 1001
fields:
part: 100
parent: 1000
location: 1
quantity: 11
tree_id: 14
level: 0
lft: 1
rght: 2
tree_id: 9
level: 1
lft: 2
rght: 3
- model: stock.stockitem
pk: 1002

View File

@@ -621,6 +621,13 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock(location=7)
self.assertEqual(len(response), 18)
def test_filter_by_exclude_tree(self):
"""Filter StockItem by excluding a StockItem tree."""
response = self.get_stock(exclude_tree=1000)
for item in response:
self.assertNotEqual(item['pk'], 1000)
self.assertNotEqual(item['parent'], 1000)
def test_filter_by_depleted(self):
"""Filter StockItem by depleted status."""
response = self.get_stock(depleted=1)
@@ -786,10 +793,10 @@ class StockItemListTest(StockAPITestCase):
def test_filter_has_child_items(self):
"""Filter StockItem by has_child_items."""
response = self.get_stock(has_child_items=True)
self.assertEqual(len(response), 0)
self.assertEqual(len(response), 1)
response = self.get_stock(has_child_items=False)
self.assertEqual(len(response), 29) # TODO: adjust test dataset (belongs_to)
self.assertEqual(len(response), 28) # TODO: adjust test dataset (belongs_to)
def test_filter_sent_to_customer(self):
"""Filter StockItem by sent_to_customer."""