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