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:
@@ -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
|
||||
|
||||
|
@@ -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."""
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
Reference in New Issue
Block a user