2
0
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:
Reza
2025-10-02 10:18:09 +03:30
committed by GitHub
parent 7b2b174ab2
commit ae14c65c7c
5 changed files with 138 additions and 43 deletions

View File

@@ -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

View File

@@ -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))

View File

@@ -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()

View 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

View File

@@ -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