2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-15 05:32:21 +00:00

Refactor API endpoint: Order (6/6) (#10445)

* add output options for PurchaseOrder, SalesOrder, and ReturnOrder endpoints

* add output options for PurchaseOrder, SalesOrder, and ReturnOrder endpoints

* add serializer context handling and update sales order fixture with additional line item

* bump API version to 398 and update output options tests for PurchaseOrder endpoint

* add output options tests for SalesOrder and ReturnOrder detail endpoints

* fix typo

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Reza
2025-10-05 10:21:49 +03:30
committed by GitHub
parent 66e6d184ea
commit 65c8af427f
5 changed files with 342 additions and 92 deletions

View File

@@ -1,12 +1,21 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 398 INVENTREE_API_VERSION = 399
"""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 = """
v399 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10445
- Refactors 'customer_detail' param in SalesOrder API endpoint
- Refactors 'customer_detail' param in ReturnOrder API endpoint
- Refactors 'supplier_detail' param in PurchaseOrder API endpoint
- Refactors 'part_detail' and 'order_detail' params in PurchaseOrderLineItem API endpoint
- Refactors 'part_detail', 'item_detail' and 'order_detail' params in ReturnOrderLineItem API endpoint
- Refactors 'part_detail', 'order_detail' and 'customer_detail' params in SalesOrderLineItem API endpoint
- Refactors 'part_detail', 'item_detail', 'order_detail', 'location_detail' and 'customer_detail' params in SalesOrderAllocation API endpoint
v398 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10487 v398 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10487
- Refactors 'part_detail', 'path_detail', 'supplier_part_detail', 'location_detail' and 'tests' params in Stock API endpoint - Refactors 'part_detail', 'path_detail', 'supplier_part_detail', 'location_detail' and 'tests' params in Stock API endpoint

View File

@@ -212,17 +212,14 @@ class DataImportExportSerializerMixin(
class OutputOptionsMixin: class OutputOptionsMixin:
"""Mixin to handle output options for API endpoints.""" """Mixin to handle output options for API endpoints."""
output_options: OutputConfiguration output_options: OutputConfiguration = None
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: if getattr(cls, 'output_options', None) is not None:
raise ValueError( schema_for_view_output_options(cls)
f"Class {cls.__name__} must define 'output_options' attribute"
)
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."""

View File

@@ -29,6 +29,7 @@ import stock.serializers as stock_serializers
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 BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import ( from InvenTree.filters import (
SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS, SEARCH_ORDER_FILTER_ALIAS,
@@ -36,7 +37,13 @@ from InvenTree.filters import (
) )
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from InvenTree.helpers_model import construct_absolute_url, get_base_url from InvenTree.helpers_model import construct_absolute_url, get_base_url
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI from InvenTree.mixins import (
CreateAPI,
ListAPI,
ListCreateAPI,
OutputOptionsMixin,
RetrieveUpdateDestroyAPI,
)
from order import models, serializers from order import models, serializers
from order.status_codes import ( from order.status_codes import (
PurchaseOrderStatus, PurchaseOrderStatus,
@@ -345,6 +352,12 @@ class PurchaseOrderFilter(OrderFilter):
return queryset.filter(lines__build_order=build).distinct() return queryset.filter(lines__build_order=build).distinct()
class PurchaseOrderOutputOptions(OutputConfiguration):
"""Output options for the PurchaseOrder endpoint."""
OPTIONS = [InvenTreeOutputOption('supplier_detail')]
class PurchaseOrderMixin: class PurchaseOrderMixin:
"""Mixin class for PurchaseOrder endpoints.""" """Mixin class for PurchaseOrder endpoints."""
@@ -353,16 +366,8 @@ class PurchaseOrderMixin:
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."""
try:
kwargs['supplier_detail'] = str2bool(
self.request.query_params.get('supplier_detail', False)
)
except AttributeError:
pass
# Ensure the request context is passed through # Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return super().get_serializer(*args, **kwargs) return super().get_serializer(*args, **kwargs)
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
@@ -379,7 +384,11 @@ class PurchaseOrderMixin:
class PurchaseOrderList( class PurchaseOrderList(
PurchaseOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI PurchaseOrderMixin,
OrderCreateMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateAPI,
): ):
"""API endpoint for accessing a list of PurchaseOrder objects. """API endpoint for accessing a list of PurchaseOrder objects.
@@ -389,6 +398,7 @@ class PurchaseOrderList(
filterset_class = PurchaseOrderFilter filterset_class = PurchaseOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = PurchaseOrderOutputOptions
ordering_field_aliases = { ordering_field_aliases = {
'reference': ['reference_int', 'reference'], 'reference': ['reference_int', 'reference'],
@@ -421,9 +431,13 @@ class PurchaseOrderList(
ordering = '-reference' ordering = '-reference'
class PurchaseOrderDetail(PurchaseOrderMixin, RetrieveUpdateDestroyAPI): class PurchaseOrderDetail(
PurchaseOrderMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
):
"""API endpoint for detail view of a PurchaseOrder object.""" """API endpoint for detail view of a PurchaseOrder object."""
output_options = PurchaseOrderOutputOptions
class PurchaseOrderContextMixin: class PurchaseOrderContextMixin:
"""Mixin to add purchase order object as serializer context variable.""" """Mixin to add purchase order object as serializer context variable."""
@@ -605,6 +619,15 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
) )
class PurchaseOrderLineItemOutputOptions(OutputConfiguration):
"""Output options for the PurchaseOrderLineItem endpoint."""
OPTIONS = [
InvenTreeOutputOption('part_detail'),
InvenTreeOutputOption('order_detail'),
]
class PurchaseOrderLineItemMixin: class PurchaseOrderLineItemMixin:
"""Mixin class for PurchaseOrderLineItem endpoints.""" """Mixin class for PurchaseOrderLineItem endpoints."""
@@ -623,16 +646,6 @@ class PurchaseOrderLineItemMixin:
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint.""" """Return serializer instance for this endpoint."""
try:
kwargs['part_detail'] = str2bool(
self.request.query_params.get('part_detail', False)
)
kwargs['order_detail'] = str2bool(
self.request.query_params.get('order_detail', False)
)
except AttributeError:
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)
@@ -647,7 +660,10 @@ class PurchaseOrderLineItemMixin:
class PurchaseOrderLineItemList( class PurchaseOrderLineItemList(
PurchaseOrderLineItemMixin, DataExportViewMixin, ListCreateDestroyAPIView PurchaseOrderLineItemMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateDestroyAPIView,
): ):
"""API endpoint for accessing a list of PurchaseOrderLineItem objects. """API endpoint for accessing a list of PurchaseOrderLineItem objects.
@@ -656,6 +672,7 @@ class PurchaseOrderLineItemList(
""" """
filterset_class = PurchaseOrderLineItemFilter filterset_class = PurchaseOrderLineItemFilter
output_options = PurchaseOrderLineItemOutputOptions
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
"""Create or update a new PurchaseOrderLineItem object.""" """Create or update a new PurchaseOrderLineItem object."""
@@ -732,9 +749,13 @@ class PurchaseOrderLineItemList(
] ]
class PurchaseOrderLineItemDetail(PurchaseOrderLineItemMixin, RetrieveUpdateDestroyAPI): class PurchaseOrderLineItemDetail(
PurchaseOrderLineItemMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
):
"""Detail API endpoint for PurchaseOrderLineItem object.""" """Detail API endpoint for PurchaseOrderLineItem object."""
output_options = PurchaseOrderLineItemOutputOptions
class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI): class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of PurchaseOrderExtraLine objects.""" """API endpoint for accessing a list of PurchaseOrderExtraLine objects."""
@@ -818,13 +839,6 @@ class SalesOrderMixin:
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint.""" """Return serializer instance for this endpoint."""
try:
kwargs['customer_detail'] = str2bool(
self.request.query_params.get('customer_detail', False)
)
except AttributeError:
pass
# Ensure the context is passed through to the serializer # Ensure the context is passed through to the serializer
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
@@ -843,8 +857,18 @@ class SalesOrderMixin:
return queryset return queryset
class SalesOrderOutputOptions(OutputConfiguration):
"""Output options for the SalesOrder endpoint."""
OPTIONS = [InvenTreeOutputOption('customer_detail')]
class SalesOrderList( class SalesOrderList(
SalesOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI SalesOrderMixin,
OrderCreateMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateAPI,
): ):
"""API endpoint for accessing a list of SalesOrder objects. """API endpoint for accessing a list of SalesOrder objects.
@@ -856,6 +880,8 @@ class SalesOrderList(
filter_backends = SEARCH_ORDER_FILTER_ALIAS filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = SalesOrderOutputOptions
ordering_field_aliases = { ordering_field_aliases = {
'reference': ['reference_int', 'reference'], 'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'], 'project_code': ['project_code__code'],
@@ -889,9 +915,11 @@ class SalesOrderList(
ordering = '-reference' ordering = '-reference'
class SalesOrderDetail(SalesOrderMixin, RetrieveUpdateDestroyAPI): class SalesOrderDetail(SalesOrderMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrder object.""" """API endpoint for detail view of a SalesOrder object."""
output_options = SalesOrderOutputOptions
class SalesOrderLineItemFilter(LineItemFilter): class SalesOrderLineItemFilter(LineItemFilter):
"""Custom filters for SalesOrderLineItemList endpoint.""" """Custom filters for SalesOrderLineItemList endpoint."""
@@ -1002,16 +1030,6 @@ class SalesOrderLineItemMixin:
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
"""Return serializer for this endpoint with extra data as requested.""" """Return serializer for this endpoint with extra data as requested."""
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['customer_detail'] = str2bool(params.get('customer_detail', False))
except AttributeError:
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)
@@ -1038,8 +1056,18 @@ class SalesOrderLineItemMixin:
return queryset return queryset
class SalesOrderLineItemOutputOptions(OutputConfiguration):
"""Output options for the SalesOrderAllocation endpoint."""
OPTIONS = [
InvenTreeOutputOption('part_detail'),
InvenTreeOutputOption('order_detail'),
InvenTreeOutputOption('customer_detail'),
]
class SalesOrderLineItemList( class SalesOrderLineItemList(
SalesOrderLineItemMixin, DataExportViewMixin, ListCreateAPI SalesOrderLineItemMixin, DataExportViewMixin, OutputOptionsMixin, ListCreateAPI
): ):
"""API endpoint for accessing a list of SalesOrderLineItem objects.""" """API endpoint for accessing a list of SalesOrderLineItem objects."""
@@ -1047,6 +1075,8 @@ class SalesOrderLineItemList(
filter_backends = SEARCH_ORDER_FILTER_ALIAS filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = SalesOrderLineItemOutputOptions
ordering_fields = [ ordering_fields = [
'customer', 'customer',
'order', 'order',
@@ -1069,9 +1099,13 @@ class SalesOrderLineItemList(
search_fields = ['part__name', 'quantity', 'reference'] search_fields = ['part__name', 'quantity', 'reference']
class SalesOrderLineItemDetail(SalesOrderLineItemMixin, RetrieveUpdateDestroyAPI): class SalesOrderLineItemDetail(
SalesOrderLineItemMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
):
"""API endpoint for detail view of a SalesOrderLineItem object.""" """API endpoint for detail view of a SalesOrderLineItem object."""
output_options = SalesOrderLineItemOutputOptions
class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI): class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of SalesOrderExtraLine objects.""" """API endpoint for accessing a list of SalesOrderExtraLine objects."""
@@ -1265,11 +1299,26 @@ class SalesOrderAllocationMixin:
return queryset return queryset
class SalesOrderAllocationList(SalesOrderAllocationMixin, BulkUpdateMixin, ListAPI): class SalesOrderAllocationOutputOptions(OutputConfiguration):
"""Output options for the SalesOrderAllocation endpoint."""
OPTIONS = [
InvenTreeOutputOption('part_detail'),
InvenTreeOutputOption('item_detail'),
InvenTreeOutputOption('order_detail'),
InvenTreeOutputOption('location_detail'),
InvenTreeOutputOption('customer_detail'),
]
class SalesOrderAllocationList(
SalesOrderAllocationMixin, BulkUpdateMixin, OutputOptionsMixin, ListAPI
):
"""API endpoint for listing SalesOrderAllocation objects.""" """API endpoint for listing SalesOrderAllocation objects."""
filterset_class = SalesOrderAllocationFilter filterset_class = SalesOrderAllocationFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = SalesOrderAllocationOutputOptions
ordering_fields = [ ordering_fields = [
'quantity', 'quantity',
@@ -1299,24 +1348,6 @@ class SalesOrderAllocationList(SalesOrderAllocationMixin, BulkUpdateMixin, ListA
'item__batch', 'item__batch',
} }
def get_serializer(self, *args, **kwargs):
"""Return the serializer instance for this endpoint.
Adds extra detail serializers if requested
"""
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['item_detail'] = str2bool(params.get('item_detail', False))
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
kwargs['customer_detail'] = str2bool(params.get('customer_detail', False))
except AttributeError:
pass
return super().get_serializer(*args, **kwargs)
class SalesOrderAllocationDetail(SalesOrderAllocationMixin, RetrieveUpdateDestroyAPI): class SalesOrderAllocationDetail(SalesOrderAllocationMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detali view of a SalesOrderAllocation object.""" """API endpoint for detali view of a SalesOrderAllocation object."""
@@ -1470,13 +1501,6 @@ class ReturnOrderMixin:
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint.""" """Return serializer instance for this endpoint."""
try:
kwargs['customer_detail'] = str2bool(
self.request.query_params.get('customer_detail', False)
)
except AttributeError:
pass
# Ensure the context is passed through to the serializer # Ensure the context is passed through to the serializer
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
@@ -1495,8 +1519,18 @@ class ReturnOrderMixin:
return queryset return queryset
class ReturnOrderOutputOptions(OutputConfiguration):
"""Output options for the ReturnOrder endpoint."""
OPTIONS = [InvenTreeOutputOption(flag='customer_detail')]
class ReturnOrderList( class ReturnOrderList(
ReturnOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI ReturnOrderMixin,
OrderCreateMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateAPI,
): ):
"""API endpoint for accessing a list of ReturnOrder objects.""" """API endpoint for accessing a list of ReturnOrder objects."""
@@ -1504,6 +1538,8 @@ class ReturnOrderList(
filter_backends = SEARCH_ORDER_FILTER_ALIAS filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = ReturnOrderOutputOptions
ordering_field_aliases = { ordering_field_aliases = {
'reference': ['reference_int', 'reference'], 'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'], 'project_code': ['project_code__code'],
@@ -1534,9 +1570,11 @@ class ReturnOrderList(
ordering = '-reference' ordering = '-reference'
class ReturnOrderDetail(ReturnOrderMixin, RetrieveUpdateDestroyAPI): class ReturnOrderDetail(ReturnOrderMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single ReturnOrder object.""" """API endpoint for detail view of a single ReturnOrder object."""
output_options = ReturnOrderOutputOptions
class ReturnOrderContextMixin: class ReturnOrderContextMixin:
"""Simple mixin class to add a ReturnOrder to the serializer context.""" """Simple mixin class to add a ReturnOrder to the serializer context."""
@@ -1620,15 +1658,6 @@ class ReturnOrderLineItemMixin:
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
"""Return serializer for this endpoint with extra data as requested.""" """Return serializer for this endpoint with extra data as requested."""
try:
params = self.request.query_params
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['item_detail'] = str2bool(params.get('item_detail', True))
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
except AttributeError:
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)
@@ -1642,8 +1671,18 @@ class ReturnOrderLineItemMixin:
return queryset return queryset
class ReturnOrderLineItemOutputOptions(OutputConfiguration):
"""Output options for the ReturnOrderLineItem endpoint."""
OPTIONS = [
InvenTreeOutputOption('part_detail'),
InvenTreeOutputOption('item_detail', default=True),
InvenTreeOutputOption('order_detail'),
]
class ReturnOrderLineItemList( class ReturnOrderLineItemList(
ReturnOrderLineItemMixin, DataExportViewMixin, ListCreateAPI ReturnOrderLineItemMixin, DataExportViewMixin, OutputOptionsMixin, ListCreateAPI
): ):
"""API endpoint for accessing a list of ReturnOrderLineItemList objects.""" """API endpoint for accessing a list of ReturnOrderLineItemList objects."""
@@ -1651,6 +1690,8 @@ class ReturnOrderLineItemList(
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
output_options = ReturnOrderLineItemOutputOptions
ordering_fields = ['reference', 'target_date', 'received_date'] ordering_fields = ['reference', 'target_date', 'received_date']
search_fields = [ search_fields = [
@@ -1661,9 +1702,13 @@ class ReturnOrderLineItemList(
] ]
class ReturnOrderLineItemDetail(ReturnOrderLineItemMixin, RetrieveUpdateDestroyAPI): class ReturnOrderLineItemDetail(
ReturnOrderLineItemMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
):
"""API endpoint for detail view of a ReturnOrderLineItem object.""" """API endpoint for detail view of a ReturnOrderLineItem object."""
output_options = ReturnOrderLineItemOutputOptions
class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI): class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of ReturnOrderExtraLine objects.""" """API endpoint for accessing a list of ReturnOrderExtraLine objects."""

View File

@@ -46,6 +46,14 @@
part: 5 part: 5
quantity: 1 quantity: 1
- model: order.salesorderlineitem
pk: 2
fields:
order: 4
part: 5
quantity: 1
# An extra line item # An extra line item
- model: order.salesorderextraline - model: order.salesorderextraline
pk: 1 pk: 1
@@ -60,3 +68,37 @@
fields: fields:
order: 1 order: 1
reference: "Test Shipment, must be present for metadata test" reference: "Test Shipment, must be present for metadata test"
# Allocations for sales orders
- model: order.salesorderallocation
pk: 1
fields:
line: 1
shipment: 1
item: 1
quantity: 100
- model: order.salesorderallocation
pk: 2
fields:
line: 1
shipment: 1
item: 2
quantity: 50
- model: order.salesorderallocation
pk: 3
fields:
line: 1
shipment: null
item: 11
quantity: 1
- model: order.salesorderallocation
pk: 4
fields:
line: 1
shipment: 1
item: 501
quantity: 1

View File

@@ -313,6 +313,15 @@ class PurchaseOrderTest(OrderTest):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_output_options(self):
"""Test the various output options for the PurchaseOrder detail endpoint."""
url = reverse('api-po-detail', kwargs={'pk': 1})
response = self.get(url, {'supplier_detail': 'true'}, expected_code=200)
self.assertIn('supplier_detail', response.data)
response = self.get(url, {'supplier_detail': 'false'}, expected_code=200)
self.assertNotIn('supplier_detail', response.data)
def test_po_operations(self): def test_po_operations(self):
"""Test that we can create / edit and delete a PurchaseOrder via the API.""" """Test that we can create / edit and delete a PurchaseOrder via the API."""
n = models.PurchaseOrder.objects.count() n = models.PurchaseOrder.objects.count()
@@ -852,6 +861,20 @@ class PurchaseOrderLineItemTest(OrderTest):
).json() ).json()
self.assertEqual(float(li5['purchase_price']), 1) self.assertEqual(float(li5['purchase_price']), 1)
def test_output_options(self):
"""Test PurchaseOrderLineItem output option endpoint."""
url = reverse('api-po-line-detail', kwargs={'pk': 1})
response = self.get(url, {'part_detail': 'true'}, expected_code=200)
self.assertIn('part_detail', response.data)
response = self.get(url, {'part_detail': 'false'}, expected_code=200)
self.assertNotIn('part_detail', response.data)
response = self.get(url, {'order_detail': 'true'}, expected_code=200)
self.assertIn('order_detail', response.data)
response = self.get(url, {'order_detail': 'false'}, expected_code=200)
self.assertNotIn('order_detail', response.data)
class PurchaseOrderDownloadTest(OrderTest): class PurchaseOrderDownloadTest(OrderTest):
"""Unit tests for downloading PurchaseOrder data via the API endpoint.""" """Unit tests for downloading PurchaseOrder data via the API endpoint."""
@@ -1749,6 +1772,14 @@ class SalesOrderTest(OrderTest):
self.assertIsNotNone(so.shipment_date) self.assertIsNotNone(so.shipment_date)
self.assertIsNotNone(so.shipped_by) self.assertIsNotNone(so.shipped_by)
def test_output_options(self):
"""Test the output options for the SalesOrder detail endpoint."""
url = reverse('api-so-detail', kwargs={'pk': 1})
response = self.get(url, {'customer_detail': True}, expected_code=200)
self.assertIn('customer_detail', response.data)
response = self.get(url, {'customer_detail': False}, expected_code=200)
self.assertNotIn('customer_detail', response.data)
class SalesOrderLineItemTest(OrderTest): class SalesOrderLineItemTest(OrderTest):
"""Tests for the SalesOrderLineItem API.""" """Tests for the SalesOrderLineItem API."""
@@ -1821,8 +1852,8 @@ class SalesOrderLineItemTest(OrderTest):
self.filter({'completed': 0}, n) self.filter({'completed': 0}, n)
# Filter by 'allocated' status # Filter by 'allocated' status
self.filter({'allocated': 'true'}, 0) self.filter({'allocated': 'true'}, 1)
self.filter({'allocated': 'false'}, n) self.filter({'allocated': 'false'}, n - 1)
def test_so_line_allocated_filters(self): def test_so_line_allocated_filters(self):
"""Test filtering by allocation status for a SalesOrderLineItem.""" """Test filtering by allocation status for a SalesOrderLineItem."""
@@ -1910,6 +1941,17 @@ class SalesOrderLineItemTest(OrderTest):
self.filter({'order': order_id, 'completed': 1}, 2) self.filter({'order': order_id, 'completed': 1}, 2)
self.filter({'order': order_id, 'completed': 0}, 1) self.filter({'order': order_id, 'completed': 0}, 1)
def test_output_options(self):
"""Test the various output options for the SalesOrderLineItem detail endpoint."""
url = reverse('api-so-line-detail', kwargs={'pk': 1})
options = ['part_detail', 'order_detail', 'customer_detail']
for option in options:
response = self.get(url, {f'{option}': True}, expected_code=200)
self.assertIn(option, response.data)
response = self.get(url, {f'{option}': False}, expected_code=200)
self.assertNotIn(option, response.data)
class SalesOrderDownloadTest(OrderTest): class SalesOrderDownloadTest(OrderTest):
"""Unit tests for downloading SalesOrder data via the API endpoint.""" """Unit tests for downloading SalesOrder data via the API endpoint."""
@@ -2252,6 +2294,23 @@ class SalesOrderAllocateTest(OrderTest):
len(response.data), count_before + 3 * models.SalesOrder.objects.count() len(response.data), count_before + 3 * models.SalesOrder.objects.count()
) )
def test_output_options(self):
"""Test the various output options for the SalesOrderAllocation detail endpoint."""
url = reverse('api-so-allocation-list')
options = [
'part_detail',
'item_detail',
'order_detail',
'location_detail',
'customer_detail',
]
for option in options:
response = self.get(url, {f'{option}': True}, expected_code=200)
self.assertIn(option, response.data[0])
response = self.get(url, {f'{option}': False}, expected_code=200)
self.assertNotIn(option, response.data[0])
class ReturnOrderTests(InvenTreeAPITestCase): class ReturnOrderTests(InvenTreeAPITestCase):
"""Unit tests for ReturnOrder API endpoints.""" """Unit tests for ReturnOrder API endpoints."""
@@ -2618,6 +2677,104 @@ class ReturnOrderTests(InvenTreeAPITestCase):
data, required_rows=0, required_cols=['Order', 'Reference', 'Target Date'] data, required_rows=0, required_cols=['Order', 'Reference', 'Target Date']
) )
def test_output_options(self):
"""Test the various output options for the ReturnOrder detail endpoint."""
url = reverse('api-return-order-detail', kwargs={'pk': 1})
options = ['customer_detail']
for option in options:
response = self.get(url, {f'{option}': True}, expected_code=200)
self.assertIn(option, response.data)
response = self.get(url, {f'{option}': False}, expected_code=200)
self.assertNotIn(option, response.data)
class ReturnOrderLineItemTests(InvenTreeAPITestCase):
"""Unit tests for ReturnOrderLineItem API endpoints."""
fixtures = [
'category',
'company',
'return_order',
'part',
'location',
'supplier_part',
'stock',
]
roles = ['return_order.view']
def test_options(self):
"""Test the OPTIONS endpoint."""
self.assignRole('return_order.add')
data = self.options(
reverse('api-return-order-line-list'), expected_code=200
).data
self.assertEqual(data['name'], 'Return Order Line Item List')
# Check POST fields
post = data['actions']['POST']
self.assertIn('order', post)
self.assertIn('item', post)
self.assertIn('quantity', post)
self.assertIn('outcome', post)
def test_list(self):
"""Test list endpoint."""
url = reverse('api-return-order-line-list')
response = self.get(url, expected_code=200)
self.assertGreater(len(response.data), 0)
# Test with pagination
data = self.get(
url, {'limit': 1, 'ordering': 'reference'}, expected_code=200
).data
self.assertIn('count', data)
self.assertIn('results', data)
self.assertEqual(len(data['results']), 1)
def test_detail(self):
"""Test detail endpoint."""
url = reverse('api-return-order-line-detail', kwargs={'pk': 1})
response = self.get(url, expected_code=200)
data = response.data
self.assertIn('order', data)
self.assertIn('item', data)
self.assertIn('quantity', data)
self.assertIn('outcome', data)
def test_output_options(self):
"""Test output options for detail endpoint."""
url = reverse('api-return-order-line-detail', kwargs={'pk': 1})
options = ['part_detail', 'item_detail', 'order_detail']
for option in options:
# Test with option enabled
response = self.get(url, {option: True}, expected_code=200)
self.assertIn(option, response.data)
# Test with option disabled
response = self.get(url, {option: False}, expected_code=200)
self.assertNotIn(option, response.data)
def test_update(self):
"""Test updating ReturnOrderLineItem."""
url = reverse('api-return-order-line-detail', kwargs={'pk': 1})
# Without permissions
self.patch(url, {'price': '10.50'}, expected_code=403)
self.assignRole('return_order.change')
self.patch(url, {'price': '15.75'}, expected_code=200)
line = models.ReturnOrderLineItem.objects.get(pk=1)
self.assertEqual(float(line.price.amount), 15.75)
class OrderMetadataAPITest(InvenTreeAPITestCase): class OrderMetadataAPITest(InvenTreeAPITestCase):
"""Unit tests for the various metadata endpoints of API.""" """Unit tests for the various metadata endpoints of API."""