2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-14 21:22:20 +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
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."""
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
- 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:
"""Mixin to handle output options for API endpoints."""
output_options: OutputConfiguration
output_options: OutputConfiguration = None
def __init_subclass__(cls, **kwargs):
"""Automatically attaches OpenAPI schema parameters for its output options."""
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)
if getattr(cls, 'output_options', None) is not None:
schema_for_view_output_options(cls)
def get_serializer(self, *args, **kwargs):
"""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 generic.states.api import StatusView
from InvenTree.api import BulkUpdateMixin, ListCreateDestroyAPIView, MetadataView
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import (
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
@@ -36,7 +37,13 @@ from InvenTree.filters import (
)
from InvenTree.helpers import str2bool
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.status_codes import (
PurchaseOrderStatus,
@@ -345,6 +352,12 @@ class PurchaseOrderFilter(OrderFilter):
return queryset.filter(lines__build_order=build).distinct()
class PurchaseOrderOutputOptions(OutputConfiguration):
"""Output options for the PurchaseOrder endpoint."""
OPTIONS = [InvenTreeOutputOption('supplier_detail')]
class PurchaseOrderMixin:
"""Mixin class for PurchaseOrder endpoints."""
@@ -353,16 +366,8 @@ class PurchaseOrderMixin:
def get_serializer(self, *args, **kwargs):
"""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
kwargs['context'] = self.get_serializer_context()
return super().get_serializer(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
@@ -379,7 +384,11 @@ class PurchaseOrderMixin:
class PurchaseOrderList(
PurchaseOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
PurchaseOrderMixin,
OrderCreateMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateAPI,
):
"""API endpoint for accessing a list of PurchaseOrder objects.
@@ -389,6 +398,7 @@ class PurchaseOrderList(
filterset_class = PurchaseOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = PurchaseOrderOutputOptions
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
@@ -421,9 +431,13 @@ class PurchaseOrderList(
ordering = '-reference'
class PurchaseOrderDetail(PurchaseOrderMixin, RetrieveUpdateDestroyAPI):
class PurchaseOrderDetail(
PurchaseOrderMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
):
"""API endpoint for detail view of a PurchaseOrder object."""
output_options = PurchaseOrderOutputOptions
class PurchaseOrderContextMixin:
"""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:
"""Mixin class for PurchaseOrderLineItem endpoints."""
@@ -623,16 +646,6 @@ class PurchaseOrderLineItemMixin:
def get_serializer(self, *args, **kwargs):
"""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()
return super().get_serializer(*args, **kwargs)
@@ -647,7 +660,10 @@ class PurchaseOrderLineItemMixin:
class PurchaseOrderLineItemList(
PurchaseOrderLineItemMixin, DataExportViewMixin, ListCreateDestroyAPIView
PurchaseOrderLineItemMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateDestroyAPIView,
):
"""API endpoint for accessing a list of PurchaseOrderLineItem objects.
@@ -656,6 +672,7 @@ class PurchaseOrderLineItemList(
"""
filterset_class = PurchaseOrderLineItemFilter
output_options = PurchaseOrderLineItemOutputOptions
def create(self, request, *args, **kwargs):
"""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."""
output_options = PurchaseOrderLineItemOutputOptions
class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of PurchaseOrderExtraLine objects."""
@@ -818,13 +839,6 @@ class SalesOrderMixin:
def get_serializer(self, *args, **kwargs):
"""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
kwargs['context'] = self.get_serializer_context()
@@ -843,8 +857,18 @@ class SalesOrderMixin:
return queryset
class SalesOrderOutputOptions(OutputConfiguration):
"""Output options for the SalesOrder endpoint."""
OPTIONS = [InvenTreeOutputOption('customer_detail')]
class SalesOrderList(
SalesOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
SalesOrderMixin,
OrderCreateMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateAPI,
):
"""API endpoint for accessing a list of SalesOrder objects.
@@ -856,6 +880,8 @@ class SalesOrderList(
filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = SalesOrderOutputOptions
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'],
@@ -889,9 +915,11 @@ class SalesOrderList(
ordering = '-reference'
class SalesOrderDetail(SalesOrderMixin, RetrieveUpdateDestroyAPI):
class SalesOrderDetail(SalesOrderMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrder object."""
output_options = SalesOrderOutputOptions
class SalesOrderLineItemFilter(LineItemFilter):
"""Custom filters for SalesOrderLineItemList endpoint."""
@@ -1002,16 +1030,6 @@ class SalesOrderLineItemMixin:
def get_serializer(self, *args, **kwargs):
"""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()
return super().get_serializer(*args, **kwargs)
@@ -1038,8 +1056,18 @@ class SalesOrderLineItemMixin:
return queryset
class SalesOrderLineItemOutputOptions(OutputConfiguration):
"""Output options for the SalesOrderAllocation endpoint."""
OPTIONS = [
InvenTreeOutputOption('part_detail'),
InvenTreeOutputOption('order_detail'),
InvenTreeOutputOption('customer_detail'),
]
class SalesOrderLineItemList(
SalesOrderLineItemMixin, DataExportViewMixin, ListCreateAPI
SalesOrderLineItemMixin, DataExportViewMixin, OutputOptionsMixin, ListCreateAPI
):
"""API endpoint for accessing a list of SalesOrderLineItem objects."""
@@ -1047,6 +1075,8 @@ class SalesOrderLineItemList(
filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = SalesOrderLineItemOutputOptions
ordering_fields = [
'customer',
'order',
@@ -1069,9 +1099,13 @@ class SalesOrderLineItemList(
search_fields = ['part__name', 'quantity', 'reference']
class SalesOrderLineItemDetail(SalesOrderLineItemMixin, RetrieveUpdateDestroyAPI):
class SalesOrderLineItemDetail(
SalesOrderLineItemMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
):
"""API endpoint for detail view of a SalesOrderLineItem object."""
output_options = SalesOrderLineItemOutputOptions
class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of SalesOrderExtraLine objects."""
@@ -1265,11 +1299,26 @@ class SalesOrderAllocationMixin:
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."""
filterset_class = SalesOrderAllocationFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = SalesOrderAllocationOutputOptions
ordering_fields = [
'quantity',
@@ -1299,24 +1348,6 @@ class SalesOrderAllocationList(SalesOrderAllocationMixin, BulkUpdateMixin, ListA
'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):
"""API endpoint for detali view of a SalesOrderAllocation object."""
@@ -1470,13 +1501,6 @@ class ReturnOrderMixin:
def get_serializer(self, *args, **kwargs):
"""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
kwargs['context'] = self.get_serializer_context()
@@ -1495,8 +1519,18 @@ class ReturnOrderMixin:
return queryset
class ReturnOrderOutputOptions(OutputConfiguration):
"""Output options for the ReturnOrder endpoint."""
OPTIONS = [InvenTreeOutputOption(flag='customer_detail')]
class ReturnOrderList(
ReturnOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
ReturnOrderMixin,
OrderCreateMixin,
DataExportViewMixin,
OutputOptionsMixin,
ListCreateAPI,
):
"""API endpoint for accessing a list of ReturnOrder objects."""
@@ -1504,6 +1538,8 @@ class ReturnOrderList(
filter_backends = SEARCH_ORDER_FILTER_ALIAS
output_options = ReturnOrderOutputOptions
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'],
@@ -1534,9 +1570,11 @@ class ReturnOrderList(
ordering = '-reference'
class ReturnOrderDetail(ReturnOrderMixin, RetrieveUpdateDestroyAPI):
class ReturnOrderDetail(ReturnOrderMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single ReturnOrder object."""
output_options = ReturnOrderOutputOptions
class ReturnOrderContextMixin:
"""Simple mixin class to add a ReturnOrder to the serializer context."""
@@ -1620,15 +1658,6 @@ class ReturnOrderLineItemMixin:
def get_serializer(self, *args, **kwargs):
"""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()
return super().get_serializer(*args, **kwargs)
@@ -1642,8 +1671,18 @@ class ReturnOrderLineItemMixin:
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(
ReturnOrderLineItemMixin, DataExportViewMixin, ListCreateAPI
ReturnOrderLineItemMixin, DataExportViewMixin, OutputOptionsMixin, ListCreateAPI
):
"""API endpoint for accessing a list of ReturnOrderLineItemList objects."""
@@ -1651,6 +1690,8 @@ class ReturnOrderLineItemList(
filter_backends = SEARCH_ORDER_FILTER
output_options = ReturnOrderLineItemOutputOptions
ordering_fields = ['reference', 'target_date', 'received_date']
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."""
output_options = ReturnOrderLineItemOutputOptions
class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of ReturnOrderExtraLine objects."""

View File

@@ -46,6 +46,14 @@
part: 5
quantity: 1
- model: order.salesorderlineitem
pk: 2
fields:
order: 4
part: 5
quantity: 1
# An extra line item
- model: order.salesorderextraline
pk: 1
@@ -60,3 +68,37 @@
fields:
order: 1
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)
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):
"""Test that we can create / edit and delete a PurchaseOrder via the API."""
n = models.PurchaseOrder.objects.count()
@@ -852,6 +861,20 @@ class PurchaseOrderLineItemTest(OrderTest):
).json()
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):
"""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.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):
"""Tests for the SalesOrderLineItem API."""
@@ -1821,8 +1852,8 @@ class SalesOrderLineItemTest(OrderTest):
self.filter({'completed': 0}, n)
# Filter by 'allocated' status
self.filter({'allocated': 'true'}, 0)
self.filter({'allocated': 'false'}, n)
self.filter({'allocated': 'true'}, 1)
self.filter({'allocated': 'false'}, n - 1)
def test_so_line_allocated_filters(self):
"""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': 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):
"""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()
)
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):
"""Unit tests for ReturnOrder API endpoints."""
@@ -2618,6 +2677,104 @@ class ReturnOrderTests(InvenTreeAPITestCase):
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):
"""Unit tests for the various metadata endpoints of API."""