mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-21 10:40:52 +00:00
[API] Filter refactor (#11073)
* Lazy evaluation of optional serializer fields - Add OptionalField dataclass - Pass serializer class and kwargs separately * Refactor BuildLineSerializer class * Simplify gathering * Refactor BuildSerializer * Refactor other Build serializers * Refactor Part serializers * Refactoring more serializers to use OptionalField * More refactoring * Cleanup for mixin class * Ensure any optional fields we added in are not missed * Fixes * Rehydrate optional fields for metadata * Add TreePathSerializer class * Further improvements: - Handle case where optional field shadows model property - Consider read_only and write_only fields * Adjust unit tests * Fix for "build_relational_field" - Handle case where optional field shadows model relation * Fix case where multiple fields can share same filter * additional unit tests * Bump API version * Remove top-level detection - Request object is only available for the top-level serializer anyway * Cache serializer to prevent multiple __init__ calls * Revert caching change - Breaks search results * Simplify field removal * Adjust unit test * Remove docstring comment which is no longer true * Ensure read-only fields are skipped for data import * Use SAFE_METHODS * Do not convert to lowercase * Updated docstring * Remove FilterableSerializerField mixin - Annotation now performed using OptionalField - Code can be greatly simplified * Ensure all fields are returned when generating schema * Fix order of operations * Add assertion to unit test * fix style * Fix api_version * Remove duplicate API entries * Remove duplicate API entries * Fix formatting in api_version.py * Tweak ManufacturerPart serializer * Revert formatting change --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 477
|
INVENTREE_API_VERSION = 478
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v478 -> 2026-04-11 : https://github.com/inventree/InvenTree/pull/11073
|
||||||
|
- Add OptionalField class for cleaner handling of optional fields in serializers
|
||||||
|
|
||||||
v477 -> 2026-04-11 : https://github.com/inventree/InvenTree/pull/11617
|
v477 -> 2026-04-11 : https://github.com/inventree/InvenTree/pull/11617
|
||||||
- Non-functional refactor, adaptations of descriptions
|
- Non-functional refactor, adaptations of descriptions
|
||||||
|
|
||||||
|
|||||||
@@ -380,6 +380,15 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
We take the regular DRF metadata and add our own unique flavor
|
We take the regular DRF metadata and add our own unique flavor
|
||||||
"""
|
"""
|
||||||
|
from InvenTree.serializers import OptionalField
|
||||||
|
|
||||||
|
if isinstance(field, OptionalField) or issubclass(
|
||||||
|
field.__class__, OptionalField
|
||||||
|
):
|
||||||
|
# Rehydrate the OptionalField for proper introspection
|
||||||
|
rehydrated_field = field.serializer_class(**(field.serializer_kwargs or {}))
|
||||||
|
return self.get_field_info(rehydrated_field)
|
||||||
|
|
||||||
# Try to add the child property to the dependent field to be used by the super call
|
# Try to add the child property to the dependent field to be used by the super call
|
||||||
if self.label_lookup[field] == 'dependent field':
|
if self.label_lookup[field] == 'dependent field':
|
||||||
field.get_child(raise_exception=True)
|
field.get_child(raise_exception=True)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from rest_framework import generics, mixins, status
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
import data_exporter.mixins
|
import data_exporter.mixins
|
||||||
import data_exporter.serializers
|
|
||||||
import importer.mixins
|
import importer.mixins
|
||||||
from InvenTree.fields import InvenTreeNotesField, OutputConfiguration
|
from InvenTree.fields import InvenTreeNotesField, OutputConfiguration
|
||||||
from InvenTree.helpers import (
|
from InvenTree.helpers import (
|
||||||
@@ -214,20 +213,6 @@ class OutputOptionsMixin:
|
|||||||
if getattr(cls, 'output_options', None) is not None:
|
if getattr(cls, 'output_options', None) is not None:
|
||||||
schema_for_view_output_options(cls)
|
schema_for_view_output_options(cls)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the mixin. Check that the serializer is compatible."""
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Check that the serializer was defined
|
|
||||||
if (
|
|
||||||
hasattr(self, 'serializer_class')
|
|
||||||
and isinstance(self.serializer_class, type)
|
|
||||||
and (not issubclass(self.serializer_class, FilterableSerializerMixin))
|
|
||||||
):
|
|
||||||
raise Exception(
|
|
||||||
'INVE-I2: `OutputOptionsMixin` can only be used with serializers that contain the `FilterableSerializerMixin` mixin'
|
|
||||||
)
|
|
||||||
|
|
||||||
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."""
|
||||||
request = getattr(self, 'request', None)
|
request = getattr(self, 'request', None)
|
||||||
@@ -241,20 +226,7 @@ class OutputOptionsMixin:
|
|||||||
context['request'] = request
|
context['request'] = request
|
||||||
kwargs['context'] = context
|
kwargs['context'] = context
|
||||||
|
|
||||||
serializer = super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
# Check if the serializer actually can be filtered - makes not much sense to use this mixin without that prerequisite
|
|
||||||
if isinstance(
|
|
||||||
serializer, data_exporter.serializers.DataExportOptionsSerializer
|
|
||||||
):
|
|
||||||
# Skip in this instance, special case for determining export options
|
|
||||||
pass
|
|
||||||
elif not isinstance(serializer, FilterableSerializerMixin):
|
|
||||||
raise Exception(
|
|
||||||
'INVE-I2: `OutputOptionsMixin` can only be used with serializers that contain the `FilterableSerializerMixin` mixin'
|
|
||||||
)
|
|
||||||
|
|
||||||
return serializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return the queryset with output options applied.
|
"""Return the queryset with output options applied.
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
@@ -21,9 +22,9 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.fields import empty
|
from rest_framework.fields import empty
|
||||||
from rest_framework.mixins import ListModelMixin
|
from rest_framework.mixins import ListModelMixin
|
||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
from rest_framework.serializers import DecimalField
|
from rest_framework.serializers import DecimalField, Serializer
|
||||||
from rest_framework.utils import model_meta
|
from rest_framework.utils import model_meta
|
||||||
from taggit.serializers import TaggitSerializer, TagListSerializerField
|
from taggit.serializers import TaggitSerializer
|
||||||
|
|
||||||
import common.models as common_models
|
import common.models as common_models
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
@@ -33,94 +34,239 @@ from InvenTree.helpers import str2bool
|
|||||||
from InvenTree.helpers_model import getModelsWithMixin
|
from InvenTree.helpers_model import getModelsWithMixin
|
||||||
|
|
||||||
|
|
||||||
# region path filtering
|
@dataclass
|
||||||
class FilterableSerializerField:
|
class OptionalField:
|
||||||
"""Mixin to mark serializer as filterable.
|
"""DataClass used to optionally enable a serializer field.
|
||||||
|
|
||||||
This needs to be used in conjunction with `enable_filter` on the serializer field!
|
This is used in conjunction with the `FilterableSerializerMixin` to allow
|
||||||
"""
|
dynamic inclusion or exclusion of serializer fields at runtime.
|
||||||
|
|
||||||
is_filterable = None
|
Adding OptionalField instances to a serializer class is more "efficient"
|
||||||
is_filterable_vals = {}
|
than directly adding the field (and later removing it),
|
||||||
|
as the field is never instantiated unless it is required.
|
||||||
|
|
||||||
# Options for automatic queryset prefetching
|
Additionally, you can specify prefetch fields which will be applied
|
||||||
prefetch_fields: Optional[list[str]] = None
|
to the queryset, *only* if the field is included in the final serializer.
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
This allows for optimization of database queries based only on the requested data.
|
||||||
"""Initialize the serializer."""
|
|
||||||
self.is_filterable = kwargs.pop('is_filterable', None)
|
|
||||||
self.is_filterable_vals = kwargs.pop('is_filterable_vals', {})
|
|
||||||
self.prefetch_fields = kwargs.pop('prefetch_fields', None)
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
Example:
|
||||||
|
class MySerializer(FilterableSerializerMixin, serializers.ModelSerializer):
|
||||||
|
my_optional_field = OptionalField(
|
||||||
def enable_filter(
|
serializer_class=serializers.CharField,
|
||||||
func: Any,
|
default_include=False,
|
||||||
default_include: bool = False,
|
filter_name='include_my_field',
|
||||||
filter_name: Optional[str] = None,
|
serializer_kwargs={
|
||||||
filter_by_query: bool = True,
|
'help_text': 'This is an optional field',
|
||||||
prefetch_fields: Optional[list[str]] = None,
|
'read_only': True,
|
||||||
):
|
},
|
||||||
"""Decorator for marking a serializer field as filterable.
|
prefetch_fields=['related_field'],
|
||||||
|
|
||||||
This can be customized by passing in arguments. This only works in conjunction with serializer fields or serializers that contain the `FilterableSerializerField` mixin.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
func: The serializer field to mark as filterable. Will automatically be passed when used as a decorator.
|
|
||||||
default_include (bool): If True, the field will be included by default unless explicitly excluded. If False, the field will be excluded by default unless explicitly included.
|
|
||||||
filter_name (str, optional): The name of the filter parameter to use in the URL. If None, the function name of the (decorated) function will be used.
|
|
||||||
filter_by_query (bool): If True, also look for filter parameters in the request query parameters.
|
|
||||||
prefetch_fields (list of str, optional): List of related fields to prefetch when this field is included. This can be used to optimize database queries.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The decorated serializer field, marked as filterable.
|
|
||||||
"""
|
|
||||||
# Ensure this function can be actually filtered
|
|
||||||
if not issubclass(func.__class__, FilterableSerializerField):
|
|
||||||
raise TypeError(
|
|
||||||
'INVE-I2: `enable_filter` can only be applied to serializer fields / serializers that contain the `FilterableSerializerField` mixin!'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mark the function as filterable
|
"""
|
||||||
func._kwargs['is_filterable'] = True
|
|
||||||
func._kwargs['is_filterable_vals'] = {
|
|
||||||
'default': default_include,
|
|
||||||
'filter_name': filter_name if filter_name else func.field_name,
|
|
||||||
'filter_by_query': filter_by_query,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Attach queryset prefetching information
|
serializer_class: Serializer
|
||||||
func._kwargs['prefetch_fields'] = prefetch_fields
|
serializer_kwargs: Optional[dict] = None
|
||||||
|
default_include: bool = False
|
||||||
return func
|
filter_name: Optional[str] = None
|
||||||
|
filter_by_query: bool = True
|
||||||
|
prefetch_fields: Optional[list[str]] = None
|
||||||
|
|
||||||
|
|
||||||
class FilterableSerializerMixin:
|
class FilterableSerializerMixin:
|
||||||
"""Mixin that enables filtering of marked fields on a serializer.
|
"""Mixin that enables filtering of marked fields on a serializer.
|
||||||
|
|
||||||
Use the `enable_filter` decorator to mark serializer fields as filterable.
|
Use the `OptionalField` helper class to mark serializer fields as filterable.
|
||||||
This introduces overhead during initialization, so only use this mixin when necessary.
|
This introduces overhead during initialization, so only use this mixin when necessary.
|
||||||
If you need to mark a serializer as filterable but it does not contain any filterable fields, set `no_filters = True` to avoid getting an exception that protects against over-application of this mixin.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_was_filtered = False
|
optional_filters: dict = None
|
||||||
no_filters = False
|
fields_to_remove: set = None
|
||||||
"""If True, do not raise an exception if no filterable fields are found."""
|
optional_fields: set = None
|
||||||
filter_on_query = True
|
filter_on_query: bool = True
|
||||||
"""If True, also look for filter parameters in the request query parameters."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialization routine for the serializer. This gathers and applies filters through kwargs."""
|
"""Initialization routine for the serializer. This gathers and applies filters through kwargs."""
|
||||||
# add list_serializer_class to meta if not present - reduces duplication
|
# Extract some useful context information for later use
|
||||||
if not isinstance(self, FilterableListSerializer) and (
|
context = kwargs.get('context', {})
|
||||||
not hasattr(self.Meta, 'list_serializer_class')
|
self.request = context.get('request', None) or getattr(self, 'request', None)
|
||||||
):
|
self.request_query_params = (
|
||||||
self.Meta.list_serializer_class = FilterableListSerializer
|
dict(getattr(self.request, 'query_params', {})) if self.request else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.gather_optional_fields(kwargs)
|
||||||
|
|
||||||
self.gather_filters(kwargs)
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.do_filtering()
|
|
||||||
|
# Ensure any fields we are *not* using are removed
|
||||||
|
for field_name in self.fields_to_remove:
|
||||||
|
self.fields.pop(field_name, None)
|
||||||
|
|
||||||
|
def is_exporting(self) -> bool:
|
||||||
|
"""Determine if we are exporting data."""
|
||||||
|
return getattr(self, '_exporting_data', False)
|
||||||
|
|
||||||
|
def is_field_included(
|
||||||
|
self, field_name: str, field: OptionalField, kwargs: dict
|
||||||
|
) -> bool:
|
||||||
|
"""Determine at runtime whether an OptionalField should be included.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
field_name: Name of the field
|
||||||
|
field: The OptionalField instance
|
||||||
|
kwargs: The kwargs provided to the serializer instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the field should be included, False otherwise.
|
||||||
|
|
||||||
|
Order of operations:
|
||||||
|
|
||||||
|
- If we are generating the schema, always include the field
|
||||||
|
- If this is a write request (POST, PUT, PATCH) and we are not exporting, always include the field
|
||||||
|
- If this is a top-level serializer, check the request query parameters for the filter name
|
||||||
|
- Check the kwargs provided to the serializer instance
|
||||||
|
- Finally, fall back to the default_include value for the field itself
|
||||||
|
"""
|
||||||
|
field_ref = field.filter_name or field_name
|
||||||
|
|
||||||
|
# If we have already found a value for this filter, use it
|
||||||
|
# This allows multiple optional fields to share the same filter value
|
||||||
|
cached_value = self.optional_filters.get(field_ref, None)
|
||||||
|
|
||||||
|
if cached_value is not None:
|
||||||
|
return cached_value
|
||||||
|
|
||||||
|
# First, check kwargs provided to the serializer instance
|
||||||
|
# We also pop the value to avoid issues with nested serializers
|
||||||
|
value = kwargs.pop(field_ref, None)
|
||||||
|
|
||||||
|
# We do not want to pop fields while generating the schema
|
||||||
|
if InvenTree.ready.isGeneratingSchema():
|
||||||
|
return True
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
# Cache the value for future reference
|
||||||
|
self.optional_filters[field_ref] = value
|
||||||
|
|
||||||
|
field_kwargs = field.serializer_kwargs or {}
|
||||||
|
|
||||||
|
# Skip filtering for a write request - all fields should be present for data creation
|
||||||
|
if method := getattr(self.request, 'method', None):
|
||||||
|
if method not in SAFE_METHODS and not self.is_exporting():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Ignore write_only fields for read requests
|
||||||
|
if field_kwargs.get('write_only', False):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# For a top-level serializer, check request query parameters
|
||||||
|
if self.request and self.filter_on_query and field.filter_by_query:
|
||||||
|
param_value = self.request.query_params.get(field_ref, None)
|
||||||
|
|
||||||
|
if param_value is not None:
|
||||||
|
# Convert from list to single value if needed
|
||||||
|
if type(param_value) == list and len(param_value) == 1:
|
||||||
|
param_value = param_value[0]
|
||||||
|
|
||||||
|
value = str2bool(param_value)
|
||||||
|
|
||||||
|
# Cache the value for future reference
|
||||||
|
self.optional_filters[field_ref] = value
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = field.default_include
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def find_optional_fields(self):
|
||||||
|
"""Find all optional fields defined on this serializer."""
|
||||||
|
optional_fields = {}
|
||||||
|
|
||||||
|
# Walk upwards through the class hierarchy
|
||||||
|
seen_vars = set()
|
||||||
|
|
||||||
|
for base in self.__class__.__mro__:
|
||||||
|
for field_name, field in vars(base).items():
|
||||||
|
if field_name in seen_vars:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen_vars.add(field_name)
|
||||||
|
|
||||||
|
if field and isinstance(field, OptionalField):
|
||||||
|
optional_fields[field_name] = field
|
||||||
|
|
||||||
|
return optional_fields
|
||||||
|
|
||||||
|
def gather_optional_fields(self, kwargs):
|
||||||
|
"""Determine which optional fields will be included on this serializer.
|
||||||
|
|
||||||
|
Note that there may be instances of OptionalField in the field set,
|
||||||
|
which need to either be instantiated or removed.
|
||||||
|
"""
|
||||||
|
self.optional_filters = {}
|
||||||
|
self.prefetch_list = set()
|
||||||
|
self.fields_to_remove = set()
|
||||||
|
self.optional_fields = set()
|
||||||
|
|
||||||
|
for field_name, field in self.find_optional_fields().items():
|
||||||
|
if self.is_field_included(field_name, field, kwargs):
|
||||||
|
self.optional_fields.add(field_name)
|
||||||
|
# Add prefetch information
|
||||||
|
if field.prefetch_fields:
|
||||||
|
for pf in field.prefetch_fields:
|
||||||
|
self.prefetch_list.add(pf)
|
||||||
|
else:
|
||||||
|
self.fields_to_remove.add(field_name)
|
||||||
|
|
||||||
|
def get_field_names(self, declared_fields, info):
|
||||||
|
"""Remove unused fields before returning field names."""
|
||||||
|
field_names = super().get_field_names(declared_fields, info)
|
||||||
|
|
||||||
|
# Add any optional fields which are included
|
||||||
|
for field_name in self.optional_fields:
|
||||||
|
if field_name not in field_names:
|
||||||
|
field_names.append(field_name)
|
||||||
|
|
||||||
|
# Remove any fields which are marked for removal
|
||||||
|
for field_name in self.fields_to_remove:
|
||||||
|
if field_name in field_names:
|
||||||
|
field_names.remove(field_name)
|
||||||
|
|
||||||
|
return field_names
|
||||||
|
|
||||||
|
def build_optional_field(self, field_name: str):
|
||||||
|
"""Build an optional field, based on the provided field name."""
|
||||||
|
field = getattr(self, field_name, None)
|
||||||
|
|
||||||
|
if field and isinstance(field, OptionalField):
|
||||||
|
serializer_kwargs = {**field.serializer_kwargs} or {}
|
||||||
|
return field.serializer_class, serializer_kwargs
|
||||||
|
|
||||||
|
def build_relational_field(self, field_name, relation_info):
|
||||||
|
"""Handle a special case where an OptionalField shadows a model relation."""
|
||||||
|
if field_name in self.optional_fields:
|
||||||
|
if field := self.build_optional_field(field_name):
|
||||||
|
return field
|
||||||
|
|
||||||
|
return super().build_relational_field(field_name, relation_info)
|
||||||
|
|
||||||
|
def build_property_field(self, field_name, model_class):
|
||||||
|
"""Handle a special case where an OptionalField shadows a model property."""
|
||||||
|
if field_name in self.optional_fields:
|
||||||
|
if field := self.build_optional_field(field_name):
|
||||||
|
return field
|
||||||
|
|
||||||
|
return super().build_property_field(field_name, model_class)
|
||||||
|
|
||||||
|
def build_unknown_field(self, field_name, model_class):
|
||||||
|
"""Perform lazy initialization of OptionalFields.
|
||||||
|
|
||||||
|
The DRF framework calls this method when it encounters a field which is not yet initialized.
|
||||||
|
"""
|
||||||
|
if field := self.build_optional_field(field_name):
|
||||||
|
return field
|
||||||
|
|
||||||
|
return super().build_unknown_field(field_name, model_class)
|
||||||
|
|
||||||
def prefetch_queryset(self, queryset: QuerySet) -> QuerySet:
|
def prefetch_queryset(self, queryset: QuerySet) -> QuerySet:
|
||||||
"""Apply any prefetching to the queryset based on the optionally included fields.
|
"""Apply any prefetching to the queryset based on the optionally included fields.
|
||||||
@@ -140,157 +286,48 @@ class FilterableSerializerMixin:
|
|||||||
if getattr(request, '_metadata_requested', False):
|
if getattr(request, '_metadata_requested', False):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
# Gather up the set of simple 'prefetch' fields and functions
|
if self.prefetch_list and len(self.prefetch_list) > 0:
|
||||||
prefetch_fields = set()
|
queryset = queryset.prefetch_related(*list(self.prefetch_list))
|
||||||
|
|
||||||
filterable_fields = [
|
|
||||||
field
|
|
||||||
for field in self.fields.values()
|
|
||||||
if getattr(field, 'is_filterable', None)
|
|
||||||
]
|
|
||||||
|
|
||||||
for field in filterable_fields:
|
|
||||||
if prefetch_names := getattr(field, 'prefetch_fields', None):
|
|
||||||
for pf in prefetch_names:
|
|
||||||
prefetch_fields.add(pf)
|
|
||||||
|
|
||||||
if prefetch_fields and len(prefetch_fields) > 0:
|
|
||||||
queryset = queryset.prefetch_related(*list(prefetch_fields))
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def gather_filters(self, kwargs) -> None:
|
|
||||||
"""Gather filterable fields through introspection."""
|
|
||||||
context = kwargs.get('context', {})
|
|
||||||
request = context.get('request', None) or getattr(self, 'request', None)
|
|
||||||
|
|
||||||
# Gather query parameters from the request context
|
|
||||||
query_params = dict(getattr(request, 'query_params', {})) if request else {}
|
|
||||||
|
|
||||||
# Fast exit if this has already been done or would not have any effect
|
|
||||||
if getattr(self, '_was_filtered', False) or not hasattr(self, 'fields'):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Actually gather the filterable fields
|
|
||||||
# Also see `enable_filter` where` is_filterable and is_filterable_vals are set
|
|
||||||
self.filter_targets: dict[str, dict] = {
|
|
||||||
str(k): {'serializer': a, **getattr(a, 'is_filterable_vals', {})}
|
|
||||||
for k, a in self.fields.items()
|
|
||||||
if getattr(a, 'is_filterable', None)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove filter args from kwargs to avoid issues with super().__init__
|
|
||||||
popped_kwargs = {} # store popped kwargs as a arg might be reused for multiple fields
|
|
||||||
tgs_vals: dict[str, bool] = {}
|
|
||||||
for k, v in self.filter_targets.items():
|
|
||||||
pop_ref = v['filter_name'] or k
|
|
||||||
val = kwargs.pop(pop_ref, popped_kwargs.get(pop_ref))
|
|
||||||
# Optionally also look in query parameters
|
|
||||||
# Note that we only do this for a top-level serializer, to avoid issues with nested serializers
|
|
||||||
if (
|
|
||||||
request
|
|
||||||
and val is None
|
|
||||||
and self.filter_on_query
|
|
||||||
and v.get('filter_by_query', True)
|
|
||||||
):
|
|
||||||
val = query_params.pop(pop_ref, None)
|
|
||||||
|
|
||||||
if isinstance(val, list) and len(val) == 1:
|
|
||||||
val = val[0]
|
|
||||||
|
|
||||||
if val: # Save popped value for reuse
|
|
||||||
popped_kwargs[pop_ref] = val
|
|
||||||
tgs_vals[k] = (
|
|
||||||
str2bool(val) if isinstance(val, (str, int, float)) else val
|
|
||||||
) # Support for various filtering style for backwards compatibility
|
|
||||||
|
|
||||||
self.filter_target_values = tgs_vals
|
|
||||||
self._was_filtered = True
|
|
||||||
|
|
||||||
# Ensure this mixin is not broadly applied as it is expensive on scale (total CI time increased by 21% when running all coverage tests)
|
|
||||||
if len(self.filter_targets) == 0 and not self.no_filters:
|
|
||||||
raise Exception(
|
|
||||||
'INVE-I2: No filter targets found in fields, remove `PathScopedMixin`'
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_filtering(self) -> None:
|
|
||||||
"""Do the actual filtering."""
|
|
||||||
# This serializer might not contain filters or we do not want to pop fields while generating the schema
|
|
||||||
if (
|
|
||||||
not hasattr(self, 'filter_target_values')
|
|
||||||
or InvenTree.ready.isGeneratingSchema()
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
is_exporting = getattr(self, '_exporting_data', False)
|
|
||||||
|
|
||||||
# Skip filtering for a write requests - all fields should be present for data creation
|
|
||||||
if request := self.context.get('request', None):
|
|
||||||
if method := getattr(request, 'method', None):
|
|
||||||
if method not in SAFE_METHODS and not is_exporting:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Throw out fields which are not requested (either by default or explicitly)
|
|
||||||
for k, v in self.filter_target_values.items():
|
|
||||||
# See `enable_filter` where` is_filterable and is_filterable_vals are set
|
|
||||||
value = v if v is not None else bool(self.filter_targets[k]['default'])
|
|
||||||
if value is not True:
|
|
||||||
self.fields.pop(k, None)
|
|
||||||
|
|
||||||
|
|
||||||
# special serializers which allow filtering
|
|
||||||
class FilterableListSerializer(
|
|
||||||
FilterableSerializerField, FilterableSerializerMixin, serializers.ListSerializer
|
|
||||||
):
|
|
||||||
"""Custom ListSerializer which allows filtering of fields."""
|
|
||||||
|
|
||||||
|
|
||||||
# special serializer fields which allow filtering
|
|
||||||
class FilterableListField(FilterableSerializerField, serializers.ListField):
|
|
||||||
"""Custom ListField which allows filtering."""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterableSerializerMethodField(
|
|
||||||
FilterableSerializerField, serializers.SerializerMethodField
|
|
||||||
):
|
|
||||||
"""Custom SerializerMethodField which allows filtering."""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterableDateTimeField(FilterableSerializerField, serializers.DateTimeField):
|
|
||||||
"""Custom DateTimeField which allows filtering."""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterableFloatField(FilterableSerializerField, serializers.FloatField):
|
|
||||||
"""Custom FloatField which allows filtering."""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterableCharField(FilterableSerializerField, serializers.CharField):
|
|
||||||
"""Custom CharField which allows filtering."""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterableIntegerField(FilterableSerializerField, serializers.IntegerField):
|
|
||||||
"""Custom IntegerField which allows filtering."""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterableTagListField(FilterableSerializerField, TagListSerializerField):
|
|
||||||
"""Custom TagListSerializerField which allows filtering."""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Empty Meta class."""
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
|
|
||||||
class EmptySerializer(serializers.Serializer):
|
class EmptySerializer(serializers.Serializer):
|
||||||
"""Empty serializer for use in testing."""
|
"""Empty serializer for use in testing."""
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeMoneySerializer(FilterableSerializerField, MoneyField):
|
class TreePathSerializer(serializers.Serializer):
|
||||||
|
"""Serializer field for representing a tree path."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Metaclass options."""
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'name',
|
||||||
|
# Any fields after this point are optional, and can be included via extra_fields
|
||||||
|
'icon',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, extra_fields: Optional[list[str]] = None, **kwargs):
|
||||||
|
"""Initialize the TreePathSerializer."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
allowed_fields = ['pk', 'name', *(extra_fields or [])]
|
||||||
|
|
||||||
|
for field in list(self.fields.keys()):
|
||||||
|
if field not in allowed_fields:
|
||||||
|
self.fields.pop(field, None)
|
||||||
|
|
||||||
|
pk = serializers.IntegerField(read_only=True)
|
||||||
|
name = serializers.CharField(read_only=True)
|
||||||
|
icon = serializers.CharField(required=False, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeMoneySerializer(MoneyField):
|
||||||
"""Custom serializer for 'MoneyField', which ensures that passed values are numerically valid.
|
"""Custom serializer for 'MoneyField', which ensures that passed values are numerically valid.
|
||||||
|
|
||||||
Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py
|
Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py
|
||||||
This field allows filtering.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -477,7 +514,7 @@ class DependentField(serializers.Field):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeModelSerializer(FilterableSerializerField, serializers.ModelSerializer):
|
class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||||
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
|
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
|
||||||
|
|
||||||
# Switch out URLField mapping
|
# Switch out URLField mapping
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from rest_framework.serializers import SerializerMethodField
|
|||||||
|
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin
|
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin
|
||||||
|
from InvenTree.serializers import OptionalField
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from InvenTree.urls import backendpatterns
|
from InvenTree.urls import backendpatterns
|
||||||
|
|
||||||
@@ -25,21 +26,25 @@ class SampleSerializer(
|
|||||||
fields = ['field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'id']
|
fields = ['field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'id']
|
||||||
|
|
||||||
field_a = SerializerMethodField(method_name='sample')
|
field_a = SerializerMethodField(method_name='sample')
|
||||||
field_b = InvenTree.serializers.enable_filter(
|
field_b = OptionalField(
|
||||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample')
|
serializer_class=SerializerMethodField,
|
||||||
|
serializer_kwargs={'method_name': 'sample'},
|
||||||
)
|
)
|
||||||
field_c = InvenTree.serializers.enable_filter(
|
field_c = OptionalField(
|
||||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
|
serializer_class=SerializerMethodField,
|
||||||
True,
|
serializer_kwargs={'method_name': 'sample'},
|
||||||
|
default_include=True,
|
||||||
filter_name='crazy_name',
|
filter_name='crazy_name',
|
||||||
)
|
)
|
||||||
field_d = InvenTree.serializers.enable_filter(
|
field_d = OptionalField(
|
||||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
|
serializer_class=SerializerMethodField,
|
||||||
True,
|
serializer_kwargs={'method_name': 'sample'},
|
||||||
|
default_include=True,
|
||||||
filter_name='crazy_name',
|
filter_name='crazy_name',
|
||||||
)
|
)
|
||||||
field_e = InvenTree.serializers.enable_filter(
|
field_e = OptionalField(
|
||||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
|
serializer_class=SerializerMethodField,
|
||||||
|
serializer_kwargs={'method_name': 'sample'},
|
||||||
filter_name='field_e',
|
filter_name='field_e',
|
||||||
filter_by_query=False,
|
filter_by_query=False,
|
||||||
)
|
)
|
||||||
@@ -106,110 +111,3 @@ class FilteredSerializers(InvenTreeAPITestCase):
|
|||||||
self.assertContains(response, 'field_c')
|
self.assertContains(response, 'field_c')
|
||||||
self.assertContains(response, 'field_d')
|
self.assertContains(response, 'field_d')
|
||||||
self.assertNotContains(response, 'field_e')
|
self.assertNotContains(response, 'field_e')
|
||||||
|
|
||||||
def test_failiure_enable_filter(self):
|
|
||||||
"""Test sanity check for enable_filter."""
|
|
||||||
# Allowed usage
|
|
||||||
field_b = InvenTree.serializers.enable_filter( # noqa: F841
|
|
||||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Disallowed usage
|
|
||||||
with self.assertRaises(Exception) as cm:
|
|
||||||
field_a = InvenTree.serializers.enable_filter( # noqa: F841
|
|
||||||
SerializerMethodField(method_name='sample')
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
'INVE-I2: `enable_filter` can only be applied to serializer fields',
|
|
||||||
str(cm.exception),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_failiure_FilterableSerializerMixin(self):
|
|
||||||
"""Test failure case for FilteredSerializerMixin."""
|
|
||||||
|
|
||||||
class BadSerializer(
|
|
||||||
InvenTree.serializers.FilterableSerializerMixin,
|
|
||||||
InvenTree.serializers.InvenTreeModelSerializer,
|
|
||||||
):
|
|
||||||
"""Bad serializer for testing FilterableSerializerMixin."""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta options."""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
fields = ['field_a', 'id']
|
|
||||||
|
|
||||||
field_a = SerializerMethodField(method_name='sample')
|
|
||||||
|
|
||||||
def sample(self, obj):
|
|
||||||
"""Sample method field."""
|
|
||||||
return 'sample' # pragma: no cover
|
|
||||||
|
|
||||||
with self.assertRaises(Exception) as cm:
|
|
||||||
_ = BadSerializer()
|
|
||||||
self.assertIn(
|
|
||||||
'INVE-I2: No filter targets found in fields, remove `PathScopedMixin`',
|
|
||||||
str(cm.exception),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test override
|
|
||||||
BadSerializer.no_filters = True
|
|
||||||
_ = BadSerializer()
|
|
||||||
self.assertTrue(True) # Dummy assertion to ensure we reach here
|
|
||||||
|
|
||||||
def test_failure_OutputOptionsMixin(self):
|
|
||||||
"""Test failure case for OutputOptionsMixin."""
|
|
||||||
|
|
||||||
class BadSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|
||||||
"""Sample serializer."""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta options."""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
fields = ['id']
|
|
||||||
|
|
||||||
field_a = SerializerMethodField(method_name='sample')
|
|
||||||
|
|
||||||
# Bad implementation of OutputOptionsMixin
|
|
||||||
with self.assertRaises(Exception) as cm:
|
|
||||||
|
|
||||||
class BadList(OutputOptionsMixin, ListCreateAPI):
|
|
||||||
"""Bad list endpoint for testing OutputOptionsMixin."""
|
|
||||||
|
|
||||||
serializer_class = BadSerializer
|
|
||||||
queryset = User.objects.all()
|
|
||||||
permission_classes = []
|
|
||||||
|
|
||||||
self.assertTrue(True)
|
|
||||||
_ = BadList() # this should raise an exception
|
|
||||||
self.assertEqual(
|
|
||||||
str(cm.exception),
|
|
||||||
'INVE-I2: `OutputOptionsMixin` can only be used with serializers that contain the `FilterableSerializerMixin` mixin',
|
|
||||||
)
|
|
||||||
|
|
||||||
# More creative bad implementation
|
|
||||||
with self.assertRaises(Exception) as cm:
|
|
||||||
|
|
||||||
class BadList(OutputOptionsMixin, ListCreateAPI):
|
|
||||||
"""Bad list endpoint for testing OutputOptionsMixin."""
|
|
||||||
|
|
||||||
queryset = User.objects.all()
|
|
||||||
permission_classes = []
|
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
|
||||||
"""Get serializer override."""
|
|
||||||
self.serializer_class = BadSerializer
|
|
||||||
return super().get_serializer(*args, **kwargs)
|
|
||||||
|
|
||||||
view = BadList()
|
|
||||||
self.assertTrue(True)
|
|
||||||
# mock some stuff to allow get_serializer to run
|
|
||||||
view.request = self.client.request()
|
|
||||||
view.format_kwarg = {}
|
|
||||||
view.get_serializer() # this should raise an exception
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(cm.exception),
|
|
||||||
'INVE-I2: `OutputOptionsMixin` can only be used with serializers that contain the `FilterableSerializerMixin` mixin',
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from InvenTree.serializers import (
|
|||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
NotesFieldMixin,
|
NotesFieldMixin,
|
||||||
enable_filter,
|
OptionalField,
|
||||||
)
|
)
|
||||||
from stock.generators import generate_batch_code
|
from stock.generators import generate_batch_code
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
@@ -116,9 +116,10 @@ class BuildSerializer(
|
|||||||
|
|
||||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(source='part', many=False, read_only=True),
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
True,
|
serializer_kwargs={'source': 'part', 'many': False, 'read_only': True},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['part', 'part__category', 'part__pricing_data'],
|
prefetch_fields=['part', 'part__category', 'part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,16 +133,22 @@ class BuildSerializer(
|
|||||||
|
|
||||||
overdue = serializers.BooleanField(read_only=True, default=False)
|
overdue = serializers.BooleanField(read_only=True, default=False)
|
||||||
|
|
||||||
issued_by_detail = enable_filter(
|
issued_by_detail = OptionalField(
|
||||||
UserSerializer(source='issued_by', read_only=True),
|
serializer_class=UserSerializer,
|
||||||
True,
|
serializer_kwargs={'source': 'issued_by', 'read_only': True},
|
||||||
|
default_include=True,
|
||||||
filter_name='user_detail',
|
filter_name='user_detail',
|
||||||
prefetch_fields=['issued_by'],
|
prefetch_fields=['issued_by'],
|
||||||
)
|
)
|
||||||
|
|
||||||
responsible_detail = enable_filter(
|
responsible_detail = OptionalField(
|
||||||
OwnerSerializer(source='responsible', read_only=True, allow_null=True),
|
serializer_class=OwnerSerializer,
|
||||||
True,
|
serializer_kwargs={
|
||||||
|
'source': 'responsible',
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='user_detail',
|
filter_name='user_detail',
|
||||||
prefetch_fields=['responsible'],
|
prefetch_fields=['responsible'],
|
||||||
)
|
)
|
||||||
@@ -1200,31 +1207,33 @@ class BuildItemSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Extra (optional) detail fields
|
# Extra (optional) detail fields
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
label=_('Part'),
|
serializer_kwargs={
|
||||||
source='stock_item.part',
|
'label': _('Part'),
|
||||||
many=False,
|
'source': 'stock_item.part',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
pricing=False,
|
'allow_null': True,
|
||||||
),
|
'pricing': False,
|
||||||
True,
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['stock_item__part'],
|
prefetch_fields=['stock_item__part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
stock_item_detail = enable_filter(
|
stock_item_detail = OptionalField(
|
||||||
StockItemSerializer(
|
serializer_class=StockItemSerializer,
|
||||||
source='stock_item',
|
serializer_kwargs={
|
||||||
read_only=True,
|
'source': 'stock_item',
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
label=_('Stock Item'),
|
'allow_null': True,
|
||||||
part_detail=False,
|
'label': _('Stock Item'),
|
||||||
location_detail=False,
|
'part_detail': False,
|
||||||
supplier_part_detail=False,
|
'location_detail': False,
|
||||||
path_detail=False,
|
'supplier_part_detail': False,
|
||||||
),
|
'path_detail': False,
|
||||||
True,
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='stock_detail',
|
filter_name='stock_detail',
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'stock_item',
|
'stock_item',
|
||||||
@@ -1234,18 +1243,19 @@ class BuildItemSerializer(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
install_into_detail = enable_filter(
|
install_into_detail = OptionalField(
|
||||||
StockItemSerializer(
|
serializer_class=StockItemSerializer,
|
||||||
source='install_into',
|
serializer_kwargs={
|
||||||
read_only=True,
|
'source': 'install_into',
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
label=_('Install Into'),
|
'allow_null': True,
|
||||||
part_detail=False,
|
'label': _('Install Into'),
|
||||||
location_detail=False,
|
'part_detail': False,
|
||||||
supplier_part_detail=False,
|
'location_detail': False,
|
||||||
path_detail=False,
|
'supplier_part_detail': False,
|
||||||
),
|
'path_detail': False,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['install_into', 'install_into__part'],
|
prefetch_fields=['install_into', 'install_into__part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1253,26 +1263,28 @@ class BuildItemSerializer(
|
|||||||
label=_('Location'), source='stock_item.location', many=False, read_only=True
|
label=_('Location'), source='stock_item.location', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
location_detail = enable_filter(
|
location_detail = OptionalField(
|
||||||
LocationBriefSerializer(
|
serializer_class=LocationBriefSerializer,
|
||||||
label=_('Location'),
|
serializer_kwargs={
|
||||||
source='stock_item.location',
|
'label': _('Location'),
|
||||||
read_only=True,
|
'source': 'stock_item.location',
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
True,
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['stock_item__location'],
|
prefetch_fields=['stock_item__location'],
|
||||||
)
|
)
|
||||||
|
|
||||||
build_detail = enable_filter(
|
build_detail = OptionalField(
|
||||||
BuildSerializer(
|
serializer_class=BuildSerializer,
|
||||||
label=_('Build'),
|
serializer_kwargs={
|
||||||
source='build_line.build',
|
'label': _('Build'),
|
||||||
many=False,
|
'source': 'build_line.build',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
True,
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'build_line__build',
|
'build_line__build',
|
||||||
'build_line__build__part',
|
'build_line__build__part',
|
||||||
@@ -1283,16 +1295,17 @@ class BuildItemSerializer(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_part_detail = enable_filter(
|
supplier_part_detail = OptionalField(
|
||||||
company.serializers.SupplierPartSerializer(
|
serializer_class=company.serializers.SupplierPartSerializer,
|
||||||
label=_('Supplier Part'),
|
serializer_kwargs={
|
||||||
source='stock_item.supplier_part',
|
'label': _('Supplier Part'),
|
||||||
many=False,
|
'source': 'stock_item.supplier_part',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
brief=True,
|
'allow_null': True,
|
||||||
),
|
'brief': True,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'stock_item__supplier_part',
|
'stock_item__supplier_part',
|
||||||
'stock_item__supplier_part__supplier',
|
'stock_item__supplier_part__supplier',
|
||||||
@@ -1382,11 +1395,15 @@ class BuildLineSerializer(
|
|||||||
read_only=True,
|
read_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
allocations = enable_filter(
|
allocations = OptionalField(
|
||||||
BuildItemSerializer(
|
serializer_class=BuildItemSerializer,
|
||||||
many=True, read_only=True, allow_null=True, build_detail=False
|
serializer_kwargs={
|
||||||
),
|
'many': True,
|
||||||
True,
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'build_detail': False,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'allocations',
|
'allocations',
|
||||||
'allocations__stock_item',
|
'allocations__stock_item',
|
||||||
@@ -1427,73 +1444,79 @@ class BuildLineSerializer(
|
|||||||
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
|
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
|
||||||
|
|
||||||
# Foreign key fields
|
# Foreign key fields
|
||||||
bom_item_detail = enable_filter(
|
bom_item_detail = OptionalField(
|
||||||
part_serializers.BomItemSerializer(
|
serializer_class=part_serializers.BomItemSerializer,
|
||||||
label=_('BOM Item'),
|
serializer_kwargs={
|
||||||
source='bom_item',
|
'label': _('BOM Item'),
|
||||||
many=False,
|
'source': 'bom_item',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
pricing=False,
|
'allow_null': True,
|
||||||
substitutes=False,
|
'pricing': False,
|
||||||
sub_part_detail=False,
|
'substitutes': False,
|
||||||
part_detail=False,
|
'sub_part_detail': False,
|
||||||
can_build=False,
|
'part_detail': False,
|
||||||
),
|
'can_build': False,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['bom_item'],
|
prefetch_fields=['bom_item'],
|
||||||
)
|
)
|
||||||
|
|
||||||
assembly_detail = enable_filter(
|
assembly_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
label=_('Assembly'),
|
serializer_kwargs={
|
||||||
source='bom_item.part',
|
'label': _('Assembly'),
|
||||||
many=False,
|
'source': 'bom_item.part',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
pricing=False,
|
'allow_null': True,
|
||||||
),
|
'pricing': False,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['bom_item__part', 'bom_item__part__pricing_data'],
|
prefetch_fields=['bom_item__part', 'bom_item__part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
label=_('Part'),
|
serializer_kwargs={
|
||||||
source='bom_item.sub_part',
|
'label': _('Part'),
|
||||||
many=False,
|
'source': 'bom_item.sub_part',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
pricing=False,
|
'allow_null': True,
|
||||||
),
|
'pricing': False,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['bom_item__sub_part', 'bom_item__sub_part__pricing_data'],
|
prefetch_fields=['bom_item__sub_part', 'bom_item__sub_part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
category_detail = enable_filter(
|
category_detail = OptionalField(
|
||||||
part_serializers.CategorySerializer(
|
serializer_class=part_serializers.CategorySerializer,
|
||||||
label=_('Category'),
|
serializer_kwargs={
|
||||||
source='bom_item.sub_part.category',
|
'label': _('Category'),
|
||||||
many=False,
|
'source': 'bom_item.sub_part.category',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
False,
|
'path_detail': False,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['bom_item__sub_part__category'],
|
prefetch_fields=['bom_item__sub_part__category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
build_detail = enable_filter(
|
build_detail = OptionalField(
|
||||||
BuildSerializer(
|
serializer_class=BuildSerializer,
|
||||||
label=_('Build'),
|
serializer_kwargs={
|
||||||
source='build',
|
'label': _('Build'),
|
||||||
many=False,
|
'source': 'build',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
part_detail=False,
|
'allow_null': True,
|
||||||
user_detail=False,
|
'part_detail': False,
|
||||||
project_code_detail=False,
|
'user_detail': False,
|
||||||
),
|
'project_code_detail': False,
|
||||||
True,
|
},
|
||||||
|
default_include=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotated (calculated) fields
|
# Annotated (calculated) fields
|
||||||
|
|||||||
@@ -1162,18 +1162,12 @@ class BuildListTest(BuildAPITest):
|
|||||||
data = self.options(self.url, expected_code=200).data
|
data = self.options(self.url, expected_code=200).data
|
||||||
|
|
||||||
self.assertEqual(data['name'], 'Build List')
|
self.assertEqual(data['name'], 'Build List')
|
||||||
actions = data['actions']['POST']
|
actions = data['actions']['GET']
|
||||||
|
|
||||||
for field_name in [
|
for field_name in ['pk', 'title', 'part', 'project_code', 'quantity']:
|
||||||
'pk',
|
# Fields should exist in both GET and POST actions
|
||||||
'title',
|
|
||||||
'part',
|
|
||||||
'part_detail',
|
|
||||||
'project_code',
|
|
||||||
'project_code_detail',
|
|
||||||
'quantity',
|
|
||||||
]:
|
|
||||||
self.assertIn(field_name, actions)
|
self.assertIn(field_name, actions)
|
||||||
|
self.assertIn(field_name, data['actions']['POST'])
|
||||||
|
|
||||||
# Specific checks for certain fields
|
# Specific checks for certain fields
|
||||||
for field_name in ['part', 'project_code', 'take_from']:
|
for field_name in ['part', 'project_code', 'take_from']:
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ from django.db.models import (
|
|||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
from taggit.serializers import TagListSerializerField
|
||||||
|
|
||||||
import InvenTree.conversion
|
import InvenTree.conversion
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
@@ -325,11 +328,15 @@ def enable_project_code_filter(default: bool = True):
|
|||||||
"""
|
"""
|
||||||
from common.serializers import ProjectCodeSerializer
|
from common.serializers import ProjectCodeSerializer
|
||||||
|
|
||||||
return InvenTree.serializers.enable_filter(
|
return InvenTree.serializers.OptionalField(
|
||||||
ProjectCodeSerializer(
|
serializer_class=ProjectCodeSerializer,
|
||||||
source='project_code', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'project_code',
|
||||||
default,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=default,
|
||||||
filter_name='project_code_detail',
|
filter_name='project_code_detail',
|
||||||
prefetch_fields=['project_code'],
|
prefetch_fields=['project_code'],
|
||||||
)
|
)
|
||||||
@@ -344,14 +351,15 @@ def enable_project_label_filter(default: bool = True):
|
|||||||
|
|
||||||
If applied, this field will automatically prefetch the 'project_code' relationship.
|
If applied, this field will automatically prefetch the 'project_code' relationship.
|
||||||
"""
|
"""
|
||||||
return InvenTree.serializers.enable_filter(
|
return InvenTree.serializers.OptionalField(
|
||||||
InvenTree.serializers.FilterableCharField(
|
serializer_class=serializers.CharField,
|
||||||
source='project_code.code',
|
serializer_kwargs={
|
||||||
read_only=True,
|
'source': 'project_code.code',
|
||||||
label=_('Project Code Label'),
|
'read_only': True,
|
||||||
allow_null=True,
|
'label': _('Project Code Label'),
|
||||||
),
|
'allow_null': True,
|
||||||
default,
|
},
|
||||||
|
default_include=default,
|
||||||
filter_name='project_code_detail',
|
filter_name='project_code_detail',
|
||||||
prefetch_fields=['project_code'],
|
prefetch_fields=['project_code'],
|
||||||
)
|
)
|
||||||
@@ -369,9 +377,15 @@ def enable_parameters_filter():
|
|||||||
"""
|
"""
|
||||||
from common.serializers import ParameterSerializer
|
from common.serializers import ParameterSerializer
|
||||||
|
|
||||||
return InvenTree.serializers.enable_filter(
|
return InvenTree.serializers.OptionalField(
|
||||||
ParameterSerializer(many=True, read_only=True, allow_null=True),
|
serializer_class=ParameterSerializer,
|
||||||
False,
|
serializer_kwargs={
|
||||||
|
'many': True,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'required': False,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='parameters',
|
filter_name='parameters',
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'parameters_list',
|
'parameters_list',
|
||||||
@@ -390,11 +404,10 @@ def enable_tags_filter(default: bool = False):
|
|||||||
|
|
||||||
If applied, this field will automatically prefetch the 'tags' relationship.
|
If applied, this field will automatically prefetch the 'tags' relationship.
|
||||||
"""
|
"""
|
||||||
from InvenTree.serializers import FilterableTagListField
|
return InvenTree.serializers.OptionalField(
|
||||||
|
serializer_class=TagListSerializerField,
|
||||||
return InvenTree.serializers.enable_filter(
|
serializer_kwargs={'required': False},
|
||||||
FilterableTagListField(required=False),
|
default_include=default,
|
||||||
default,
|
|
||||||
filter_name='tags',
|
filter_name='tags',
|
||||||
prefetch_fields=['tags', 'tagged_items', 'tagged_items__tag'],
|
prefetch_fields=['tags', 'tagged_items', 'tagged_items__tag'],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from InvenTree.serializers import (
|
|||||||
InvenTreeAttachmentSerializerField,
|
InvenTreeAttachmentSerializerField,
|
||||||
InvenTreeImageSerializerField,
|
InvenTreeImageSerializerField,
|
||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
enable_filter,
|
OptionalField,
|
||||||
)
|
)
|
||||||
from plugin import registry as plugin_registry
|
from plugin import registry as plugin_registry
|
||||||
from users.serializers import OwnerSerializer, UserSerializer
|
from users.serializers import OwnerSerializer, UserSerializer
|
||||||
@@ -857,6 +857,7 @@ class ParameterSerializer(
|
|||||||
'note',
|
'note',
|
||||||
'updated',
|
'updated',
|
||||||
'updated_by',
|
'updated_by',
|
||||||
|
# Optional fields
|
||||||
'template_detail',
|
'template_detail',
|
||||||
'updated_by_detail',
|
'updated_by_detail',
|
||||||
]
|
]
|
||||||
@@ -906,17 +907,22 @@ class ParameterSerializer(
|
|||||||
allow_null=False,
|
allow_null=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_by_detail = enable_filter(
|
updated_by_detail = OptionalField(
|
||||||
UserSerializer(
|
serializer_class=UserSerializer,
|
||||||
source='updated_by', read_only=True, allow_null=True, many=False
|
serializer_kwargs={
|
||||||
),
|
'source': 'updated_by',
|
||||||
True,
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'many': False,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['updated_by'],
|
prefetch_fields=['updated_by'],
|
||||||
)
|
)
|
||||||
|
|
||||||
template_detail = enable_filter(
|
template_detail = OptionalField(
|
||||||
ParameterTemplateSerializer(source='template', read_only=True, many=False),
|
serializer_class=ParameterTemplateSerializer,
|
||||||
True,
|
serializer_kwargs={'source': 'template', 'read_only': True, 'many': False},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['template', 'template__model_type'],
|
prefetch_fields=['template', 'template__model_type'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ class ManufacturerPartMixin(SerializerContextMixin):
|
|||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related('supplier_parts')
|
queryset = queryset.prefetch_related('supplier_parts')
|
||||||
|
queryset = queryset.prefetch_related('part', 'part__pricing_data')
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from importer.registry import register_importer
|
|||||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||||
from InvenTree.ready import isGeneratingSchema
|
from InvenTree.ready import isGeneratingSchema
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import (
|
||||||
FilterableCharField,
|
|
||||||
FilterableSerializerMixin,
|
FilterableSerializerMixin,
|
||||||
InvenTreeCurrencySerializer,
|
InvenTreeCurrencySerializer,
|
||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
@@ -26,8 +25,8 @@ from InvenTree.serializers import (
|
|||||||
InvenTreeMoneySerializer,
|
InvenTreeMoneySerializer,
|
||||||
InvenTreeTagModelSerializer,
|
InvenTreeTagModelSerializer,
|
||||||
NotesFieldMixin,
|
NotesFieldMixin,
|
||||||
|
OptionalField,
|
||||||
RemoteImageMixin,
|
RemoteImageMixin,
|
||||||
enable_filter,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
@@ -164,9 +163,10 @@ class CompanySerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
primary_address = enable_filter(
|
primary_address = OptionalField(
|
||||||
AddressBriefSerializer(read_only=True, allow_null=True),
|
serializer_class=AddressBriefSerializer,
|
||||||
False,
|
serializer_kwargs={'read_only': True, 'many': False, 'allow_null': True},
|
||||||
|
default_include=False,
|
||||||
filter_name='address_detail',
|
filter_name='address_detail',
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
Prefetch(
|
Prefetch(
|
||||||
@@ -263,27 +263,37 @@ class ManufacturerPartSerializer(
|
|||||||
|
|
||||||
parameters = common.filters.enable_parameters_filter()
|
parameters = common.filters.enable_parameters_filter()
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
source='part', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'part',
|
||||||
True,
|
'many': False,
|
||||||
prefetch_fields=['part', 'part__pricing_data', 'part__category'],
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
|
prefetch_fields=['part__category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
pretty_name = enable_filter(
|
pretty_name = OptionalField(
|
||||||
FilterableCharField(read_only=True, allow_null=True), filter_name='pretty'
|
serializer_class=serializers.CharField,
|
||||||
|
serializer_kwargs={'read_only': True, 'allow_null': True},
|
||||||
|
filter_name='pretty',
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer = serializers.PrimaryKeyRelatedField(
|
manufacturer = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=Company.objects.filter(is_manufacturer=True)
|
queryset=Company.objects.filter(is_manufacturer=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer_detail = enable_filter(
|
manufacturer_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
source='manufacturer', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'manufacturer',
|
||||||
True,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['manufacturer'],
|
prefetch_fields=['manufacturer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -415,70 +425,86 @@ class SupplierPartSerializer(
|
|||||||
|
|
||||||
pack_quantity_native = serializers.FloatField(read_only=True)
|
pack_quantity_native = serializers.FloatField(read_only=True)
|
||||||
|
|
||||||
price_breaks = enable_filter(
|
price_breaks = OptionalField(
|
||||||
SupplierPriceBreakBriefSerializer(
|
serializer_class=SupplierPriceBreakBriefSerializer,
|
||||||
source='pricebreaks',
|
serializer_kwargs={
|
||||||
many=True,
|
'source': 'pricebreaks',
|
||||||
read_only=True,
|
'many': True,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
label=_('Price Breaks'),
|
'allow_null': True,
|
||||||
),
|
'label': _('Price Breaks'),
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='price_breaks',
|
filter_name='price_breaks',
|
||||||
prefetch_fields=['pricebreaks'],
|
prefetch_fields=['pricebreaks'],
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = common.filters.enable_parameters_filter()
|
parameters = common.filters.enable_parameters_filter()
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'label': _('Part'),
|
||||||
False,
|
'source': 'part',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['part', 'part__pricing_data'],
|
prefetch_fields=['part', 'part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_detail = enable_filter(
|
supplier_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
label=_('Supplier'),
|
serializer_kwargs={
|
||||||
source='supplier',
|
'label': _('Supplier'),
|
||||||
many=False,
|
'source': 'supplier',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['supplier'],
|
prefetch_fields=['supplier'],
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer_detail = enable_filter(
|
manufacturer_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
label=_('Manufacturer'),
|
serializer_kwargs={
|
||||||
source='manufacturer_part.manufacturer',
|
'label': _('Manufacturer'),
|
||||||
many=False,
|
'source': 'manufacturer_part.manufacturer',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['manufacturer_part__manufacturer'],
|
prefetch_fields=['manufacturer_part__manufacturer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
pretty_name = enable_filter(
|
pretty_name = OptionalField(
|
||||||
FilterableCharField(read_only=True, allow_null=True), filter_name='pretty'
|
serializer_class=serializers.CharField,
|
||||||
|
serializer_kwargs={
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'label': _('Pretty Name'),
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
|
filter_name='pretty',
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier = serializers.PrimaryKeyRelatedField(
|
supplier = serializers.PrimaryKeyRelatedField(
|
||||||
label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
|
label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer_part_detail = enable_filter(
|
manufacturer_part_detail = OptionalField(
|
||||||
ManufacturerPartSerializer(
|
serializer_class=ManufacturerPartSerializer,
|
||||||
label=_('Manufacturer Part'),
|
serializer_kwargs={
|
||||||
source='manufacturer_part',
|
'label': _('Manufacturer Part'),
|
||||||
part_detail=False,
|
'source': 'manufacturer_part',
|
||||||
read_only=True,
|
'part_detail': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['manufacturer_part'],
|
prefetch_fields=['manufacturer_part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -568,18 +594,27 @@ class SupplierPriceBreakSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
supplier_detail = enable_filter(
|
supplier_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
source='part.supplier', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'part.supplier',
|
||||||
False,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['part__supplier'],
|
prefetch_fields=['part__supplier'],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
SupplierPartSerializer(
|
serializer_class=SupplierPartSerializer,
|
||||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'part',
|
||||||
False,
|
'brief': True,
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['part', 'part__part', 'part__part__pricing_data'],
|
prefetch_fields=['part', 'part__part', 'part__part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -507,6 +507,8 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
|||||||
"""Tests for the ManufacturerPart detail endpoint."""
|
"""Tests for the ManufacturerPart detail endpoint."""
|
||||||
mp = ManufacturerPart.objects.first()
|
mp = ManufacturerPart.objects.first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(mp)
|
||||||
|
|
||||||
url = reverse('api-manufacturer-part-detail', kwargs={'pk': mp.pk})
|
url = reverse('api-manufacturer-part-detail', kwargs={'pk': mp.pk})
|
||||||
|
|
||||||
response = self.get(url)
|
response = self.get(url)
|
||||||
|
|||||||
@@ -394,7 +394,14 @@ class DataImportSession(models.Model):
|
|||||||
|
|
||||||
if serializer_class := self.serializer_class:
|
if serializer_class := self.serializer_class:
|
||||||
serializer = serializer_class(data={}, importing=True)
|
serializer = serializer_class(data={}, importing=True)
|
||||||
fields.update(metadata.get_serializer_info(serializer))
|
serializer_fields = metadata.get_serializer_info(serializer)
|
||||||
|
|
||||||
|
for field_name, field in serializer_fields.items():
|
||||||
|
# Skip read-only fields
|
||||||
|
if field.get('read_only', False):
|
||||||
|
continue
|
||||||
|
|
||||||
|
fields[field_name] = field
|
||||||
|
|
||||||
# Cache the available fields against this instance
|
# Cache the available fields against this instance
|
||||||
self._available_fields = fields
|
self._available_fields = fields
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from InvenTree.serializers import (
|
|||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
InvenTreeMoneySerializer,
|
InvenTreeMoneySerializer,
|
||||||
NotesFieldMixin,
|
NotesFieldMixin,
|
||||||
enable_filter,
|
OptionalField,
|
||||||
)
|
)
|
||||||
from order.status_codes import (
|
from order.status_codes import (
|
||||||
PurchaseOrderStatusGroups,
|
PurchaseOrderStatusGroups,
|
||||||
@@ -126,20 +126,28 @@ class AbstractOrderSerializer(
|
|||||||
reference = serializers.CharField(required=True)
|
reference = serializers.CharField(required=True)
|
||||||
|
|
||||||
# Detail for point-of-contact field
|
# Detail for point-of-contact field
|
||||||
contact_detail = enable_filter(
|
contact_detail = OptionalField(
|
||||||
ContactSerializer(
|
serializer_class=ContactSerializer,
|
||||||
source='contact', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'contact',
|
||||||
True,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['contact'],
|
prefetch_fields=['contact'],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Detail for responsible field
|
# Detail for responsible field
|
||||||
responsible_detail = enable_filter(
|
responsible_detail = OptionalField(
|
||||||
OwnerSerializer(
|
serializer_class=OwnerSerializer,
|
||||||
source='responsible', read_only=True, allow_null=True, many=False
|
serializer_kwargs={
|
||||||
),
|
'source': 'responsible',
|
||||||
True,
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'many': False,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['responsible'],
|
prefetch_fields=['responsible'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,11 +155,15 @@ class AbstractOrderSerializer(
|
|||||||
project_code_detail = common.filters.enable_project_code_filter()
|
project_code_detail = common.filters.enable_project_code_filter()
|
||||||
|
|
||||||
# Detail for address field
|
# Detail for address field
|
||||||
address_detail = enable_filter(
|
address_detail = OptionalField(
|
||||||
AddressBriefSerializer(
|
serializer_class=AddressBriefSerializer,
|
||||||
source='address', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'address',
|
||||||
True,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['address'],
|
prefetch_fields=['address'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -432,10 +444,14 @@ class PurchaseOrderSerializer(
|
|||||||
source='supplier.name', read_only=True, label=_('Supplier Name')
|
source='supplier.name', read_only=True, label=_('Supplier Name')
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_detail = enable_filter(
|
supplier_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
source='supplier', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'supplier',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['supplier'],
|
prefetch_fields=['supplier'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -629,19 +645,28 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
|
|
||||||
total_price = serializers.FloatField(read_only=True)
|
total_price = serializers.FloatField(read_only=True)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
PartBriefSerializer(
|
serializer_class=PartBriefSerializer,
|
||||||
source='get_base_part', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'get_base_part',
|
||||||
False,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='part_detail',
|
filter_name='part_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_part_detail = enable_filter(
|
supplier_part_detail = OptionalField(
|
||||||
SupplierPartSerializer(
|
serializer_class=SupplierPartSerializer,
|
||||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'part',
|
||||||
False,
|
'brief': True,
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='part_detail',
|
filter_name='part_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -655,11 +680,14 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
destination_detail = enable_filter(
|
destination_detail = OptionalField(
|
||||||
stock.serializers.LocationBriefSerializer(
|
serializer_class=stock.serializers.LocationBriefSerializer,
|
||||||
source='get_destination', read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'get_destination',
|
||||||
True,
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['destination', 'order__destination'],
|
prefetch_fields=['destination', 'order__destination'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -667,17 +695,26 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
help_text=_('Purchase price currency')
|
help_text=_('Purchase price currency')
|
||||||
)
|
)
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
PurchaseOrderSerializer(
|
serializer_class=PurchaseOrderSerializer,
|
||||||
source='order', read_only=True, allow_null=True, many=False
|
serializer_kwargs={
|
||||||
)
|
'source': 'order',
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'many': False,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
build_order_detail = enable_filter(
|
build_order_detail = OptionalField(
|
||||||
build.serializers.BuildSerializer(
|
serializer_class=build.serializers.BuildSerializer,
|
||||||
source='build_order', read_only=True, allow_null=True, many=False
|
serializer_kwargs={
|
||||||
),
|
'source': 'build_order',
|
||||||
True,
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'many': False,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'build_order__responsible',
|
'build_order__responsible',
|
||||||
'build_order__issued_by',
|
'build_order__issued_by',
|
||||||
@@ -763,10 +800,14 @@ class PurchaseOrderExtraLineSerializer(
|
|||||||
model = order.models.PurchaseOrderExtraLine
|
model = order.models.PurchaseOrderExtraLine
|
||||||
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
PurchaseOrderSerializer(
|
serializer_class=PurchaseOrderSerializer,
|
||||||
source='order', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
)
|
'source': 'order',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1098,10 +1139,14 @@ class SalesOrderSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
customer_detail = enable_filter(
|
customer_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
source='customer', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'customer',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['customer'],
|
prefetch_fields=['customer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1252,10 +1297,14 @@ class SalesOrderLineItemSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
SalesOrderSerializer(
|
serializer_class=SalesOrderSerializer,
|
||||||
source='order', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'order',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'order__created_by',
|
'order__created_by',
|
||||||
'order__responsible',
|
'order__responsible',
|
||||||
@@ -1265,15 +1314,25 @@ class SalesOrderLineItemSerializer(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
PartBriefSerializer(source='part', many=False, read_only=True, allow_null=True),
|
serializer_class=PartBriefSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'part',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['part__pricing_data'],
|
prefetch_fields=['part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
customer_detail = enable_filter(
|
customer_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
source='order.customer', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'order.customer',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['order__customer'],
|
prefetch_fields=['order__customer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1343,19 +1402,27 @@ class SalesOrderShipmentSerializer(
|
|||||||
read_only=True, allow_null=True, label=_('Allocated Items')
|
read_only=True, allow_null=True, label=_('Allocated Items')
|
||||||
)
|
)
|
||||||
|
|
||||||
checked_by_detail = enable_filter(
|
checked_by_detail = OptionalField(
|
||||||
UserSerializer(
|
serializer_class=UserSerializer,
|
||||||
source='checked_by', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'checked_by',
|
||||||
True,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['checked_by'],
|
prefetch_fields=['checked_by'],
|
||||||
)
|
)
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
SalesOrderSerializer(
|
serializer_class=SalesOrderSerializer,
|
||||||
source='order', read_only=True, allow_null=True, many=False
|
serializer_kwargs={
|
||||||
),
|
'source': 'order',
|
||||||
True,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'order',
|
'order',
|
||||||
'order__customer',
|
'order__customer',
|
||||||
@@ -1365,19 +1432,27 @@ class SalesOrderShipmentSerializer(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
customer_detail = enable_filter(
|
customer_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
source='order.customer', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'order.customer',
|
||||||
False,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['order__customer'],
|
prefetch_fields=['order__customer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
shipment_address_detail = enable_filter(
|
shipment_address_detail = OptionalField(
|
||||||
AddressBriefSerializer(
|
serializer_class=AddressBriefSerializer,
|
||||||
source='shipment_address', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'shipment_address',
|
||||||
True,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['shipment_address'],
|
prefetch_fields=['shipment_address'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1428,46 +1503,71 @@ class SalesOrderAllocationSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Extra detail fields
|
# Extra detail fields
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
SalesOrderSerializer(
|
serializer_class=SalesOrderSerializer,
|
||||||
source='line.order', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
)
|
'source': 'line.order',
|
||||||
)
|
'many': False,
|
||||||
part_detail = enable_filter(
|
'read_only': True,
|
||||||
PartBriefSerializer(
|
'allow_null': True,
|
||||||
source='item.part', many=False, read_only=True, allow_null=True
|
},
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
item_detail = enable_filter(
|
|
||||||
stock.serializers.StockItemSerializer(
|
|
||||||
source='item',
|
|
||||||
many=False,
|
|
||||||
read_only=True,
|
|
||||||
allow_null=True,
|
|
||||||
part_detail=False,
|
|
||||||
location_detail=False,
|
|
||||||
supplier_part_detail=False,
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
location_detail = enable_filter(
|
|
||||||
stock.serializers.LocationBriefSerializer(
|
|
||||||
source='item.location', many=False, read_only=True, allow_null=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
customer_detail = enable_filter(
|
|
||||||
CompanyBriefSerializer(
|
|
||||||
source='line.order.customer', many=False, read_only=True, allow_null=True
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
shipment_detail = SalesOrderShipmentSerializer(
|
part_detail = OptionalField(
|
||||||
source='shipment',
|
serializer_class=PartBriefSerializer,
|
||||||
order_detail=False,
|
serializer_kwargs={
|
||||||
many=False,
|
'source': 'item.part',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
item_detail = OptionalField(
|
||||||
|
serializer_class=stock.serializers.StockItemSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'item',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'part_detail': False,
|
||||||
|
'location_detail': False,
|
||||||
|
'supplier_part_detail': False,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
|
)
|
||||||
|
location_detail = OptionalField(
|
||||||
|
serializer_class=stock.serializers.LocationBriefSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'item.location',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
customer_detail = OptionalField(
|
||||||
|
serializer_class=CompanyBriefSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'line.order.customer',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
shipment_detail = OptionalField(
|
||||||
|
serializer_class=SalesOrderShipmentSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'shipment',
|
||||||
|
'order_detail': False,
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
|
prefetch_fields=['shipment'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1881,10 +1981,14 @@ class SalesOrderExtraLineSerializer(
|
|||||||
model = order.models.SalesOrderExtraLine
|
model = order.models.SalesOrderExtraLine
|
||||||
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
SalesOrderSerializer(
|
serializer_class=SalesOrderSerializer,
|
||||||
source='order', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
)
|
'source': 'order',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1942,10 +2046,14 @@ class ReturnOrderSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
customer_detail = enable_filter(
|
customer_detail = OptionalField(
|
||||||
CompanyBriefSerializer(
|
serializer_class=CompanyBriefSerializer,
|
||||||
source='customer', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'customer',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['customer'],
|
prefetch_fields=['customer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2103,10 +2211,14 @@ class ReturnOrderLineItemSerializer(
|
|||||||
'part_detail',
|
'part_detail',
|
||||||
])
|
])
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
ReturnOrderSerializer(
|
serializer_class=ReturnOrderSerializer,
|
||||||
source='order', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'order',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'order__created_by',
|
'order__created_by',
|
||||||
'order__responsible',
|
'order__responsible',
|
||||||
@@ -2120,17 +2232,25 @@ class ReturnOrderLineItemSerializer(
|
|||||||
label=_('Quantity'), help_text=_('Quantity to return')
|
label=_('Quantity'), help_text=_('Quantity to return')
|
||||||
)
|
)
|
||||||
|
|
||||||
item_detail = enable_filter(
|
item_detail = OptionalField(
|
||||||
stock.serializers.StockItemSerializer(
|
serializer_class=stock.serializers.StockItemSerializer,
|
||||||
source='item', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'item',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['item__supplier_part'],
|
prefetch_fields=['item__supplier_part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
PartBriefSerializer(
|
serializer_class=PartBriefSerializer,
|
||||||
source='item.part', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
)
|
'source': 'item.part',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
price = InvenTreeMoneySerializer(allow_null=True)
|
price = InvenTreeMoneySerializer(allow_null=True)
|
||||||
@@ -2149,8 +2269,12 @@ class ReturnOrderExtraLineSerializer(
|
|||||||
model = order.models.ReturnOrderExtraLine
|
model = order.models.ReturnOrderExtraLine
|
||||||
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
||||||
|
|
||||||
order_detail = enable_filter(
|
order_detail = OptionalField(
|
||||||
ReturnOrderSerializer(
|
serializer_class=ReturnOrderSerializer,
|
||||||
source='order', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
)
|
'source': 'order',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,13 +35,7 @@ from data_exporter.mixins import DataExportSerializerMixin
|
|||||||
from importer.registry import register_importer
|
from importer.registry import register_importer
|
||||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||||
from InvenTree.ready import isGeneratingSchema
|
from InvenTree.ready import isGeneratingSchema
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import OptionalField, TreePathSerializer
|
||||||
FilterableDateTimeField,
|
|
||||||
FilterableFloatField,
|
|
||||||
FilterableListField,
|
|
||||||
FilterableListSerializer,
|
|
||||||
enable_filter,
|
|
||||||
)
|
|
||||||
from users.serializers import UserSerializer
|
from users.serializers import UserSerializer
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
@@ -141,13 +135,15 @@ class CategorySerializer(
|
|||||||
|
|
||||||
return category.pk in self.starred_categories
|
return category.pk in self.starred_categories
|
||||||
|
|
||||||
path = enable_filter(
|
path = OptionalField(
|
||||||
FilterableListField(
|
serializer_class=TreePathSerializer,
|
||||||
child=serializers.DictField(),
|
serializer_kwargs={
|
||||||
source='get_path',
|
'source': 'get_path',
|
||||||
read_only=True,
|
'many': True,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='path_detail',
|
filter_name='path_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -354,18 +350,25 @@ class PartBriefSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Pricing fields
|
# Pricing fields
|
||||||
pricing_min = enable_filter(
|
pricing_min = OptionalField(
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'pricing_data.overall_min',
|
||||||
True,
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
pricing_max = enable_filter(
|
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
pricing_max = OptionalField(
|
||||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
),
|
serializer_kwargs={
|
||||||
True,
|
'source': 'pricing_data.overall_max',
|
||||||
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -774,28 +777,37 @@ class PartSerializer(
|
|||||||
return part.pk in self.starred_parts
|
return part.pk in self.starred_parts
|
||||||
|
|
||||||
# Extra detail for the category
|
# Extra detail for the category
|
||||||
category_detail = enable_filter(
|
category_detail = OptionalField(
|
||||||
CategorySerializer(
|
serializer_class=CategorySerializer,
|
||||||
source='category', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'category',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['category'],
|
prefetch_fields=['category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
category_path = enable_filter(
|
category_path = OptionalField(
|
||||||
FilterableListField(
|
serializer_class=TreePathSerializer,
|
||||||
child=serializers.DictField(),
|
serializer_kwargs={
|
||||||
source='category.get_path',
|
'source': 'category.get_path',
|
||||||
read_only=True,
|
'many': True,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
|
},
|
||||||
filter_name='path_detail',
|
filter_name='path_detail',
|
||||||
prefetch_fields=['category'],
|
prefetch_fields=['category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
default_location_detail = enable_filter(
|
default_location_detail = OptionalField(
|
||||||
DefaultLocationSerializer(
|
serializer_class=DefaultLocationSerializer,
|
||||||
source='default_location', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'default_location',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
filter_name='location_detail',
|
filter_name='location_detail',
|
||||||
prefetch_fields=['default_location'],
|
prefetch_fields=['default_location'],
|
||||||
)
|
)
|
||||||
@@ -901,25 +913,36 @@ class PartSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Pricing fields
|
# Pricing fields
|
||||||
pricing_min = enable_filter(
|
pricing_min = OptionalField(
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'pricing_data.overall_min',
|
||||||
True,
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
pricing_max = enable_filter(
|
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
pricing_max = OptionalField(
|
||||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
),
|
serializer_kwargs={
|
||||||
True,
|
'source': 'pricing_data.overall_max',
|
||||||
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
pricing_updated = enable_filter(
|
|
||||||
FilterableDateTimeField(
|
pricing_updated = OptionalField(
|
||||||
source='pricing_data.updated', allow_null=True, read_only=True
|
serializer_class=serializers.DateTimeField,
|
||||||
),
|
serializer_kwargs={
|
||||||
True,
|
'source': 'pricing_data.updated',
|
||||||
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -927,11 +950,15 @@ class PartSerializer(
|
|||||||
|
|
||||||
tags = common.filters.enable_tags_filter()
|
tags = common.filters.enable_tags_filter()
|
||||||
|
|
||||||
price_breaks = enable_filter(
|
price_breaks = OptionalField(
|
||||||
PartSalePriceSerializer(
|
serializer_class=PartSalePriceSerializer,
|
||||||
source='salepricebreaks', many=True, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'salepricebreaks',
|
||||||
False,
|
'many': True,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='price_breaks',
|
filter_name='price_breaks',
|
||||||
prefetch_fields=['salepricebreaks'],
|
prefetch_fields=['salepricebreaks'],
|
||||||
)
|
)
|
||||||
@@ -1267,10 +1294,15 @@ class PartStocktakeSerializer(
|
|||||||
label=_('Part Description'),
|
label=_('Part Description'),
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
PartBriefSerializer(
|
serializer_class=PartBriefSerializer,
|
||||||
source='part', read_only=True, allow_null=True, many=False, pricing=False
|
serializer_kwargs={
|
||||||
),
|
'source': 'part',
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'many': False,
|
||||||
|
'pricing': False,
|
||||||
|
},
|
||||||
default_include=False,
|
default_include=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1595,7 +1627,7 @@ class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer
|
|||||||
|
|
||||||
model = BomItemSubstitute
|
model = BomItemSubstitute
|
||||||
fields = ['pk', 'bom_item', 'part', 'part_detail']
|
fields = ['pk', 'bom_item', 'part', 'part_detail']
|
||||||
list_serializer_class = FilterableListSerializer
|
# list_serializer_class = FilterableListSerializer
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(
|
part_detail = PartBriefSerializer(
|
||||||
source='part', read_only=True, many=False, pricing=False
|
source='part', read_only=True, many=False, pricing=False
|
||||||
@@ -1697,9 +1729,15 @@ class BomItemSerializer(
|
|||||||
help_text=_('Select the parent assembly'),
|
help_text=_('Select the parent assembly'),
|
||||||
)
|
)
|
||||||
|
|
||||||
substitutes = enable_filter(
|
substitutes = OptionalField(
|
||||||
BomItemSubstituteSerializer(many=True, read_only=True, allow_null=True),
|
serializer_class=BomItemSubstituteSerializer,
|
||||||
False,
|
serializer_kwargs={
|
||||||
|
'many': True,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
'required': False,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='substitutes',
|
filter_name='substitutes',
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'substitutes',
|
'substitutes',
|
||||||
@@ -1709,14 +1747,15 @@ class BomItemSerializer(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
PartBriefSerializer(
|
serializer_class=PartBriefSerializer,
|
||||||
source='part',
|
serializer_kwargs={
|
||||||
label=_('Assembly'),
|
'source': 'part',
|
||||||
many=False,
|
'label': _('Assembly'),
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
)
|
'allow_null': True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
sub_part = serializers.PrimaryKeyRelatedField(
|
sub_part = serializers.PrimaryKeyRelatedField(
|
||||||
@@ -1725,26 +1764,28 @@ class BomItemSerializer(
|
|||||||
help_text=_('Select the component part'),
|
help_text=_('Select the component part'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sub_part_detail = enable_filter(
|
sub_part_detail = OptionalField(
|
||||||
PartBriefSerializer(
|
serializer_class=PartBriefSerializer,
|
||||||
source='sub_part',
|
serializer_kwargs={
|
||||||
label=_('Component'),
|
'source': 'sub_part',
|
||||||
many=False,
|
'label': _('Component'),
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
True,
|
},
|
||||||
|
default_include=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
category_detail = enable_filter(
|
category_detail = OptionalField(
|
||||||
CategorySerializer(
|
serializer_class=CategorySerializer,
|
||||||
source='sub_part.category',
|
serializer_kwargs={
|
||||||
label=_('Category'),
|
'source': 'sub_part.category',
|
||||||
many=False,
|
'label': _('Category'),
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
on_order = serializers.FloatField(
|
on_order = serializers.FloatField(
|
||||||
@@ -1755,41 +1796,61 @@ class BomItemSerializer(
|
|||||||
label=_('In Production'), read_only=True, allow_null=True
|
label=_('In Production'), read_only=True, allow_null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
can_build = enable_filter(
|
can_build = OptionalField(
|
||||||
FilterableFloatField(label=_('Can Build'), read_only=True, allow_null=True),
|
serializer_class=serializers.FloatField,
|
||||||
True,
|
serializer_kwargs={
|
||||||
|
'label': _('Can Build'),
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cached pricing fields
|
# Cached pricing fields
|
||||||
pricing_min = enable_filter(
|
pricing_min = OptionalField(
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'sub_part.pricing_data.overall_min',
|
||||||
True,
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
pricing_max = enable_filter(
|
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
pricing_max = OptionalField(
|
||||||
source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
),
|
serializer_kwargs={
|
||||||
True,
|
'source': 'sub_part.pricing_data.overall_max',
|
||||||
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
pricing_min_total = enable_filter(
|
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
|
pricing_min_total = OptionalField(
|
||||||
True,
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
|
serializer_kwargs={'allow_null': True, 'read_only': True},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
pricing_max_total = enable_filter(
|
|
||||||
InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
|
pricing_max_total = OptionalField(
|
||||||
True,
|
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||||
|
serializer_kwargs={'allow_null': True, 'read_only': True},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
pricing_updated = enable_filter(
|
|
||||||
FilterableDateTimeField(
|
pricing_updated = OptionalField(
|
||||||
source='sub_part.pricing_data.updated', allow_null=True, read_only=True
|
serializer_class=serializers.DateTimeField,
|
||||||
),
|
serializer_kwargs={
|
||||||
True,
|
'source': 'sub_part.pricing_data.updated',
|
||||||
|
'allow_null': True,
|
||||||
|
'read_only': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1887,19 +1948,22 @@ class CategoryParameterTemplateSerializer(
|
|||||||
'default_value',
|
'default_value',
|
||||||
]
|
]
|
||||||
|
|
||||||
template_detail = enable_filter(
|
template_detail = OptionalField(
|
||||||
common.serializers.ParameterTemplateSerializer(
|
serializer_class=common.serializers.ParameterTemplateSerializer,
|
||||||
source='template', many=False, read_only=True
|
serializer_kwargs={'source': 'template', 'many': False, 'read_only': True},
|
||||||
),
|
default_include=True,
|
||||||
True,
|
|
||||||
prefetch_fields=['template'],
|
prefetch_fields=['template'],
|
||||||
)
|
)
|
||||||
|
|
||||||
category_detail = enable_filter(
|
category_detail = OptionalField(
|
||||||
CategorySerializer(
|
serializer_class=CategorySerializer,
|
||||||
source='category', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'category',
|
||||||
True,
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
prefetch_fields=['category'],
|
prefetch_fields=['category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1406,6 +1406,53 @@ class PartAPITest(PartAPITestBase):
|
|||||||
assert_subset=True,
|
assert_subset=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_pricing_info(self):
|
||||||
|
"""Test annotation of 'pricing' detail against a Part instance."""
|
||||||
|
part = Part.objects.first()
|
||||||
|
url = reverse('api-part-detail', kwargs={'pk': part.pk})
|
||||||
|
|
||||||
|
pricing_fields = ['pricing_min', 'pricing_max', 'pricing_updated']
|
||||||
|
|
||||||
|
for included in [True, False]:
|
||||||
|
response = self.get(url, {'pricing': included}, expected_code=200)
|
||||||
|
|
||||||
|
for field in pricing_fields:
|
||||||
|
if included:
|
||||||
|
self.assertIn(field, response.data)
|
||||||
|
else:
|
||||||
|
self.assertNotIn(field, response.data)
|
||||||
|
|
||||||
|
def test_parameters_info(self):
|
||||||
|
"""Test annotation of 'parameters' detail against a Part instance."""
|
||||||
|
part = Part.objects.first()
|
||||||
|
url = reverse('api-part-detail', kwargs={'pk': part.pk})
|
||||||
|
|
||||||
|
for included in [True, False]:
|
||||||
|
response = self.get(url, {'parameters': included}, expected_code=200)
|
||||||
|
|
||||||
|
if included:
|
||||||
|
self.assertIn('parameters', response.data)
|
||||||
|
else:
|
||||||
|
self.assertNotIn('parameters', response.data)
|
||||||
|
|
||||||
|
def test_category_detail(self):
|
||||||
|
"""Test annotation of 'category_detail' against a Part instance."""
|
||||||
|
part = Part.objects.get(pk=1)
|
||||||
|
url = reverse('api-part-detail', kwargs={'pk': part.pk})
|
||||||
|
|
||||||
|
for included in [True, False]:
|
||||||
|
response = self.get(url, {'category_detail': included}, expected_code=200)
|
||||||
|
|
||||||
|
if not included:
|
||||||
|
self.assertNotIn('category_detail', response.data)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.assertIn('category_detail', response.data)
|
||||||
|
category = response.data['category_detail']
|
||||||
|
|
||||||
|
for field in ['name', 'description', 'structural']:
|
||||||
|
self.assertIn(field, category)
|
||||||
|
|
||||||
|
|
||||||
class PartCreationTests(PartAPITestBase):
|
class PartCreationTests(PartAPITestBase):
|
||||||
"""Tests for creating new Part instances via the API."""
|
"""Tests for creating new Part instances via the API."""
|
||||||
@@ -2707,8 +2754,28 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_get_bom_detail(self):
|
def test_get_bom_detail(self):
|
||||||
"""Get the detail view for a single BomItem object."""
|
"""Get the detail view for a single BomItem object."""
|
||||||
url = reverse('api-bom-item-detail', kwargs={'pk': 3})
|
from part.models import BomItemSubstitute
|
||||||
|
|
||||||
|
bom_item = BomItem.objects.get(pk=3)
|
||||||
|
|
||||||
|
# Create some substitutes for this BomItem
|
||||||
|
substitute_parts = Part.objects.filter(component=True).exclude(
|
||||||
|
pk=bom_item.sub_part.pk
|
||||||
|
)[:3]
|
||||||
|
|
||||||
|
for part in substitute_parts:
|
||||||
|
BomItemSubstitute.objects.create(bom_item=bom_item, part=part)
|
||||||
|
|
||||||
|
self.assertEqual(bom_item.substitutes.count(), 3)
|
||||||
|
|
||||||
|
url = reverse('api-bom-item-detail', kwargs={'pk': bom_item.pk})
|
||||||
|
|
||||||
|
# First, get without substitutes
|
||||||
|
response = self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
self.assertNotIn('substitutes', response.data)
|
||||||
|
|
||||||
|
# Now, get with substitutes
|
||||||
response = self.get(url, {'substitutes': True}, expected_code=200)
|
response = self.get(url, {'substitutes': True}, expected_code=200)
|
||||||
|
|
||||||
expected_values = [
|
expected_values = [
|
||||||
@@ -2735,6 +2802,15 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(int(float(response.data['quantity'])), 25)
|
self.assertEqual(int(float(response.data['quantity'])), 25)
|
||||||
|
|
||||||
|
# Look at the substitutes data
|
||||||
|
subs = response.data['substitutes']
|
||||||
|
|
||||||
|
self.assertEqual(len(subs), 3)
|
||||||
|
|
||||||
|
for sub in subs:
|
||||||
|
for field in ['pk', 'part', 'bom_item', 'part_detail']:
|
||||||
|
self.assertIn(field, sub)
|
||||||
|
|
||||||
# Increase the quantity
|
# Increase the quantity
|
||||||
data = response.data
|
data = response.data
|
||||||
data['quantity'] = 57
|
data['quantity'] = 57
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
|||||||
from importer.registry import register_importer
|
from importer.registry import register_importer
|
||||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import (
|
||||||
FilterableListField,
|
|
||||||
InvenTreeCurrencySerializer,
|
InvenTreeCurrencySerializer,
|
||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
enable_filter,
|
OptionalField,
|
||||||
|
TreePathSerializer,
|
||||||
)
|
)
|
||||||
from users.serializers import UserSerializer
|
from users.serializers import UserSerializer
|
||||||
|
|
||||||
@@ -231,8 +231,9 @@ class StockItemTestResultSerializer(
|
|||||||
self.fields['user'].read_only = True
|
self.fields['user'].read_only = True
|
||||||
self.fields['date'].read_only = True
|
self.fields['date'].read_only = True
|
||||||
|
|
||||||
user_detail = enable_filter(
|
user_detail = OptionalField(
|
||||||
UserSerializer(source='user', read_only=True, allow_null=True),
|
serializer_class=UserSerializer,
|
||||||
|
serializer_kwargs={'source': 'user', 'read_only': True, 'allow_null': True},
|
||||||
prefetch_fields=['user'],
|
prefetch_fields=['user'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -245,10 +246,9 @@ class StockItemTestResultSerializer(
|
|||||||
label=_('Test template for this result'),
|
label=_('Test template for this result'),
|
||||||
)
|
)
|
||||||
|
|
||||||
template_detail = enable_filter(
|
template_detail = OptionalField(
|
||||||
part_serializers.PartTestTemplateSerializer(
|
serializer_class=part_serializers.PartTestTemplateSerializer,
|
||||||
source='template', read_only=True, allow_null=True
|
serializer_kwargs={'source': 'template', 'read_only': True, 'allow_null': True},
|
||||||
),
|
|
||||||
prefetch_fields=['template'],
|
prefetch_fields=['template'],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -377,20 +377,20 @@ class StockItemSerializer(
|
|||||||
'purchase_price_currency',
|
'purchase_price_currency',
|
||||||
'use_pack_size',
|
'use_pack_size',
|
||||||
'serial_numbers',
|
'serial_numbers',
|
||||||
'tests',
|
|
||||||
# Annotated fields
|
# Annotated fields
|
||||||
'allocated',
|
'allocated',
|
||||||
'expired',
|
'expired',
|
||||||
'installed_items',
|
'installed_items',
|
||||||
'child_items',
|
'child_items',
|
||||||
'location_path',
|
|
||||||
'stale',
|
'stale',
|
||||||
'tracking_items',
|
# Optional fields (FK relationships)
|
||||||
'tags',
|
|
||||||
# Detail fields (FK relationships)
|
|
||||||
'supplier_part_detail',
|
|
||||||
'part_detail',
|
|
||||||
'location_detail',
|
'location_detail',
|
||||||
|
'location_path',
|
||||||
|
'part_detail',
|
||||||
|
'supplier_part_detail',
|
||||||
|
'tags',
|
||||||
|
'tests',
|
||||||
|
'tracking_items',
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'allocated',
|
'allocated',
|
||||||
@@ -428,13 +428,16 @@ class StockItemSerializer(
|
|||||||
help_text=_('Parent stock item'),
|
help_text=_('Parent stock item'),
|
||||||
)
|
)
|
||||||
|
|
||||||
location_path = enable_filter(
|
location_path = OptionalField(
|
||||||
FilterableListField(
|
serializer_class=TreePathSerializer,
|
||||||
child=serializers.DictField(),
|
serializer_kwargs={
|
||||||
source='location.get_path',
|
'source': 'location.get_path',
|
||||||
read_only=True,
|
'extra_fields': ['icon'],
|
||||||
allow_null=True,
|
'many': True,
|
||||||
),
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='path_detail',
|
filter_name='path_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -577,19 +580,20 @@ class StockItemSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Optional detail fields, which can be appended via query parameters
|
# Optional detail fields, which can be appended via query parameters
|
||||||
supplier_part_detail = enable_filter(
|
supplier_part_detail = OptionalField(
|
||||||
company_serializers.SupplierPartSerializer(
|
serializer_class=company_serializers.SupplierPartSerializer,
|
||||||
label=_('Supplier Part'),
|
serializer_kwargs={
|
||||||
source='supplier_part',
|
'label': _('Supplier Part'),
|
||||||
brief=True,
|
'source': 'supplier_part',
|
||||||
supplier_detail=False,
|
'brief': True,
|
||||||
manufacturer_detail=False,
|
'supplier_detail': False,
|
||||||
part_detail=False,
|
'manufacturer_detail': False,
|
||||||
many=False,
|
'part_detail': False,
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'supplier_part__supplier',
|
'supplier_part__supplier',
|
||||||
'supplier_part__purchase_order_line_items',
|
'supplier_part__purchase_order_line_items',
|
||||||
@@ -597,30 +601,40 @@ class StockItemSerializer(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'label': _('Part'),
|
||||||
True,
|
'source': 'part',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
location_detail = enable_filter(
|
location_detail = OptionalField(
|
||||||
LocationBriefSerializer(
|
serializer_class=LocationBriefSerializer,
|
||||||
label=_('Location'),
|
serializer_kwargs={
|
||||||
source='location',
|
'label': _('Location'),
|
||||||
many=False,
|
'source': 'location',
|
||||||
read_only=True,
|
'many': False,
|
||||||
allow_null=True,
|
'read_only': True,
|
||||||
),
|
'allow_null': True,
|
||||||
False,
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=['location'],
|
prefetch_fields=['location'],
|
||||||
)
|
)
|
||||||
|
|
||||||
tests = enable_filter(
|
tests = OptionalField(
|
||||||
StockItemTestResultSerializer(
|
serializer_class=StockItemTestResultSerializer,
|
||||||
source='test_results', many=True, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'test_results',
|
||||||
False,
|
'many': True,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
prefetch_fields=[
|
prefetch_fields=[
|
||||||
'test_results',
|
'test_results',
|
||||||
'test_results__user',
|
'test_results__user',
|
||||||
@@ -1222,13 +1236,16 @@ class LocationSerializer(
|
|||||||
|
|
||||||
tags = common.filters.enable_tags_filter()
|
tags = common.filters.enable_tags_filter()
|
||||||
|
|
||||||
path = enable_filter(
|
path = OptionalField(
|
||||||
FilterableListField(
|
serializer_class=TreePathSerializer,
|
||||||
child=serializers.DictField(),
|
serializer_kwargs={
|
||||||
source='get_path',
|
'many': True,
|
||||||
read_only=True,
|
'source': 'get_path',
|
||||||
allow_null=True,
|
'extra_fields': ['icon'],
|
||||||
),
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
|
default_include=False,
|
||||||
filter_name='path_detail',
|
filter_name='path_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1273,21 +1290,37 @@ class StockTrackingSerializer(
|
|||||||
|
|
||||||
label = serializers.CharField(read_only=True)
|
label = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
item_detail = enable_filter(
|
item_detail = OptionalField(
|
||||||
StockItemSerializer(source='item', many=False, read_only=True, allow_null=True),
|
serializer_class=StockItemSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'item',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['item', 'item__part'],
|
prefetch_fields=['item', 'item__part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = OptionalField(
|
||||||
part_serializers.PartBriefSerializer(
|
serializer_class=part_serializers.PartBriefSerializer,
|
||||||
source='part', many=False, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'part',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
default_include=False,
|
default_include=False,
|
||||||
prefetch_fields=['part'],
|
prefetch_fields=['part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
user_detail = enable_filter(
|
user_detail = OptionalField(
|
||||||
UserSerializer(source='user', many=False, read_only=True, allow_null=True),
|
serializer_class=UserSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'user',
|
||||||
|
'many': False,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
prefetch_fields=['user'],
|
prefetch_fields=['user'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ class UserList(ListCreateAPI):
|
|||||||
- Otherwise authenticated users have read-only access
|
- Otherwise authenticated users have read-only access
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all().prefetch_related('groups')
|
||||||
serializer_class = UserCreateSerializer
|
serializer_class = UserCreateSerializer
|
||||||
|
|
||||||
# User must have the right role, AND be a staff user, else read-only
|
# User must have the right role, AND be a staff user, else read-only
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ from rest_framework import serializers
|
|||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import (
|
||||||
FilterableListSerializer,
|
|
||||||
FilterableSerializerMethodField,
|
|
||||||
FilterableSerializerMixin,
|
FilterableSerializerMixin,
|
||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
enable_filter,
|
OptionalField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import ApiToken, Owner, RuleSet, UserProfile
|
from .models import ApiToken, Owner, RuleSet, UserProfile
|
||||||
@@ -56,7 +54,6 @@ class RuleSetSerializer(InvenTreeModelSerializer):
|
|||||||
'can_delete',
|
'can_delete',
|
||||||
]
|
]
|
||||||
read_only_fields = ['pk', 'name', 'label', 'group']
|
read_only_fields = ['pk', 'name', 'label', 'group']
|
||||||
list_serializer_class = FilterableListSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class RoleSerializer(InvenTreeModelSerializer):
|
class RoleSerializer(InvenTreeModelSerializer):
|
||||||
@@ -185,7 +182,6 @@ class UserSerializer(InvenTreeModelSerializer):
|
|||||||
model = User
|
model = User
|
||||||
fields = ['pk', 'username', 'first_name', 'last_name', 'email']
|
fields = ['pk', 'username', 'first_name', 'last_name', 'email']
|
||||||
read_only_fields = ['username', 'email']
|
read_only_fields = ['username', 'email']
|
||||||
list_serializer_class = FilterableListSerializer
|
|
||||||
|
|
||||||
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
||||||
|
|
||||||
@@ -267,8 +263,9 @@ class GroupSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
|||||||
model = Group
|
model = Group
|
||||||
fields = ['pk', 'name', 'permissions', 'roles', 'users']
|
fields = ['pk', 'name', 'permissions', 'roles', 'users']
|
||||||
|
|
||||||
permissions = enable_filter(
|
permissions = OptionalField(
|
||||||
FilterableSerializerMethodField(allow_null=True, read_only=True),
|
serializer_class=serializers.SerializerMethodField,
|
||||||
|
serializer_kwargs={'allow_null': True, 'read_only': True},
|
||||||
filter_name='permission_detail',
|
filter_name='permission_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -276,16 +273,26 @@ class GroupSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
|||||||
"""Return a list of permissions associated with the group."""
|
"""Return a list of permissions associated with the group."""
|
||||||
return generate_permission_dict(group.permissions.all())
|
return generate_permission_dict(group.permissions.all())
|
||||||
|
|
||||||
roles = enable_filter(
|
roles = OptionalField(
|
||||||
RuleSetSerializer(
|
serializer_class=RuleSetSerializer,
|
||||||
source='rule_sets', many=True, read_only=True, allow_null=True
|
serializer_kwargs={
|
||||||
),
|
'source': 'rule_sets',
|
||||||
|
'many': True,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
filter_name='role_detail',
|
filter_name='role_detail',
|
||||||
prefetch_fields=['rule_sets'],
|
prefetch_fields=['rule_sets'],
|
||||||
)
|
)
|
||||||
|
|
||||||
users = enable_filter(
|
users = OptionalField(
|
||||||
UserSerializer(source='user_set', many=True, read_only=True, allow_null=True),
|
serializer_class=UserSerializer,
|
||||||
|
serializer_kwargs={
|
||||||
|
'source': 'user_set',
|
||||||
|
'many': True,
|
||||||
|
'read_only': True,
|
||||||
|
'allow_null': True,
|
||||||
|
},
|
||||||
filter_name='user_detail',
|
filter_name='user_detail',
|
||||||
prefetch_fields=['user_set'],
|
prefetch_fields=['user_set'],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -218,8 +218,11 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
expected_code=200,
|
expected_code=200,
|
||||||
)
|
)
|
||||||
self.assertIn('name', response.data)
|
self.assertIn('name', response.data)
|
||||||
|
self.assertIn('roles', response.data)
|
||||||
self.assertIn('permissions', response.data)
|
self.assertIn('permissions', response.data)
|
||||||
|
|
||||||
|
self.assertGreater(len(response.data['roles']), 0)
|
||||||
|
|
||||||
def test_login_redirect(self):
|
def test_login_redirect(self):
|
||||||
"""Test login redirect endpoint."""
|
"""Test login redirect endpoint."""
|
||||||
response = self.get(reverse('api-login-redirect'), expected_code=302)
|
response = self.get(reverse('api-login-redirect'), expected_code=302)
|
||||||
|
|||||||
Reference in New Issue
Block a user