mirror of
https://github.com/inventree/InvenTree.git
synced 2025-09-14 06:31:27 +00:00
Refactor (backend): Improve BuildItemList API filters (#10214)
* add output filter to BuildItemFilter and optimize queryset retrieval * add NumberOrNullFilter to handle filtering by numeric values or 'null'; update BuildItemFilter to use new filter and add tests for output filtering * update api_version * fix(tests): Add missing stock fixture to build tests --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 388
|
INVENTREE_API_VERSION = 389
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
v389 -> 2025-08-27 : https://github.com/inventree/InvenTree/pull/10214
|
||||||
|
- Adds "output" filter to the BuildItem API endpoint
|
||||||
|
- Removes undocumented 'output' query parameter handling
|
||||||
|
|
||||||
v388 -> 2025-08-23 : https://github.com/inventree/InvenTree/pull/10213
|
v388 -> 2025-08-23 : https://github.com/inventree/InvenTree/pull/10213
|
||||||
- Disable paging on PurchaseOrderReceive call
|
- Disable paging on PurchaseOrderReceive call
|
||||||
|
|
||||||
|
@@ -159,6 +159,38 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):
|
|||||||
return ordering
|
return ordering
|
||||||
|
|
||||||
|
|
||||||
|
class NumberOrNullFilter(rest_filters.NumberFilter):
|
||||||
|
"""Custom NumberFilter that allows filtering by numeric values or the literal string "null".
|
||||||
|
|
||||||
|
This allows matching either numeric values or NULL values in the database.
|
||||||
|
|
||||||
|
Example Usage:
|
||||||
|
?my_field=20 → filters rows where my_field=20
|
||||||
|
?my_field=null → filters rows where my_field IS NULL
|
||||||
|
"""
|
||||||
|
|
||||||
|
def filter(self, qs, value):
|
||||||
|
"""Return queryset filtered by value or NULL if 'null' is passed."""
|
||||||
|
if value == 'null':
|
||||||
|
return qs.filter(**{self.field_name: None})
|
||||||
|
return super().filter(qs, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def field(self):
|
||||||
|
"""Allow 'null' as valid input in filter parameters."""
|
||||||
|
field = super().field
|
||||||
|
original_clean = field.clean
|
||||||
|
|
||||||
|
def custom_clean(val):
|
||||||
|
"""Custom clean function for filter input values."""
|
||||||
|
if InvenTree.helpers.isNull(val) and val is not None:
|
||||||
|
return 'null'
|
||||||
|
return original_clean(val)
|
||||||
|
|
||||||
|
field.clean = custom_clean
|
||||||
|
return field
|
||||||
|
|
||||||
|
|
||||||
SEARCH_ORDER_FILTER = [
|
SEARCH_ORDER_FILTER = [
|
||||||
rest_filters.DjangoFilterBackend,
|
rest_filters.DjangoFilterBackend,
|
||||||
InvenTreeSearchFilter,
|
InvenTreeSearchFilter,
|
||||||
|
@@ -23,8 +23,12 @@ from build.status_codes import BuildStatus, BuildStatusGroups
|
|||||||
from data_exporter.mixins import DataExportViewMixin
|
from data_exporter.mixins import DataExportViewMixin
|
||||||
from generic.states.api import StatusView
|
from generic.states.api import StatusView
|
||||||
from InvenTree.api import BulkDeleteMixin, MetadataView
|
from InvenTree.api import BulkDeleteMixin, MetadataView
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER_ALIAS, InvenTreeDateFilter
|
from InvenTree.filters import (
|
||||||
from InvenTree.helpers import isNull, str2bool
|
SEARCH_ORDER_FILTER_ALIAS,
|
||||||
|
InvenTreeDateFilter,
|
||||||
|
NumberOrNullFilter,
|
||||||
|
)
|
||||||
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.mixins import CreateAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
from InvenTree.mixins import CreateAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
@@ -850,6 +854,14 @@ class BuildItemFilter(rest_filters.FilterSet):
|
|||||||
locations = location.get_descendants(include_self=True)
|
locations = location.get_descendants(include_self=True)
|
||||||
return queryset.filter(stock_item__location__in=locations)
|
return queryset.filter(stock_item__location__in=locations)
|
||||||
|
|
||||||
|
output = NumberOrNullFilter(
|
||||||
|
field_name='install_into',
|
||||||
|
label=_('Output'),
|
||||||
|
help_text=_(
|
||||||
|
"Filter by output stock item ID. Use 'null' to find uninstalled build items."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
|
class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
|
||||||
"""API endpoint for accessing a list of BuildItem objects.
|
"""API endpoint for accessing a list of BuildItem objects.
|
||||||
@@ -888,6 +900,11 @@ class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
|
|||||||
queryset = queryset.select_related(
|
queryset = queryset.select_related(
|
||||||
'build_line',
|
'build_line',
|
||||||
'build_line__build',
|
'build_line__build',
|
||||||
|
'build_line__build__part',
|
||||||
|
'build_line__build__responsible',
|
||||||
|
'build_line__build__issued_by',
|
||||||
|
'build_line__build__project_code',
|
||||||
|
'build_line__build__part__pricing_data',
|
||||||
'build_line__bom_item',
|
'build_line__bom_item',
|
||||||
'build_line__bom_item__part',
|
'build_line__bom_item__part',
|
||||||
'build_line__bom_item__sub_part',
|
'build_line__bom_item__sub_part',
|
||||||
@@ -899,24 +916,7 @@ class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
|
|||||||
'stock_item__supplier_part__supplier',
|
'stock_item__supplier_part__supplier',
|
||||||
'stock_item__supplier_part__manufacturer_part',
|
'stock_item__supplier_part__manufacturer_part',
|
||||||
'stock_item__supplier_part__manufacturer_part__manufacturer',
|
'stock_item__supplier_part__manufacturer_part__manufacturer',
|
||||||
).prefetch_related('stock_item__location__tags')
|
).prefetch_related('stock_item__location__tags', 'stock_item__tags')
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
|
||||||
"""Custom query filtering for the BuildItem list."""
|
|
||||||
queryset = super().filter_queryset(queryset)
|
|
||||||
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
# Filter by output target
|
|
||||||
output = params.get('output', None)
|
|
||||||
|
|
||||||
if output:
|
|
||||||
if isNull(output):
|
|
||||||
queryset = queryset.filter(install_into=None)
|
|
||||||
else:
|
|
||||||
queryset = queryset.filter(install_into=output)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@@ -80,3 +80,16 @@
|
|||||||
level: 0
|
level: 0
|
||||||
lft: 1
|
lft: 1
|
||||||
rght: 2
|
rght: 2
|
||||||
|
|
||||||
|
- model: build.builditem
|
||||||
|
pk: 1
|
||||||
|
fields:
|
||||||
|
quantity: 3
|
||||||
|
stock_item: 1
|
||||||
|
|
||||||
|
- model: build.builditem
|
||||||
|
pk: 2
|
||||||
|
fields:
|
||||||
|
quantity: 10
|
||||||
|
stock_item: 11
|
||||||
|
install_into: 100
|
||||||
|
@@ -21,7 +21,7 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
|||||||
- Tests for BuildItem API
|
- Tests for BuildItem API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fixtures = ['category', 'part', 'location', 'build']
|
fixtures = ['category', 'part', 'location', 'build', 'stock']
|
||||||
|
|
||||||
roles = ['build.change', 'build.add', 'build.delete']
|
roles = ['build.change', 'build.add', 'build.delete']
|
||||||
|
|
||||||
@@ -83,15 +83,30 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(item['issued_by'], self.user.pk)
|
self.assertEqual(item['issued_by'], self.user.pk)
|
||||||
|
|
||||||
def test_get_build_item_list(self):
|
def test_get_build_item_list(self):
|
||||||
"""Test that we can retrieve list of BuildItem objects."""
|
"""Test retrieving BuildItem list and applying filters like 'part' and 'output'."""
|
||||||
url = reverse('api-build-item-list')
|
url = reverse('api-build-item-list')
|
||||||
|
|
||||||
|
# Retrieve the full list of BuildItem objects
|
||||||
response = self.get(url, expected_code=200)
|
response = self.get(url, expected_code=200)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
# Test again, filtering by park ID
|
# Filter by part ID (only items for part ID=1 expected)
|
||||||
response = self.get(url, {'part': '1'}, expected_code=200)
|
response = self.get(url, {'part': '1'}, expected_code=200)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
|
||||||
|
# Filter: output=null (install_into=None)
|
||||||
|
response = self.get(url, {'output': 'null'}, expected_code=200)
|
||||||
|
ids = [item['pk'] for item in response.data]
|
||||||
|
self.assertIn(1, ids)
|
||||||
|
self.assertNotIn(2, ids)
|
||||||
|
|
||||||
|
# Filter: output=<id> (install_into specific ID)
|
||||||
|
response = self.get(url, {'output': 100}, expected_code=200)
|
||||||
|
ids = [item['pk'] for item in response.data]
|
||||||
|
self.assertIn(2, ids)
|
||||||
|
self.assertNotIn(1, ids)
|
||||||
|
|
||||||
|
|
||||||
class BuildAPITest(InvenTreeAPITestCase):
|
class BuildAPITest(InvenTreeAPITestCase):
|
||||||
|
@@ -15,7 +15,7 @@ from .models import Build
|
|||||||
class BuildTestSimple(InvenTreeTestCase):
|
class BuildTestSimple(InvenTreeTestCase):
|
||||||
"""Basic set of tests for the BuildOrder model functionality."""
|
"""Basic set of tests for the BuildOrder model functionality."""
|
||||||
|
|
||||||
fixtures = ['category', 'part', 'location', 'build']
|
fixtures = ['category', 'part', 'location', 'build', 'stock']
|
||||||
|
|
||||||
roles = ['build.change', 'build.add', 'build.delete']
|
roles = ['build.change', 'build.add', 'build.delete']
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ from plugin.registry import registry
|
|||||||
class SampleValidatorPluginTest(InvenTreeAPITestCase, InvenTreeTestCase):
|
class SampleValidatorPluginTest(InvenTreeAPITestCase, InvenTreeTestCase):
|
||||||
"""Tests for the SampleValidatonPlugin class."""
|
"""Tests for the SampleValidatonPlugin class."""
|
||||||
|
|
||||||
fixtures = ['part', 'category', 'location', 'build']
|
fixtures = ['part', 'category', 'location', 'build', 'stock']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up the test environment."""
|
"""Set up the test environment."""
|
||||||
|
Reference in New Issue
Block a user