mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-14 21:22:20 +00:00
Refactor API endpoint: Stock (5/6) (#10444)
* add output options for StockLocation and StockItemTestResult endpoints * add output options tests for StockLocation and StockItemTestResult endpoints * add filters for include_installed and stock_item in StockItemTestResultFilter; update tests for new filter options * update stock test.py * update api_version.py * fix test_installed_tests * Enhance error handling and add tests for stock_item and include_installed filters --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com> Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 396
|
INVENTREE_API_VERSION = 397
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v397 -> 2025-10-01 : https://github.com/inventree/InvenTree/pull/10444
|
||||||
|
- Refactors 'path_detail' param in StockLocation API endpoint
|
||||||
|
- Refactors 'user_detail' and 'template_detail' params in StockItemTestResult API endpoint
|
||||||
|
- Refactors 'item_detail' and 'user_detail' params in StockTracking API endpoint
|
||||||
|
- Refactors 'include_installed' and 'stock_item' params in StockItemTestResult API endpoint
|
||||||
|
|
||||||
v396 -> 2025-10-01 : https://github.com/inventree/InvenTree/pull/10443
|
v396 -> 2025-10-01 : https://github.com/inventree/InvenTree/pull/10443
|
||||||
- Refactors 'part_detail', 'manufacturer_detail' and 'pretty' params in Manufacturer API endpoint
|
- Refactors 'part_detail', 'manufacturer_detail' and 'pretty' params in Manufacturer API endpoint
|
||||||
- Refactors 'manufacturer_part_detail' param in ManufacturerPartParameter API endpoint
|
- Refactors 'manufacturer_part_detail' param in ManufacturerPartParameter API endpoint
|
||||||
|
@@ -35,6 +35,7 @@ from InvenTree.api import (
|
|||||||
ListCreateDestroyAPIView,
|
ListCreateDestroyAPIView,
|
||||||
MetadataView,
|
MetadataView,
|
||||||
)
|
)
|
||||||
|
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
|
||||||
from InvenTree.filters import (
|
from InvenTree.filters import (
|
||||||
ORDER_FILTER_ALIAS,
|
ORDER_FILTER_ALIAS,
|
||||||
SEARCH_ORDER_FILTER,
|
SEARCH_ORDER_FILTER,
|
||||||
@@ -48,6 +49,7 @@ from InvenTree.mixins import (
|
|||||||
CustomRetrieveUpdateDestroyAPI,
|
CustomRetrieveUpdateDestroyAPI,
|
||||||
ListAPI,
|
ListAPI,
|
||||||
ListCreateAPI,
|
ListCreateAPI,
|
||||||
|
OutputOptionsMixin,
|
||||||
RetrieveAPI,
|
RetrieveAPI,
|
||||||
RetrieveUpdateDestroyAPI,
|
RetrieveUpdateDestroyAPI,
|
||||||
)
|
)
|
||||||
@@ -379,12 +381,6 @@ class StockLocationMixin:
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Set context before returning serializer."""
|
"""Set context before returning serializer."""
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
kwargs['path_detail'] = str2bool(params.get('path_detail', False))
|
|
||||||
except AttributeError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
@@ -396,8 +392,23 @@ class StockLocationMixin:
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class StockLocationOutputOptions(OutputConfiguration):
|
||||||
|
"""Output options for StockLocation serializers."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
description='Include detailed information about the BOM item linked to this build line.',
|
||||||
|
flag='path_detail',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class StockLocationList(
|
class StockLocationList(
|
||||||
DataExportViewMixin, BulkUpdateMixin, StockLocationMixin, ListCreateAPI
|
DataExportViewMixin,
|
||||||
|
BulkUpdateMixin,
|
||||||
|
StockLocationMixin,
|
||||||
|
OutputOptionsMixin,
|
||||||
|
ListCreateAPI,
|
||||||
):
|
):
|
||||||
"""API endpoint for list view of StockLocation objects.
|
"""API endpoint for list view of StockLocation objects.
|
||||||
|
|
||||||
@@ -407,6 +418,7 @@ class StockLocationList(
|
|||||||
|
|
||||||
filterset_class = StockLocationFilter
|
filterset_class = StockLocationFilter
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
output_options = StockLocationOutputOptions
|
||||||
|
|
||||||
search_fields = ['name', 'description', 'pathstring', 'tags__name', 'tags__slug']
|
search_fields = ['name', 'description', 'pathstring', 'tags__name', 'tags__slug']
|
||||||
|
|
||||||
@@ -415,9 +427,13 @@ class StockLocationList(
|
|||||||
ordering = ['tree_id', 'lft', 'name']
|
ordering = ['tree_id', 'lft', 'name']
|
||||||
|
|
||||||
|
|
||||||
class StockLocationDetail(StockLocationMixin, CustomRetrieveUpdateDestroyAPI):
|
class StockLocationDetail(
|
||||||
|
StockLocationMixin, OutputOptionsMixin, CustomRetrieveUpdateDestroyAPI
|
||||||
|
):
|
||||||
"""API endpoint for detail view of StockLocation object."""
|
"""API endpoint for detail view of StockLocation object."""
|
||||||
|
|
||||||
|
output_options = StockLocationOutputOptions
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
"""Delete a Stock location instance via the API."""
|
"""Delete a Stock location instance via the API."""
|
||||||
delete_stock_items = str(request.data.get('delete_stock_items', 0)) == '1'
|
delete_stock_items = str(request.data.get('delete_stock_items', 0)) == '1'
|
||||||
@@ -1017,30 +1033,7 @@ class StockApiMixin:
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Set context before returning serializer.
|
"""Set context before returning serializer."""
|
||||||
|
|
||||||
Extra detail may be provided to the serializer via query parameters:
|
|
||||||
|
|
||||||
- part_detail: Include detail about the StockItem's part
|
|
||||||
- location_detail: Include detail about the StockItem's location
|
|
||||||
- supplier_part_detail: Include detail about the StockItem's supplier_part
|
|
||||||
- tests: Include detail about the StockItem's test results
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
kwargs['part_detail'] = str2bool(params.get('part_detail', True))
|
|
||||||
|
|
||||||
for key in [
|
|
||||||
'path_detail',
|
|
||||||
'location_detail',
|
|
||||||
'supplier_part_detail',
|
|
||||||
'tests',
|
|
||||||
]:
|
|
||||||
kwargs[key] = str2bool(params.get(key, False))
|
|
||||||
except AttributeError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
@@ -1321,24 +1314,27 @@ class StockItemTestResultMixin:
|
|||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Set context before returning serializer."""
|
"""Set context before returning serializer."""
|
||||||
try:
|
|
||||||
kwargs['user_detail'] = str2bool(
|
|
||||||
self.request.query_params.get('user_detail', False)
|
|
||||||
)
|
|
||||||
kwargs['template_detail'] = str2bool(
|
|
||||||
self.request.query_params.get('template_detail', False)
|
|
||||||
)
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestResultDetail(StockItemTestResultMixin, RetrieveUpdateDestroyAPI):
|
class StockItemTestResultOutputOptions(OutputConfiguration):
|
||||||
|
"""Output options for StockItemTestResult endpoint."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption(flag='user_detail'),
|
||||||
|
InvenTreeOutputOption(flag='template_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StockItemTestResultDetail(
|
||||||
|
StockItemTestResultMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
|
||||||
|
):
|
||||||
"""Detail endpoint for StockItemTestResult."""
|
"""Detail endpoint for StockItemTestResult."""
|
||||||
|
|
||||||
|
output_options = StockItemTestResultOutputOptions
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestResultFilter(FilterSet):
|
class StockItemTestResultFilter(FilterSet):
|
||||||
"""API filter for the StockItemTestResult list."""
|
"""API filter for the StockItemTestResult list."""
|
||||||
@@ -1381,52 +1377,71 @@ class StockItemTestResultFilter(FilterSet):
|
|||||||
key = generateTestKey(value)
|
key = generateTestKey(value)
|
||||||
return queryset.filter(template__key=key)
|
return queryset.filter(template__key=key)
|
||||||
|
|
||||||
|
include_installed = rest_filters.BooleanFilter(
|
||||||
|
method='filter_include_installed',
|
||||||
|
label=_('Include Installed'),
|
||||||
|
help_text=_(
|
||||||
|
'If true, include test results for items installed underneath the given stock item'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_item = rest_filters.NumberFilter(
|
||||||
|
method='filter_stock_item',
|
||||||
|
label=_('Stock Item'),
|
||||||
|
help_text=_('Filter by numeric Stock Item ID'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_include_installed(self, queryset, name, value):
|
||||||
|
"""Dummy filter method for 'include_installed'.
|
||||||
|
|
||||||
|
- Ensures 'include_installed' appears in API documentation
|
||||||
|
- Does NOT actually filter the queryset directly
|
||||||
|
- The actual logic is handled in filter_stock_item method
|
||||||
|
"""
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def filter_stock_item(self, queryset, name, value):
|
||||||
|
"""Filter for stock_item that also applies include_installed logic."""
|
||||||
|
include_installed = str2bool(self.data.get('include_installed', False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = StockItem.objects.get(pk=value)
|
||||||
|
|
||||||
|
except StockItem.DoesNotExist:
|
||||||
|
raise ValidationError({
|
||||||
|
'stock_item': _('Stock item with ID {id} does not exist').format(
|
||||||
|
id=value
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
items = [item]
|
||||||
|
|
||||||
|
if include_installed:
|
||||||
|
# Include items which are installed "underneath" this item
|
||||||
|
# Note that this function is recursive!
|
||||||
|
installed_items = item.get_installed_items(cascade=True)
|
||||||
|
items += list(installed_items)
|
||||||
|
|
||||||
|
return queryset.filter(stock_item__in=items)
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestResultList(
|
class StockItemTestResultList(
|
||||||
BulkCreateMixin, StockItemTestResultMixin, ListCreateDestroyAPIView
|
BulkCreateMixin,
|
||||||
|
StockItemTestResultMixin,
|
||||||
|
OutputOptionsMixin,
|
||||||
|
ListCreateDestroyAPIView,
|
||||||
):
|
):
|
||||||
"""API endpoint for listing (and creating) a StockItemTestResult object."""
|
"""API endpoint for listing (and creating) a StockItemTestResult object."""
|
||||||
|
|
||||||
filterset_class = StockItemTestResultFilter
|
filterset_class = StockItemTestResultFilter
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
output_options = StockItemTestResultOutputOptions
|
||||||
|
|
||||||
filterset_fields = ['user', 'template', 'result', 'value']
|
filterset_fields = ['user', 'template', 'result', 'value']
|
||||||
ordering_fields = ['date', 'result']
|
ordering_fields = ['date', 'result']
|
||||||
|
|
||||||
ordering = 'date'
|
ordering = 'date'
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
|
||||||
"""Filter by build or stock_item."""
|
|
||||||
params = self.request.query_params
|
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
|
||||||
|
|
||||||
# Filter by stock item
|
|
||||||
item = params.get('stock_item', None)
|
|
||||||
|
|
||||||
if item is not None:
|
|
||||||
try:
|
|
||||||
item = StockItem.objects.get(pk=item)
|
|
||||||
|
|
||||||
items = [item]
|
|
||||||
|
|
||||||
# Do we wish to also include test results for 'installed' items?
|
|
||||||
include_installed = str2bool(params.get('include_installed', False))
|
|
||||||
|
|
||||||
if include_installed:
|
|
||||||
# Include items which are installed "underneath" this item
|
|
||||||
# Note that this function is recursive!
|
|
||||||
installed_items = item.get_installed_items(cascade=True)
|
|
||||||
|
|
||||||
items += list(installed_items)
|
|
||||||
|
|
||||||
queryset = queryset.filter(stock_item__in=items)
|
|
||||||
|
|
||||||
except (ValueError, StockItem.DoesNotExist): # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Create a new test result object.
|
"""Create a new test result object.
|
||||||
|
|
||||||
@@ -1446,7 +1461,16 @@ class StockTrackingDetail(RetrieveAPI):
|
|||||||
serializer_class = StockSerializers.StockTrackingSerializer
|
serializer_class = StockSerializers.StockTrackingSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockTrackingList(DataExportViewMixin, ListAPI):
|
class StockTrackingOutputOptions(OutputConfiguration):
|
||||||
|
"""Output options for StockItemTracking endpoint."""
|
||||||
|
|
||||||
|
OPTIONS = [
|
||||||
|
InvenTreeOutputOption(flag='item_detail'),
|
||||||
|
InvenTreeOutputOption(flag='user_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StockTrackingList(DataExportViewMixin, OutputOptionsMixin, ListAPI):
|
||||||
"""API endpoint for list view of StockItemTracking objects.
|
"""API endpoint for list view of StockItemTracking objects.
|
||||||
|
|
||||||
StockItemTracking objects are read-only
|
StockItemTracking objects are read-only
|
||||||
@@ -1457,23 +1481,10 @@ class StockTrackingList(DataExportViewMixin, ListAPI):
|
|||||||
|
|
||||||
queryset = StockItemTracking.objects.all()
|
queryset = StockItemTracking.objects.all()
|
||||||
serializer_class = StockSerializers.StockTrackingSerializer
|
serializer_class = StockSerializers.StockTrackingSerializer
|
||||||
|
output_options = StockTrackingOutputOptions
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Set context before returning serializer."""
|
"""Set context before returning serializer."""
|
||||||
try:
|
|
||||||
kwargs['item_detail'] = str2bool(
|
|
||||||
self.request.query_params.get('item_detail', False)
|
|
||||||
)
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
kwargs['user_detail'] = str2bool(
|
|
||||||
self.request.query_params.get('user_detail', False)
|
|
||||||
)
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs['context'] = self.get_serializer_context()
|
kwargs['context'] = self.get_serializer_context()
|
||||||
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
@@ -263,6 +263,7 @@
|
|||||||
level: 0
|
level: 0
|
||||||
lft: 1
|
lft: 1
|
||||||
rght: 2
|
rght: 2
|
||||||
|
belongs_to: 105
|
||||||
|
|
||||||
# Multiple stock items for "Bob" (PK 100)
|
# Multiple stock items for "Bob" (PK 100)
|
||||||
- model: stock.stockitem
|
- model: stock.stockitem
|
||||||
|
@@ -263,6 +263,16 @@ class StockLocationTest(StockAPITestCase):
|
|||||||
child.refresh_from_db()
|
child.refresh_from_db()
|
||||||
self.assertEqual(child.parent, parent_stock_location)
|
self.assertEqual(child.parent, parent_stock_location)
|
||||||
|
|
||||||
|
def test_output_options(self):
|
||||||
|
"""Test output options."""
|
||||||
|
url = reverse('api-location-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
response = self.get(url, {'path_detail': 'true'}, expected_code=200)
|
||||||
|
self.assertIn('path', response.data)
|
||||||
|
|
||||||
|
response = self.get(url, {'path_detail': 'false'}, expected_code=200)
|
||||||
|
self.assertNotIn('path', response.data)
|
||||||
|
|
||||||
def test_stock_location_structural(self):
|
def test_stock_location_structural(self):
|
||||||
"""Test the effectiveness of structural stock locations.
|
"""Test the effectiveness of structural stock locations.
|
||||||
|
|
||||||
@@ -777,18 +787,18 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
def test_filter_installed(self):
|
def test_filter_installed(self):
|
||||||
"""Filter StockItem by installed."""
|
"""Filter StockItem by installed."""
|
||||||
response = self.get_stock(installed=True)
|
response = self.get_stock(installed=True)
|
||||||
self.assertEqual(len(response), 0)
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
response = self.get_stock(installed=False)
|
response = self.get_stock(installed=False)
|
||||||
self.assertEqual(len(response), 29) # TODO: adjust test dataset (belongs_to)
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
def test_filter_has_installed(self):
|
def test_filter_has_installed(self):
|
||||||
"""Filter StockItem by has_installed."""
|
"""Filter StockItem by has_installed."""
|
||||||
response = self.get_stock(has_installed_items=True)
|
response = self.get_stock(has_installed_items=True)
|
||||||
self.assertEqual(len(response), 0)
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
response = self.get_stock(has_installed_items=False)
|
response = self.get_stock(has_installed_items=False)
|
||||||
self.assertEqual(len(response), 29) # TODO: adjust test dataset (belongs_to)
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
def test_filter_has_child_items(self):
|
def test_filter_has_child_items(self):
|
||||||
"""Filter StockItem by has_child_items."""
|
"""Filter StockItem by has_child_items."""
|
||||||
@@ -1881,6 +1891,58 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertGreaterEqual(len(response.data), 4)
|
self.assertGreaterEqual(len(response.data), 4)
|
||||||
|
|
||||||
|
def test_stock_item_and_include_installed_filters(self):
|
||||||
|
"""Test stock_item filter with and without include_installed option."""
|
||||||
|
url = self.get_url()
|
||||||
|
|
||||||
|
# Filter by stock_item without include_installed
|
||||||
|
response = self.client.get(url, data={'stock_item': 105})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
initial_count = len(response.data)
|
||||||
|
self.assertGreaterEqual(initial_count, 4)
|
||||||
|
|
||||||
|
# Verify all results belong to stock_item 105
|
||||||
|
for result in response.data:
|
||||||
|
self.assertEqual(result['stock_item'], 105)
|
||||||
|
|
||||||
|
# Filter by stock_item with include_installed=True
|
||||||
|
response = self.client.get(
|
||||||
|
url, data={'stock_item': 105, 'include_installed': True}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
installed_count = len(response.data)
|
||||||
|
self.assertGreaterEqual(installed_count, initial_count)
|
||||||
|
|
||||||
|
# Filter by stock_item with include_installed=False (explicit)
|
||||||
|
response = self.client.get(
|
||||||
|
url, data={'stock_item': 105, 'include_installed': False}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), initial_count)
|
||||||
|
|
||||||
|
# Invalid stock_item ID (does not exist) - should return 400
|
||||||
|
response = self.client.get(url, data={'stock_item': 999999})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Non-numeric stock_item ID - should return 400
|
||||||
|
response = self.client.get(url, data={'stock_item': 'invalid'})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Filter by another stock_item (522)
|
||||||
|
response = self.client.get(url, data={'stock_item': 522})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
item_522_count = len(response.data)
|
||||||
|
self.assertGreaterEqual(item_522_count, 4)
|
||||||
|
|
||||||
|
# Verify all results belong to stock_item 522
|
||||||
|
for result in response.data:
|
||||||
|
self.assertEqual(result['stock_item'], 522)
|
||||||
|
|
||||||
|
# include_installed without stock_item (should be ignored)
|
||||||
|
response_all = self.client.get(url)
|
||||||
|
response_with_flag = self.client.get(url, data={'include_installed': True})
|
||||||
|
self.assertEqual(len(response_all.data), len(response_with_flag.data))
|
||||||
|
|
||||||
def test_post_fail(self):
|
def test_post_fail(self):
|
||||||
"""Test failing posts."""
|
"""Test failing posts."""
|
||||||
# Attempt to post a new test result without specifying required data
|
# Attempt to post a new test result without specifying required data
|
||||||
@@ -2129,6 +2191,22 @@ class StockTestResultTest(StockAPITestCase):
|
|||||||
expected_code=201,
|
expected_code=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_output_options(self):
|
||||||
|
"""Test output options for single item retrieval."""
|
||||||
|
url = reverse('api-stock-test-result-detail', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
response = self.get(url, {'user_detail': 'true'}, expected_code=200)
|
||||||
|
self.assertIn('user_detail', response.data)
|
||||||
|
|
||||||
|
response = self.get(url, {'user_detail': 'false'}, expected_code=200)
|
||||||
|
self.assertNotIn('user_detail', response.data)
|
||||||
|
|
||||||
|
response = self.get(url, {'template_detail': 'true'}, expected_code=200)
|
||||||
|
self.assertIn('template_detail', response.data)
|
||||||
|
|
||||||
|
response = self.get(url, {'template_detail': 'false'}, expected_code=200)
|
||||||
|
self.assertNotIn('template_detail', response.data)
|
||||||
|
|
||||||
|
|
||||||
class StockTrackingTest(StockAPITestCase):
|
class StockTrackingTest(StockAPITestCase):
|
||||||
"""Tests for the StockTracking API endpoints."""
|
"""Tests for the StockTracking API endpoints."""
|
||||||
@@ -2224,6 +2302,25 @@ class StockTrackingTest(StockAPITestCase):
|
|||||||
self.assertIn(key, deltas)
|
self.assertIn(key, deltas)
|
||||||
self.assertIsNotNone(deltas.get(key, None))
|
self.assertIsNotNone(deltas.get(key, None))
|
||||||
|
|
||||||
|
def test_output_options(self):
|
||||||
|
"""Test output options."""
|
||||||
|
url = self.get_url()
|
||||||
|
response = self.client.get(
|
||||||
|
url, {'item_detail': True, 'user_detail': True, 'limit': 2}
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in response.data['results']:
|
||||||
|
self.assertIn('item_detail', item)
|
||||||
|
self.assertIn('user_detail', item)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
url, {'item_detail': False, 'user_detail': False, 'limit': 2}
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in response.data['results']:
|
||||||
|
self.assertNotIn('item_detail', item)
|
||||||
|
self.assertNotIn('user_detail', item)
|
||||||
|
|
||||||
|
|
||||||
class StockAssignTest(StockAPITestCase):
|
class StockAssignTest(StockAPITestCase):
|
||||||
"""Unit tests for the stock assignment API endpoint, where stock items are manually assigned to a customer."""
|
"""Unit tests for the stock assignment API endpoint, where stock items are manually assigned to a customer."""
|
||||||
|
@@ -1469,7 +1469,7 @@ class TestResultTest(StockTestBase):
|
|||||||
|
|
||||||
# Should return the same number of tests as before
|
# Should return the same number of tests as before
|
||||||
tests = item.testResultMap(include_installed=True)
|
tests = item.testResultMap(include_installed=True)
|
||||||
self.assertEqual(len(tests), 3)
|
self.assertEqual(len(tests), 6)
|
||||||
|
|
||||||
if template := PartTestTemplate.objects.filter(
|
if template := PartTestTemplate.objects.filter(
|
||||||
part=item.part, key='somenewtest'
|
part=item.part, key='somenewtest'
|
||||||
@@ -1490,7 +1490,7 @@ class TestResultTest(StockTestBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tests = item.testResultMap(include_installed=True)
|
tests = item.testResultMap(include_installed=True)
|
||||||
self.assertEqual(len(tests), 4)
|
self.assertEqual(len(tests), 7)
|
||||||
|
|
||||||
self.assertIn('somenewtest', tests)
|
self.assertIn('somenewtest', tests)
|
||||||
self.assertEqual(sub_item.test_results.count(), 2)
|
self.assertEqual(sub_item.test_results.count(), 2)
|
||||||
|
Reference in New Issue
Block a user