mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-03 15:52:51 +00:00
Refactor API endpoint: Build (2/6) (#10438)
* feat: add output options for BuildLine and BuildItem endpoints * enhance output options for BuildLine and BuildItem endpoints with detailed descriptions and tests * update test * . * update API version to v394 and modify related build fixtures and tests * create separate build_line.yaml fixture * . * roll back context in BuildLineMixin --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -1,12 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 393
|
INVENTREE_API_VERSION = 394
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v394 -> 2025-09-01 : https://github.com/inventree/InvenTree/pull/10438
|
||||||
|
- Refactors 'bom_item_detail', 'assembly_detail', 'part_detail', 'build_detail' and 'allocations' param in user BuildLine API endpoint
|
||||||
|
- Refactors 'part_detail', 'location_detail', 'stock_detail' and 'build_detail' param in user BuildItem API endpoint
|
||||||
|
|
||||||
v393 -> 2025-09-01 : https://github.com/inventree/InvenTree/pull/10437
|
v393 -> 2025-09-01 : https://github.com/inventree/InvenTree/pull/10437
|
||||||
- Refactors 'user_detail', 'permission_detail', 'role_detail' param in user GroupList API endpoint
|
- Refactors 'user_detail', 'permission_detail', 'role_detail' param in user GroupList API endpoint
|
||||||
|
|
||||||
|
@@ -217,15 +217,15 @@ class OutputOptionsMixin:
|
|||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
"""Automatically attaches OpenAPI schema parameters for its output options."""
|
"""Automatically attaches OpenAPI schema parameters for its output options."""
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
|
if getattr(cls, 'output_options', None) is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Class {cls.__name__} must define 'output_options' attribute"
|
||||||
|
)
|
||||||
schema_for_view_output_options(cls)
|
schema_for_view_output_options(cls)
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Return serializer instance with output options applied."""
|
"""Return serializer instance with output options applied."""
|
||||||
if not hasattr(self, 'output_options'):
|
|
||||||
raise AttributeError(
|
|
||||||
f"Class {self.__class__.__name__} must define 'output_options' attribute"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.output_options and hasattr(self, 'request'):
|
if self.output_options and hasattr(self, 'request'):
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
kwargs.update(self.output_options.format_params(params))
|
kwargs.update(self.output_options.format_params(params))
|
||||||
|
@@ -27,13 +27,19 @@ 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.fields import InvenTreeOutputOption, OutputConfiguration
|
||||||
from InvenTree.filters import (
|
from InvenTree.filters import (
|
||||||
SEARCH_ORDER_FILTER_ALIAS,
|
SEARCH_ORDER_FILTER_ALIAS,
|
||||||
InvenTreeDateFilter,
|
InvenTreeDateFilter,
|
||||||
NumberOrNullFilter,
|
NumberOrNullFilter,
|
||||||
)
|
)
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.mixins import CreateAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
|
from InvenTree.mixins import (
|
||||||
|
CreateAPI,
|
||||||
|
ListCreateAPI,
|
||||||
|
OutputOptionsMixin,
|
||||||
|
RetrieveUpdateDestroyAPI,
|
||||||
|
)
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
|
|
||||||
@@ -530,18 +536,6 @@ class BuildLineMixin:
|
|||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Return the serializer instance for this endpoint."""
|
"""Return the serializer instance for this endpoint."""
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
kwargs['bom_item_detail'] = str2bool(params.get('bom_item_detail', True))
|
|
||||||
kwargs['assembly_detail'] = str2bool(params.get('assembly_detail', True))
|
|
||||||
kwargs['part_detail'] = str2bool(params.get('part_detail', True))
|
|
||||||
kwargs['build_detail'] = str2bool(params.get('build_detail', False))
|
|
||||||
kwargs['allocations'] = str2bool(params.get('allocations', True))
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
def get_source_build(self) -> Build:
|
def get_source_build(self) -> Build:
|
||||||
@@ -570,12 +564,45 @@ class BuildLineMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BuildLineList(BuildLineMixin, DataExportViewMixin, ListCreateAPI):
|
class BuildLineOutputOptions(OutputConfiguration):
|
||||||
|
"""Output options for BuildLine endpoint."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'bom_item_detail',
|
||||||
|
description='Include detailed information about the BOM item linked to this build line.',
|
||||||
|
default=True,
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'assembly_detail',
|
||||||
|
description='Include brief details of the assembly (parent part) related to the BOM item in this build line.',
|
||||||
|
default=True,
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'part_detail',
|
||||||
|
description='Include detailed information about the specific part being built or consumed in this build line.',
|
||||||
|
default=True,
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'build_detail',
|
||||||
|
description='Include detailed information about the associated build order.',
|
||||||
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
'allocations',
|
||||||
|
description='Include allocation details showing which stock items are allocated to this build line.',
|
||||||
|
default=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildLineList(
|
||||||
|
BuildLineMixin, DataExportViewMixin, OutputOptionsMixin, ListCreateAPI
|
||||||
|
):
|
||||||
"""API endpoint for accessing a list of BuildLine objects."""
|
"""API endpoint for accessing a list of BuildLine objects."""
|
||||||
|
|
||||||
filterset_class = BuildLineFilter
|
filterset_class = BuildLineFilter
|
||||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||||
|
output_options = BuildLineOutputOptions
|
||||||
ordering_fields = [
|
ordering_fields = [
|
||||||
'part',
|
'part',
|
||||||
'allocated',
|
'allocated',
|
||||||
@@ -623,9 +650,11 @@ class BuildLineList(BuildLineMixin, DataExportViewMixin, ListCreateAPI):
|
|||||||
return source_build
|
return source_build
|
||||||
|
|
||||||
|
|
||||||
class BuildLineDetail(BuildLineMixin, RetrieveUpdateDestroyAPI):
|
class BuildLineDetail(BuildLineMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI):
|
||||||
"""API endpoint for detail view of a BuildLine object."""
|
"""API endpoint for detail view of a BuildLine object."""
|
||||||
|
|
||||||
|
output_options = BuildLineOutputOptions
|
||||||
|
|
||||||
def get_source_build(self) -> Optional[Build]:
|
def get_source_build(self) -> Optional[Build]:
|
||||||
"""Return the target source location for the BuildLine queryset."""
|
"""Return the target source location for the BuildLine queryset."""
|
||||||
return None
|
return None
|
||||||
@@ -867,36 +896,32 @@ class BuildItemFilter(FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI):
|
class BuildItemOutputOptions(OutputConfiguration):
|
||||||
|
"""Output options for BuildItem endpoint."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption('part_detail'),
|
||||||
|
InvenTreeOutputOption('location_detail'),
|
||||||
|
InvenTreeOutputOption('stock_detail'),
|
||||||
|
InvenTreeOutputOption('build_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildItemList(
|
||||||
|
DataExportViewMixin, OutputOptionsMixin, BulkDeleteMixin, ListCreateAPI
|
||||||
|
):
|
||||||
"""API endpoint for accessing a list of BuildItem objects.
|
"""API endpoint for accessing a list of BuildItem objects.
|
||||||
|
|
||||||
- GET: Return list of objects
|
- GET: Return list of objects
|
||||||
- POST: Create a new BuildItem object
|
- POST: Create a new BuildItem object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
output_options = BuildItemOutputOptions
|
||||||
queryset = BuildItem.objects.all()
|
queryset = BuildItem.objects.all()
|
||||||
serializer_class = build.serializers.BuildItemSerializer
|
serializer_class = build.serializers.BuildItemSerializer
|
||||||
filterset_class = BuildItemFilter
|
filterset_class = BuildItemFilter
|
||||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
|
||||||
"""Returns a BuildItemSerializer instance based on the request."""
|
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
for key in [
|
|
||||||
'part_detail',
|
|
||||||
'location_detail',
|
|
||||||
'stock_detail',
|
|
||||||
'build_detail',
|
|
||||||
]:
|
|
||||||
if key in params:
|
|
||||||
kwargs[key] = str2bool(params.get(key, False))
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Override the queryset method, to perform custom prefetch."""
|
"""Override the queryset method, to perform custom prefetch."""
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
|
18
src/backend/InvenTree/build/fixtures/build_line.yaml
Normal file
18
src/backend/InvenTree/build/fixtures/build_line.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Construct buildline objects
|
||||||
|
|
||||||
|
- model: build.buildline
|
||||||
|
pk: 2
|
||||||
|
fields:
|
||||||
|
build: 4
|
||||||
|
bom_item: 5
|
||||||
|
quantity: 20
|
||||||
|
consumed: 0
|
||||||
|
|
||||||
|
|
||||||
|
- model: build.buildline
|
||||||
|
pk: 3
|
||||||
|
fields:
|
||||||
|
build: 5
|
||||||
|
bom_item: 3
|
||||||
|
quantity: 10
|
||||||
|
consumed: 5
|
@@ -22,7 +22,7 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
|||||||
- Tests for BuildItem API
|
- Tests for BuildItem API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fixtures = ['category', 'part', 'location', 'build', 'stock']
|
fixtures = ['category', 'part', 'location', 'bom', 'build', 'build_line', 'stock']
|
||||||
|
|
||||||
roles = ['build.change', 'build.add', 'build.delete']
|
roles = ['build.change', 'build.add', 'build.delete']
|
||||||
|
|
||||||
@@ -109,11 +109,29 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
|||||||
self.assertIn(2, ids)
|
self.assertIn(2, ids)
|
||||||
self.assertNotIn(1, ids)
|
self.assertNotIn(1, ids)
|
||||||
|
|
||||||
|
# test output options
|
||||||
|
# Test cases: (parameter_name, response_field_name)
|
||||||
|
test_cases = [
|
||||||
|
('part_detail', 'part_detail'),
|
||||||
|
('location_detail', 'location_detail'),
|
||||||
|
('stock_detail', 'stock_item_detail'),
|
||||||
|
('build_detail', 'build_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for param, field in test_cases:
|
||||||
|
# Test with parameter set to 'true'
|
||||||
|
response = self.get(url, {param: 'true'}, expected_code=200)
|
||||||
|
self.assertIn(field, response.data[0])
|
||||||
|
|
||||||
|
# Test with parameter set to 'false'
|
||||||
|
response = self.get(url, {param: 'false'}, expected_code=200)
|
||||||
|
self.assertNotIn(field, response.data[0])
|
||||||
|
|
||||||
|
|
||||||
class BuildAPITest(InvenTreeAPITestCase):
|
class BuildAPITest(InvenTreeAPITestCase):
|
||||||
"""Series of tests for the Build DRF API."""
|
"""Series of tests for the Build DRF API."""
|
||||||
|
|
||||||
fixtures = ['category', 'part', 'location', 'bom', 'build', 'stock']
|
fixtures = ['category', 'part', 'location', 'bom', 'build', 'build_line', 'stock']
|
||||||
|
|
||||||
# Required roles to access Build API endpoints
|
# Required roles to access Build API endpoints
|
||||||
roles = ['build.change', 'build.add']
|
roles = ['build.change', 'build.add']
|
||||||
@@ -1356,6 +1374,36 @@ class BuildLineTests(BuildAPITest):
|
|||||||
|
|
||||||
self.assertEqual(n_t + n_f, BuildLine.objects.count())
|
self.assertEqual(n_t + n_f, BuildLine.objects.count())
|
||||||
|
|
||||||
|
def test_output_options(self):
|
||||||
|
"""Test output options for the BuildLine endpoint."""
|
||||||
|
url = reverse('api-build-line-detail', kwargs={'pk': 2})
|
||||||
|
|
||||||
|
# Test cases: (parameter_name, response_field_name)
|
||||||
|
test_cases = [
|
||||||
|
('bom_item_detail', 'bom_item_detail'),
|
||||||
|
('assembly_detail', 'assembly_detail'),
|
||||||
|
('part_detail', 'part_detail'),
|
||||||
|
('build_detail', 'build_detail'),
|
||||||
|
('allocations', 'allocations'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for param, field in test_cases:
|
||||||
|
# Test with parameter set to 'true'
|
||||||
|
response = self.get(url, {param: 'true'}, expected_code=200)
|
||||||
|
self.assertIn(
|
||||||
|
field,
|
||||||
|
response.data,
|
||||||
|
f"Field '{field}' should be present when {param}=true",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with parameter set to 'false'
|
||||||
|
response = self.get(url, {param: 'false'}, expected_code=200)
|
||||||
|
self.assertNotIn(
|
||||||
|
field,
|
||||||
|
response.data,
|
||||||
|
f"Field '{field}' should NOT be present when {param}=false",
|
||||||
|
)
|
||||||
|
|
||||||
def test_filter_consumed(self):
|
def test_filter_consumed(self):
|
||||||
"""Filter for the 'consumed' status."""
|
"""Filter for the 'consumed' status."""
|
||||||
# Create a new build order
|
# Create a new build order
|
||||||
|
Reference in New Issue
Block a user