mirror of
https://github.com/inventree/InvenTree.git
synced 2025-09-13 14:11:37 +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
|
||||
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."""
|
||||
|
||||
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
|
||||
- Disable paging on PurchaseOrderReceive call
|
||||
|
||||
|
@@ -159,6 +159,38 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):
|
||||
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 = [
|
||||
rest_filters.DjangoFilterBackend,
|
||||
InvenTreeSearchFilter,
|
||||
|
@@ -23,8 +23,12 @@ from build.status_codes import BuildStatus, BuildStatusGroups
|
||||
from data_exporter.mixins import DataExportViewMixin
|
||||
from generic.states.api import StatusView
|
||||
from InvenTree.api import BulkDeleteMixin, MetadataView
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER_ALIAS, InvenTreeDateFilter
|
||||
from InvenTree.helpers import isNull, str2bool
|
||||
from InvenTree.filters import (
|
||||
SEARCH_ORDER_FILTER_ALIAS,
|
||||
InvenTreeDateFilter,
|
||||
NumberOrNullFilter,
|
||||
)
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.mixins import CreateAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
||||
from users.models import Owner
|
||||
|
||||
@@ -850,6 +854,14 @@ class BuildItemFilter(rest_filters.FilterSet):
|
||||
locations = location.get_descendants(include_self=True)
|
||||
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):
|
||||
"""API endpoint for accessing a list of BuildItem objects.
|
||||
@@ -888,6 +900,11 @@ class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
|
||||
queryset = queryset.select_related(
|
||||
'build_line',
|
||||
'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__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__manufacturer_part',
|
||||
'stock_item__supplier_part__manufacturer_part__manufacturer',
|
||||
).prefetch_related('stock_item__location__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)
|
||||
).prefetch_related('stock_item__location__tags', 'stock_item__tags')
|
||||
|
||||
return queryset
|
||||
|
||||
|
@@ -80,3 +80,16 @@
|
||||
level: 0
|
||||
lft: 1
|
||||
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
|
||||
"""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'build']
|
||||
fixtures = ['category', 'part', 'location', 'build', 'stock']
|
||||
|
||||
roles = ['build.change', 'build.add', 'build.delete']
|
||||
|
||||
@@ -83,15 +83,30 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
||||
self.assertEqual(item['issued_by'], self.user.pk)
|
||||
|
||||
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')
|
||||
|
||||
# Retrieve the full list of BuildItem objects
|
||||
response = self.get(url, expected_code=200)
|
||||
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)
|
||||
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):
|
||||
|
@@ -15,7 +15,7 @@ from .models import Build
|
||||
class BuildTestSimple(InvenTreeTestCase):
|
||||
"""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']
|
||||
|
||||
|
@@ -12,7 +12,7 @@ from plugin.registry import registry
|
||||
class SampleValidatorPluginTest(InvenTreeAPITestCase, InvenTreeTestCase):
|
||||
"""Tests for the SampleValidatonPlugin class."""
|
||||
|
||||
fixtures = ['part', 'category', 'location', 'build']
|
||||
fixtures = ['part', 'category', 'location', 'build', 'stock']
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test environment."""
|
||||
|
Reference in New Issue
Block a user