mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-17 16:58:42 +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
|
||||
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."""
|
||||
|
||||
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
|
||||
- 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
|
||||
"""
|
||||
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
|
||||
if self.label_lookup[field] == 'dependent field':
|
||||
field.get_child(raise_exception=True)
|
||||
|
||||
@@ -6,7 +6,6 @@ from rest_framework import generics, mixins, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
import data_exporter.mixins
|
||||
import data_exporter.serializers
|
||||
import importer.mixins
|
||||
from InvenTree.fields import InvenTreeNotesField, OutputConfiguration
|
||||
from InvenTree.helpers import (
|
||||
@@ -214,20 +213,6 @@ class OutputOptionsMixin:
|
||||
if getattr(cls, 'output_options', None) is not None:
|
||||
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):
|
||||
"""Return serializer instance with output options applied."""
|
||||
request = getattr(self, 'request', None)
|
||||
@@ -241,20 +226,7 @@ class OutputOptionsMixin:
|
||||
context['request'] = request
|
||||
kwargs['context'] = context
|
||||
|
||||
serializer = 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
|
||||
return super().get_serializer(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset with output options applied.
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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.mixins import ListModelMixin
|
||||
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 taggit.serializers import TaggitSerializer, TagListSerializerField
|
||||
from taggit.serializers import TaggitSerializer
|
||||
|
||||
import common.models as common_models
|
||||
import InvenTree.ready
|
||||
@@ -33,94 +34,239 @@ from InvenTree.helpers import str2bool
|
||||
from InvenTree.helpers_model import getModelsWithMixin
|
||||
|
||||
|
||||
# region path filtering
|
||||
class FilterableSerializerField:
|
||||
"""Mixin to mark serializer as filterable.
|
||||
@dataclass
|
||||
class OptionalField:
|
||||
"""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
|
||||
is_filterable_vals = {}
|
||||
Adding OptionalField instances to a serializer class is more "efficient"
|
||||
than directly adding the field (and later removing it),
|
||||
as the field is never instantiated unless it is required.
|
||||
|
||||
# Options for automatic queryset prefetching
|
||||
prefetch_fields: Optional[list[str]] = None
|
||||
Additionally, you can specify prefetch fields which will be applied
|
||||
to the queryset, *only* if the field is included in the final serializer.
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""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)
|
||||
This allows for optimization of database queries based only on the requested data.
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def enable_filter(
|
||||
func: Any,
|
||||
default_include: bool = False,
|
||||
filter_name: Optional[str] = None,
|
||||
filter_by_query: bool = True,
|
||||
prefetch_fields: Optional[list[str]] = None,
|
||||
):
|
||||
"""Decorator for marking a serializer field as filterable.
|
||||
|
||||
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!'
|
||||
Example:
|
||||
class MySerializer(FilterableSerializerMixin, serializers.ModelSerializer):
|
||||
my_optional_field = OptionalField(
|
||||
serializer_class=serializers.CharField,
|
||||
default_include=False,
|
||||
filter_name='include_my_field',
|
||||
serializer_kwargs={
|
||||
'help_text': 'This is an optional field',
|
||||
'read_only': True,
|
||||
},
|
||||
prefetch_fields=['related_field'],
|
||||
)
|
||||
|
||||
# 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
|
||||
func._kwargs['prefetch_fields'] = prefetch_fields
|
||||
|
||||
return func
|
||||
serializer_class: Serializer
|
||||
serializer_kwargs: Optional[dict] = None
|
||||
default_include: bool = False
|
||||
filter_name: Optional[str] = None
|
||||
filter_by_query: bool = True
|
||||
prefetch_fields: Optional[list[str]] = None
|
||||
|
||||
|
||||
class FilterableSerializerMixin:
|
||||
"""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.
|
||||
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
|
||||
no_filters = False
|
||||
"""If True, do not raise an exception if no filterable fields are found."""
|
||||
filter_on_query = True
|
||||
"""If True, also look for filter parameters in the request query parameters."""
|
||||
optional_filters: dict = None
|
||||
fields_to_remove: set = None
|
||||
optional_fields: set = None
|
||||
filter_on_query: bool = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer. This gathers and applies filters through kwargs."""
|
||||
# add list_serializer_class to meta if not present - reduces duplication
|
||||
if not isinstance(self, FilterableListSerializer) and (
|
||||
not hasattr(self.Meta, 'list_serializer_class')
|
||||
):
|
||||
self.Meta.list_serializer_class = FilterableListSerializer
|
||||
# Extract some useful context information for later use
|
||||
context = kwargs.get('context', {})
|
||||
self.request = context.get('request', None) or getattr(self, 'request', None)
|
||||
self.request_query_params = (
|
||||
dict(getattr(self.request, 'query_params', {})) if self.request else {}
|
||||
)
|
||||
|
||||
self.gather_optional_fields(kwargs)
|
||||
|
||||
self.gather_filters(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:
|
||||
"""Apply any prefetching to the queryset based on the optionally included fields.
|
||||
@@ -140,157 +286,48 @@ class FilterableSerializerMixin:
|
||||
if getattr(request, '_metadata_requested', False):
|
||||
return queryset
|
||||
|
||||
# Gather up the set of simple 'prefetch' fields and functions
|
||||
prefetch_fields = set()
|
||||
|
||||
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))
|
||||
if self.prefetch_list and len(self.prefetch_list) > 0:
|
||||
queryset = queryset.prefetch_related(*list(self.prefetch_list))
|
||||
|
||||
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):
|
||||
"""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.
|
||||
|
||||
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):
|
||||
@@ -477,7 +514,7 @@ class DependentField(serializers.Field):
|
||||
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."""
|
||||
|
||||
# Switch out URLField mapping
|
||||
|
||||
@@ -8,6 +8,7 @@ from rest_framework.serializers import SerializerMethodField
|
||||
|
||||
import InvenTree.serializers
|
||||
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin
|
||||
from InvenTree.serializers import OptionalField
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from InvenTree.urls import backendpatterns
|
||||
|
||||
@@ -25,21 +26,25 @@ class SampleSerializer(
|
||||
fields = ['field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'id']
|
||||
|
||||
field_a = SerializerMethodField(method_name='sample')
|
||||
field_b = InvenTree.serializers.enable_filter(
|
||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample')
|
||||
field_b = OptionalField(
|
||||
serializer_class=SerializerMethodField,
|
||||
serializer_kwargs={'method_name': 'sample'},
|
||||
)
|
||||
field_c = InvenTree.serializers.enable_filter(
|
||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
|
||||
True,
|
||||
field_c = OptionalField(
|
||||
serializer_class=SerializerMethodField,
|
||||
serializer_kwargs={'method_name': 'sample'},
|
||||
default_include=True,
|
||||
filter_name='crazy_name',
|
||||
)
|
||||
field_d = InvenTree.serializers.enable_filter(
|
||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
|
||||
True,
|
||||
field_d = OptionalField(
|
||||
serializer_class=SerializerMethodField,
|
||||
serializer_kwargs={'method_name': 'sample'},
|
||||
default_include=True,
|
||||
filter_name='crazy_name',
|
||||
)
|
||||
field_e = InvenTree.serializers.enable_filter(
|
||||
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
|
||||
field_e = OptionalField(
|
||||
serializer_class=SerializerMethodField,
|
||||
serializer_kwargs={'method_name': 'sample'},
|
||||
filter_name='field_e',
|
||||
filter_by_query=False,
|
||||
)
|
||||
@@ -106,110 +111,3 @@ class FilteredSerializers(InvenTreeAPITestCase):
|
||||
self.assertContains(response, 'field_c')
|
||||
self.assertContains(response, 'field_d')
|
||||
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,
|
||||
InvenTreeModelSerializer,
|
||||
NotesFieldMixin,
|
||||
enable_filter,
|
||||
OptionalField,
|
||||
)
|
||||
from stock.generators import generate_batch_code
|
||||
from stock.models import StockItem, StockLocation
|
||||
@@ -116,9 +116,10 @@ class BuildSerializer(
|
||||
|
||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(source='part', many=False, read_only=True),
|
||||
True,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={'source': 'part', 'many': False, 'read_only': True},
|
||||
default_include=True,
|
||||
prefetch_fields=['part', 'part__category', 'part__pricing_data'],
|
||||
)
|
||||
|
||||
@@ -132,16 +133,22 @@ class BuildSerializer(
|
||||
|
||||
overdue = serializers.BooleanField(read_only=True, default=False)
|
||||
|
||||
issued_by_detail = enable_filter(
|
||||
UserSerializer(source='issued_by', read_only=True),
|
||||
True,
|
||||
issued_by_detail = OptionalField(
|
||||
serializer_class=UserSerializer,
|
||||
serializer_kwargs={'source': 'issued_by', 'read_only': True},
|
||||
default_include=True,
|
||||
filter_name='user_detail',
|
||||
prefetch_fields=['issued_by'],
|
||||
)
|
||||
|
||||
responsible_detail = enable_filter(
|
||||
OwnerSerializer(source='responsible', read_only=True, allow_null=True),
|
||||
True,
|
||||
responsible_detail = OptionalField(
|
||||
serializer_class=OwnerSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'responsible',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='user_detail',
|
||||
prefetch_fields=['responsible'],
|
||||
)
|
||||
@@ -1200,31 +1207,33 @@ class BuildItemSerializer(
|
||||
)
|
||||
|
||||
# Extra (optional) detail fields
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Part'),
|
||||
source='stock_item.part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
),
|
||||
True,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Part'),
|
||||
'source': 'stock_item.part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'pricing': False,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['stock_item__part'],
|
||||
)
|
||||
|
||||
stock_item_detail = enable_filter(
|
||||
StockItemSerializer(
|
||||
source='stock_item',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
label=_('Stock Item'),
|
||||
part_detail=False,
|
||||
location_detail=False,
|
||||
supplier_part_detail=False,
|
||||
path_detail=False,
|
||||
),
|
||||
True,
|
||||
stock_item_detail = OptionalField(
|
||||
serializer_class=StockItemSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'stock_item',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'label': _('Stock Item'),
|
||||
'part_detail': False,
|
||||
'location_detail': False,
|
||||
'supplier_part_detail': False,
|
||||
'path_detail': False,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='stock_detail',
|
||||
prefetch_fields=[
|
||||
'stock_item',
|
||||
@@ -1234,18 +1243,19 @@ class BuildItemSerializer(
|
||||
],
|
||||
)
|
||||
|
||||
install_into_detail = enable_filter(
|
||||
StockItemSerializer(
|
||||
source='install_into',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
label=_('Install Into'),
|
||||
part_detail=False,
|
||||
location_detail=False,
|
||||
supplier_part_detail=False,
|
||||
path_detail=False,
|
||||
),
|
||||
False,
|
||||
install_into_detail = OptionalField(
|
||||
serializer_class=StockItemSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'install_into',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'label': _('Install Into'),
|
||||
'part_detail': False,
|
||||
'location_detail': False,
|
||||
'supplier_part_detail': False,
|
||||
'path_detail': False,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['install_into', 'install_into__part'],
|
||||
)
|
||||
|
||||
@@ -1253,26 +1263,28 @@ class BuildItemSerializer(
|
||||
label=_('Location'), source='stock_item.location', many=False, read_only=True
|
||||
)
|
||||
|
||||
location_detail = enable_filter(
|
||||
LocationBriefSerializer(
|
||||
label=_('Location'),
|
||||
source='stock_item.location',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
location_detail = OptionalField(
|
||||
serializer_class=LocationBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Location'),
|
||||
'source': 'stock_item.location',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['stock_item__location'],
|
||||
)
|
||||
|
||||
build_detail = enable_filter(
|
||||
BuildSerializer(
|
||||
label=_('Build'),
|
||||
source='build_line.build',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
build_detail = OptionalField(
|
||||
serializer_class=BuildSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Build'),
|
||||
'source': 'build_line.build',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=[
|
||||
'build_line__build',
|
||||
'build_line__build__part',
|
||||
@@ -1283,16 +1295,17 @@ class BuildItemSerializer(
|
||||
],
|
||||
)
|
||||
|
||||
supplier_part_detail = enable_filter(
|
||||
company.serializers.SupplierPartSerializer(
|
||||
label=_('Supplier Part'),
|
||||
source='stock_item.supplier_part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
brief=True,
|
||||
),
|
||||
False,
|
||||
supplier_part_detail = OptionalField(
|
||||
serializer_class=company.serializers.SupplierPartSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Supplier Part'),
|
||||
'source': 'stock_item.supplier_part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'brief': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=[
|
||||
'stock_item__supplier_part',
|
||||
'stock_item__supplier_part__supplier',
|
||||
@@ -1382,11 +1395,15 @@ class BuildLineSerializer(
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
allocations = enable_filter(
|
||||
BuildItemSerializer(
|
||||
many=True, read_only=True, allow_null=True, build_detail=False
|
||||
),
|
||||
True,
|
||||
allocations = OptionalField(
|
||||
serializer_class=BuildItemSerializer,
|
||||
serializer_kwargs={
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'build_detail': False,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=[
|
||||
'allocations',
|
||||
'allocations__stock_item',
|
||||
@@ -1427,73 +1444,79 @@ class BuildLineSerializer(
|
||||
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
|
||||
|
||||
# Foreign key fields
|
||||
bom_item_detail = enable_filter(
|
||||
part_serializers.BomItemSerializer(
|
||||
label=_('BOM Item'),
|
||||
source='bom_item',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
substitutes=False,
|
||||
sub_part_detail=False,
|
||||
part_detail=False,
|
||||
can_build=False,
|
||||
),
|
||||
False,
|
||||
bom_item_detail = OptionalField(
|
||||
serializer_class=part_serializers.BomItemSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('BOM Item'),
|
||||
'source': 'bom_item',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'pricing': False,
|
||||
'substitutes': False,
|
||||
'sub_part_detail': False,
|
||||
'part_detail': False,
|
||||
'can_build': False,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['bom_item'],
|
||||
)
|
||||
|
||||
assembly_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Assembly'),
|
||||
source='bom_item.part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
),
|
||||
False,
|
||||
assembly_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Assembly'),
|
||||
'source': 'bom_item.part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'pricing': False,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['bom_item__part', 'bom_item__part__pricing_data'],
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Part'),
|
||||
source='bom_item.sub_part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
),
|
||||
False,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Part'),
|
||||
'source': 'bom_item.sub_part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'pricing': False,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['bom_item__sub_part', 'bom_item__sub_part__pricing_data'],
|
||||
)
|
||||
|
||||
category_detail = enable_filter(
|
||||
part_serializers.CategorySerializer(
|
||||
label=_('Category'),
|
||||
source='bom_item.sub_part.category',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
False,
|
||||
category_detail = OptionalField(
|
||||
serializer_class=part_serializers.CategorySerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Category'),
|
||||
'source': 'bom_item.sub_part.category',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'path_detail': False,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['bom_item__sub_part__category'],
|
||||
)
|
||||
|
||||
build_detail = enable_filter(
|
||||
BuildSerializer(
|
||||
label=_('Build'),
|
||||
source='build',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
part_detail=False,
|
||||
user_detail=False,
|
||||
project_code_detail=False,
|
||||
),
|
||||
True,
|
||||
build_detail = OptionalField(
|
||||
serializer_class=BuildSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Build'),
|
||||
'source': 'build',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'part_detail': False,
|
||||
'user_detail': False,
|
||||
'project_code_detail': False,
|
||||
},
|
||||
default_include=True,
|
||||
)
|
||||
|
||||
# Annotated (calculated) fields
|
||||
|
||||
@@ -1162,18 +1162,12 @@ class BuildListTest(BuildAPITest):
|
||||
data = self.options(self.url, expected_code=200).data
|
||||
|
||||
self.assertEqual(data['name'], 'Build List')
|
||||
actions = data['actions']['POST']
|
||||
actions = data['actions']['GET']
|
||||
|
||||
for field_name in [
|
||||
'pk',
|
||||
'title',
|
||||
'part',
|
||||
'part_detail',
|
||||
'project_code',
|
||||
'project_code_detail',
|
||||
'quantity',
|
||||
]:
|
||||
for field_name in ['pk', 'title', 'part', 'project_code', 'quantity']:
|
||||
# Fields should exist in both GET and POST actions
|
||||
self.assertIn(field_name, actions)
|
||||
self.assertIn(field_name, data['actions']['POST'])
|
||||
|
||||
# Specific checks for certain fields
|
||||
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.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from taggit.serializers import TagListSerializerField
|
||||
|
||||
import InvenTree.conversion
|
||||
import InvenTree.helpers
|
||||
import InvenTree.serializers
|
||||
@@ -325,11 +328,15 @@ def enable_project_code_filter(default: bool = True):
|
||||
"""
|
||||
from common.serializers import ProjectCodeSerializer
|
||||
|
||||
return InvenTree.serializers.enable_filter(
|
||||
ProjectCodeSerializer(
|
||||
source='project_code', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
default,
|
||||
return InvenTree.serializers.OptionalField(
|
||||
serializer_class=ProjectCodeSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'project_code',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=default,
|
||||
filter_name='project_code_detail',
|
||||
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.
|
||||
"""
|
||||
return InvenTree.serializers.enable_filter(
|
||||
InvenTree.serializers.FilterableCharField(
|
||||
source='project_code.code',
|
||||
read_only=True,
|
||||
label=_('Project Code Label'),
|
||||
allow_null=True,
|
||||
),
|
||||
default,
|
||||
return InvenTree.serializers.OptionalField(
|
||||
serializer_class=serializers.CharField,
|
||||
serializer_kwargs={
|
||||
'source': 'project_code.code',
|
||||
'read_only': True,
|
||||
'label': _('Project Code Label'),
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=default,
|
||||
filter_name='project_code_detail',
|
||||
prefetch_fields=['project_code'],
|
||||
)
|
||||
@@ -369,9 +377,15 @@ def enable_parameters_filter():
|
||||
"""
|
||||
from common.serializers import ParameterSerializer
|
||||
|
||||
return InvenTree.serializers.enable_filter(
|
||||
ParameterSerializer(many=True, read_only=True, allow_null=True),
|
||||
False,
|
||||
return InvenTree.serializers.OptionalField(
|
||||
serializer_class=ParameterSerializer,
|
||||
serializer_kwargs={
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'required': False,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='parameters',
|
||||
prefetch_fields=[
|
||||
'parameters_list',
|
||||
@@ -390,11 +404,10 @@ def enable_tags_filter(default: bool = False):
|
||||
|
||||
If applied, this field will automatically prefetch the 'tags' relationship.
|
||||
"""
|
||||
from InvenTree.serializers import FilterableTagListField
|
||||
|
||||
return InvenTree.serializers.enable_filter(
|
||||
FilterableTagListField(required=False),
|
||||
default,
|
||||
return InvenTree.serializers.OptionalField(
|
||||
serializer_class=TagListSerializerField,
|
||||
serializer_kwargs={'required': False},
|
||||
default_include=default,
|
||||
filter_name='tags',
|
||||
prefetch_fields=['tags', 'tagged_items', 'tagged_items__tag'],
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ from InvenTree.serializers import (
|
||||
InvenTreeAttachmentSerializerField,
|
||||
InvenTreeImageSerializerField,
|
||||
InvenTreeModelSerializer,
|
||||
enable_filter,
|
||||
OptionalField,
|
||||
)
|
||||
from plugin import registry as plugin_registry
|
||||
from users.serializers import OwnerSerializer, UserSerializer
|
||||
@@ -857,6 +857,7 @@ class ParameterSerializer(
|
||||
'note',
|
||||
'updated',
|
||||
'updated_by',
|
||||
# Optional fields
|
||||
'template_detail',
|
||||
'updated_by_detail',
|
||||
]
|
||||
@@ -906,17 +907,22 @@ class ParameterSerializer(
|
||||
allow_null=False,
|
||||
)
|
||||
|
||||
updated_by_detail = enable_filter(
|
||||
UserSerializer(
|
||||
source='updated_by', read_only=True, allow_null=True, many=False
|
||||
),
|
||||
True,
|
||||
updated_by_detail = OptionalField(
|
||||
serializer_class=UserSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'updated_by',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'many': False,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['updated_by'],
|
||||
)
|
||||
|
||||
template_detail = enable_filter(
|
||||
ParameterTemplateSerializer(source='template', read_only=True, many=False),
|
||||
True,
|
||||
template_detail = OptionalField(
|
||||
serializer_class=ParameterTemplateSerializer,
|
||||
serializer_kwargs={'source': 'template', 'read_only': True, 'many': False},
|
||||
default_include=True,
|
||||
prefetch_fields=['template', 'template__model_type'],
|
||||
)
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ class ManufacturerPartMixin(SerializerContextMixin):
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related('supplier_parts')
|
||||
queryset = queryset.prefetch_related('part', 'part__pricing_data')
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from importer.registry import register_importer
|
||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import (
|
||||
FilterableCharField,
|
||||
FilterableSerializerMixin,
|
||||
InvenTreeCurrencySerializer,
|
||||
InvenTreeDecimalField,
|
||||
@@ -26,8 +25,8 @@ from InvenTree.serializers import (
|
||||
InvenTreeMoneySerializer,
|
||||
InvenTreeTagModelSerializer,
|
||||
NotesFieldMixin,
|
||||
OptionalField,
|
||||
RemoteImageMixin,
|
||||
enable_filter,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
@@ -164,9 +163,10 @@ class CompanySerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
primary_address = enable_filter(
|
||||
AddressBriefSerializer(read_only=True, allow_null=True),
|
||||
False,
|
||||
primary_address = OptionalField(
|
||||
serializer_class=AddressBriefSerializer,
|
||||
serializer_kwargs={'read_only': True, 'many': False, 'allow_null': True},
|
||||
default_include=False,
|
||||
filter_name='address_detail',
|
||||
prefetch_fields=[
|
||||
Prefetch(
|
||||
@@ -263,27 +263,37 @@ class ManufacturerPartSerializer(
|
||||
|
||||
parameters = common.filters.enable_parameters_filter()
|
||||
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
source='part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
prefetch_fields=['part', 'part__pricing_data', 'part__category'],
|
||||
part_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['part__category'],
|
||||
)
|
||||
|
||||
pretty_name = enable_filter(
|
||||
FilterableCharField(read_only=True, allow_null=True), filter_name='pretty'
|
||||
pretty_name = OptionalField(
|
||||
serializer_class=serializers.CharField,
|
||||
serializer_kwargs={'read_only': True, 'allow_null': True},
|
||||
filter_name='pretty',
|
||||
)
|
||||
|
||||
manufacturer = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Company.objects.filter(is_manufacturer=True)
|
||||
)
|
||||
|
||||
manufacturer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='manufacturer', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
manufacturer_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'manufacturer',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['manufacturer'],
|
||||
)
|
||||
|
||||
@@ -415,70 +425,86 @@ class SupplierPartSerializer(
|
||||
|
||||
pack_quantity_native = serializers.FloatField(read_only=True)
|
||||
|
||||
price_breaks = enable_filter(
|
||||
SupplierPriceBreakBriefSerializer(
|
||||
source='pricebreaks',
|
||||
many=True,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
label=_('Price Breaks'),
|
||||
),
|
||||
False,
|
||||
price_breaks = OptionalField(
|
||||
serializer_class=SupplierPriceBreakBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'pricebreaks',
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'label': _('Price Breaks'),
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='price_breaks',
|
||||
prefetch_fields=['pricebreaks'],
|
||||
)
|
||||
|
||||
parameters = common.filters.enable_parameters_filter()
|
||||
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Part'),
|
||||
'source': 'part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['part', 'part__pricing_data'],
|
||||
)
|
||||
|
||||
supplier_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
label=_('Supplier'),
|
||||
source='supplier',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
False,
|
||||
supplier_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Supplier'),
|
||||
'source': 'supplier',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['supplier'],
|
||||
)
|
||||
|
||||
manufacturer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
label=_('Manufacturer'),
|
||||
source='manufacturer_part.manufacturer',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
False,
|
||||
manufacturer_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Manufacturer'),
|
||||
'source': 'manufacturer_part.manufacturer',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['manufacturer_part__manufacturer'],
|
||||
)
|
||||
|
||||
pretty_name = enable_filter(
|
||||
FilterableCharField(read_only=True, allow_null=True), filter_name='pretty'
|
||||
pretty_name = OptionalField(
|
||||
serializer_class=serializers.CharField,
|
||||
serializer_kwargs={
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'label': _('Pretty Name'),
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='pretty',
|
||||
)
|
||||
|
||||
supplier = serializers.PrimaryKeyRelatedField(
|
||||
label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
|
||||
)
|
||||
|
||||
manufacturer_part_detail = enable_filter(
|
||||
ManufacturerPartSerializer(
|
||||
label=_('Manufacturer Part'),
|
||||
source='manufacturer_part',
|
||||
part_detail=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
False,
|
||||
manufacturer_part_detail = OptionalField(
|
||||
serializer_class=ManufacturerPartSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Manufacturer Part'),
|
||||
'source': 'manufacturer_part',
|
||||
'part_detail': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['manufacturer_part'],
|
||||
)
|
||||
|
||||
@@ -568,18 +594,27 @@ class SupplierPriceBreakSerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
supplier_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='part.supplier', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
supplier_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part.supplier',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['part__supplier'],
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
SupplierPartSerializer(
|
||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=SupplierPartSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part',
|
||||
'brief': True,
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['part', 'part__part', 'part__part__pricing_data'],
|
||||
)
|
||||
|
||||
@@ -507,6 +507,8 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
"""Tests for the ManufacturerPart detail endpoint."""
|
||||
mp = ManufacturerPart.objects.first()
|
||||
|
||||
self.assertIsNotNone(mp)
|
||||
|
||||
url = reverse('api-manufacturer-part-detail', kwargs={'pk': mp.pk})
|
||||
|
||||
response = self.get(url)
|
||||
|
||||
@@ -394,7 +394,14 @@ class DataImportSession(models.Model):
|
||||
|
||||
if serializer_class := self.serializer_class:
|
||||
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
|
||||
self._available_fields = fields
|
||||
|
||||
@@ -36,7 +36,7 @@ from InvenTree.serializers import (
|
||||
InvenTreeModelSerializer,
|
||||
InvenTreeMoneySerializer,
|
||||
NotesFieldMixin,
|
||||
enable_filter,
|
||||
OptionalField,
|
||||
)
|
||||
from order.status_codes import (
|
||||
PurchaseOrderStatusGroups,
|
||||
@@ -126,20 +126,28 @@ class AbstractOrderSerializer(
|
||||
reference = serializers.CharField(required=True)
|
||||
|
||||
# Detail for point-of-contact field
|
||||
contact_detail = enable_filter(
|
||||
ContactSerializer(
|
||||
source='contact', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
contact_detail = OptionalField(
|
||||
serializer_class=ContactSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'contact',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['contact'],
|
||||
)
|
||||
|
||||
# Detail for responsible field
|
||||
responsible_detail = enable_filter(
|
||||
OwnerSerializer(
|
||||
source='responsible', read_only=True, allow_null=True, many=False
|
||||
),
|
||||
True,
|
||||
responsible_detail = OptionalField(
|
||||
serializer_class=OwnerSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'responsible',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'many': False,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['responsible'],
|
||||
)
|
||||
|
||||
@@ -147,11 +155,15 @@ class AbstractOrderSerializer(
|
||||
project_code_detail = common.filters.enable_project_code_filter()
|
||||
|
||||
# Detail for address field
|
||||
address_detail = enable_filter(
|
||||
AddressBriefSerializer(
|
||||
source='address', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
address_detail = OptionalField(
|
||||
serializer_class=AddressBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'address',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['address'],
|
||||
)
|
||||
|
||||
@@ -432,10 +444,14 @@ class PurchaseOrderSerializer(
|
||||
source='supplier.name', read_only=True, label=_('Supplier Name')
|
||||
)
|
||||
|
||||
supplier_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='supplier', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
supplier_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'supplier',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['supplier'],
|
||||
)
|
||||
|
||||
@@ -629,19 +645,28 @@ class PurchaseOrderLineItemSerializer(
|
||||
|
||||
total_price = serializers.FloatField(read_only=True)
|
||||
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='get_base_part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'get_base_part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='part_detail',
|
||||
)
|
||||
|
||||
supplier_part_detail = enable_filter(
|
||||
SupplierPartSerializer(
|
||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
supplier_part_detail = OptionalField(
|
||||
serializer_class=SupplierPartSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part',
|
||||
'brief': True,
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='part_detail',
|
||||
)
|
||||
|
||||
@@ -655,11 +680,14 @@ class PurchaseOrderLineItemSerializer(
|
||||
default=False,
|
||||
)
|
||||
|
||||
destination_detail = enable_filter(
|
||||
stock.serializers.LocationBriefSerializer(
|
||||
source='get_destination', read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
destination_detail = OptionalField(
|
||||
serializer_class=stock.serializers.LocationBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'get_destination',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['destination', 'order__destination'],
|
||||
)
|
||||
|
||||
@@ -667,17 +695,26 @@ class PurchaseOrderLineItemSerializer(
|
||||
help_text=_('Purchase price currency')
|
||||
)
|
||||
|
||||
order_detail = enable_filter(
|
||||
PurchaseOrderSerializer(
|
||||
source='order', read_only=True, allow_null=True, many=False
|
||||
)
|
||||
order_detail = OptionalField(
|
||||
serializer_class=PurchaseOrderSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'many': False,
|
||||
},
|
||||
default_include=True,
|
||||
)
|
||||
|
||||
build_order_detail = enable_filter(
|
||||
build.serializers.BuildSerializer(
|
||||
source='build_order', read_only=True, allow_null=True, many=False
|
||||
),
|
||||
True,
|
||||
build_order_detail = OptionalField(
|
||||
serializer_class=build.serializers.BuildSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'build_order',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'many': False,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=[
|
||||
'build_order__responsible',
|
||||
'build_order__issued_by',
|
||||
@@ -763,10 +800,14 @@ class PurchaseOrderExtraLineSerializer(
|
||||
model = order.models.PurchaseOrderExtraLine
|
||||
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
||||
|
||||
order_detail = enable_filter(
|
||||
PurchaseOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
order_detail = OptionalField(
|
||||
serializer_class=PurchaseOrderSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1098,10 +1139,14 @@ class SalesOrderSerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='customer', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
customer_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'customer',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['customer'],
|
||||
)
|
||||
|
||||
@@ -1252,10 +1297,14 @@ class SalesOrderLineItemSerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
order_detail = OptionalField(
|
||||
serializer_class=SalesOrderSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=[
|
||||
'order__created_by',
|
||||
'order__responsible',
|
||||
@@ -1265,15 +1314,25 @@ class SalesOrderLineItemSerializer(
|
||||
],
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(source='part', many=False, read_only=True, allow_null=True),
|
||||
part_detail = OptionalField(
|
||||
serializer_class=PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['part__pricing_data'],
|
||||
)
|
||||
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='order.customer', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
customer_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order.customer',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['order__customer'],
|
||||
)
|
||||
|
||||
@@ -1343,19 +1402,27 @@ class SalesOrderShipmentSerializer(
|
||||
read_only=True, allow_null=True, label=_('Allocated Items')
|
||||
)
|
||||
|
||||
checked_by_detail = enable_filter(
|
||||
UserSerializer(
|
||||
source='checked_by', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
checked_by_detail = OptionalField(
|
||||
serializer_class=UserSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'checked_by',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['checked_by'],
|
||||
)
|
||||
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='order', read_only=True, allow_null=True, many=False
|
||||
),
|
||||
True,
|
||||
order_detail = OptionalField(
|
||||
serializer_class=SalesOrderSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=[
|
||||
'order',
|
||||
'order__customer',
|
||||
@@ -1365,19 +1432,27 @@ class SalesOrderShipmentSerializer(
|
||||
],
|
||||
)
|
||||
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='order.customer', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
customer_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order.customer',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['order__customer'],
|
||||
)
|
||||
|
||||
shipment_address_detail = enable_filter(
|
||||
AddressBriefSerializer(
|
||||
source='shipment_address', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
shipment_address_detail = OptionalField(
|
||||
serializer_class=AddressBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'shipment_address',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['shipment_address'],
|
||||
)
|
||||
|
||||
@@ -1428,46 +1503,71 @@ class SalesOrderAllocationSerializer(
|
||||
)
|
||||
|
||||
# Extra detail fields
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='line.order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
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
|
||||
)
|
||||
order_detail = OptionalField(
|
||||
serializer_class=SalesOrderSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'line.order',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
)
|
||||
|
||||
shipment_detail = SalesOrderShipmentSerializer(
|
||||
source='shipment',
|
||||
order_detail=False,
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'item.part',
|
||||
'many': False,
|
||||
'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
|
||||
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
||||
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
order_detail = OptionalField(
|
||||
serializer_class=SalesOrderSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1942,10 +2046,14 @@ class ReturnOrderSerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='customer', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
customer_detail = OptionalField(
|
||||
serializer_class=CompanyBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'customer',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['customer'],
|
||||
)
|
||||
|
||||
@@ -2103,10 +2211,14 @@ class ReturnOrderLineItemSerializer(
|
||||
'part_detail',
|
||||
])
|
||||
|
||||
order_detail = enable_filter(
|
||||
ReturnOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
order_detail = OptionalField(
|
||||
serializer_class=ReturnOrderSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'order',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=[
|
||||
'order__created_by',
|
||||
'order__responsible',
|
||||
@@ -2120,17 +2232,25 @@ class ReturnOrderLineItemSerializer(
|
||||
label=_('Quantity'), help_text=_('Quantity to return')
|
||||
)
|
||||
|
||||
item_detail = enable_filter(
|
||||
stock.serializers.StockItemSerializer(
|
||||
source='item', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
item_detail = OptionalField(
|
||||
serializer_class=stock.serializers.StockItemSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'item',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['item__supplier_part'],
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='item.part', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
part_detail = OptionalField(
|
||||
serializer_class=PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'item.part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
)
|
||||
|
||||
price = InvenTreeMoneySerializer(allow_null=True)
|
||||
@@ -2149,8 +2269,12 @@ class ReturnOrderExtraLineSerializer(
|
||||
model = order.models.ReturnOrderExtraLine
|
||||
fields = AbstractExtraLineSerializer.extra_line_fields([])
|
||||
|
||||
order_detail = enable_filter(
|
||||
ReturnOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
order_detail = OptionalField(
|
||||
serializer_class=ReturnOrderSerializer,
|
||||
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 InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import (
|
||||
FilterableDateTimeField,
|
||||
FilterableFloatField,
|
||||
FilterableListField,
|
||||
FilterableListSerializer,
|
||||
enable_filter,
|
||||
)
|
||||
from InvenTree.serializers import OptionalField, TreePathSerializer
|
||||
from users.serializers import UserSerializer
|
||||
|
||||
from .models import (
|
||||
@@ -141,13 +135,15 @@ class CategorySerializer(
|
||||
|
||||
return category.pk in self.starred_categories
|
||||
|
||||
path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
path = OptionalField(
|
||||
serializer_class=TreePathSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'get_path',
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='path_detail',
|
||||
)
|
||||
|
||||
@@ -354,18 +350,25 @@ class PartBriefSerializer(
|
||||
)
|
||||
|
||||
# Pricing fields
|
||||
pricing_min = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
pricing_min = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'pricing_data.overall_min',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_max = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
|
||||
pricing_max = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'pricing_data.overall_max',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
@@ -774,28 +777,37 @@ class PartSerializer(
|
||||
return part.pk in self.starred_parts
|
||||
|
||||
# Extra detail for the category
|
||||
category_detail = enable_filter(
|
||||
CategorySerializer(
|
||||
source='category', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
category_detail = OptionalField(
|
||||
serializer_class=CategorySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'category',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['category'],
|
||||
)
|
||||
|
||||
category_path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='category.get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
category_path = OptionalField(
|
||||
serializer_class=TreePathSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'category.get_path',
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
filter_name='path_detail',
|
||||
prefetch_fields=['category'],
|
||||
)
|
||||
|
||||
default_location_detail = enable_filter(
|
||||
DefaultLocationSerializer(
|
||||
source='default_location', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
default_location_detail = OptionalField(
|
||||
serializer_class=DefaultLocationSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'default_location',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
filter_name='location_detail',
|
||||
prefetch_fields=['default_location'],
|
||||
)
|
||||
@@ -901,25 +913,36 @@ class PartSerializer(
|
||||
)
|
||||
|
||||
# Pricing fields
|
||||
pricing_min = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
pricing_min = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'pricing_data.overall_min',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_max = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
|
||||
pricing_max = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'pricing_data.overall_max',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_updated = enable_filter(
|
||||
FilterableDateTimeField(
|
||||
source='pricing_data.updated', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
|
||||
pricing_updated = OptionalField(
|
||||
serializer_class=serializers.DateTimeField,
|
||||
serializer_kwargs={
|
||||
'source': 'pricing_data.updated',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
@@ -927,11 +950,15 @@ class PartSerializer(
|
||||
|
||||
tags = common.filters.enable_tags_filter()
|
||||
|
||||
price_breaks = enable_filter(
|
||||
PartSalePriceSerializer(
|
||||
source='salepricebreaks', many=True, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
price_breaks = OptionalField(
|
||||
serializer_class=PartSalePriceSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'salepricebreaks',
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='price_breaks',
|
||||
prefetch_fields=['salepricebreaks'],
|
||||
)
|
||||
@@ -1267,10 +1294,15 @@ class PartStocktakeSerializer(
|
||||
label=_('Part Description'),
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='part', read_only=True, allow_null=True, many=False, pricing=False
|
||||
),
|
||||
part_detail = OptionalField(
|
||||
serializer_class=PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part',
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'many': False,
|
||||
'pricing': False,
|
||||
},
|
||||
default_include=False,
|
||||
)
|
||||
|
||||
@@ -1595,7 +1627,7 @@ class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer
|
||||
|
||||
model = BomItemSubstitute
|
||||
fields = ['pk', 'bom_item', 'part', 'part_detail']
|
||||
list_serializer_class = FilterableListSerializer
|
||||
# list_serializer_class = FilterableListSerializer
|
||||
|
||||
part_detail = PartBriefSerializer(
|
||||
source='part', read_only=True, many=False, pricing=False
|
||||
@@ -1697,9 +1729,15 @@ class BomItemSerializer(
|
||||
help_text=_('Select the parent assembly'),
|
||||
)
|
||||
|
||||
substitutes = enable_filter(
|
||||
BomItemSubstituteSerializer(many=True, read_only=True, allow_null=True),
|
||||
False,
|
||||
substitutes = OptionalField(
|
||||
serializer_class=BomItemSubstituteSerializer,
|
||||
serializer_kwargs={
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
'required': False,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='substitutes',
|
||||
prefetch_fields=[
|
||||
'substitutes',
|
||||
@@ -1709,14 +1747,15 @@ class BomItemSerializer(
|
||||
],
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='part',
|
||||
label=_('Assembly'),
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
part_detail = OptionalField(
|
||||
serializer_class=PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part',
|
||||
'label': _('Assembly'),
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
)
|
||||
|
||||
sub_part = serializers.PrimaryKeyRelatedField(
|
||||
@@ -1725,26 +1764,28 @@ class BomItemSerializer(
|
||||
help_text=_('Select the component part'),
|
||||
)
|
||||
|
||||
sub_part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='sub_part',
|
||||
label=_('Component'),
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
sub_part_detail = OptionalField(
|
||||
serializer_class=PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'sub_part',
|
||||
'label': _('Component'),
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
)
|
||||
|
||||
category_detail = enable_filter(
|
||||
CategorySerializer(
|
||||
source='sub_part.category',
|
||||
label=_('Category'),
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
False,
|
||||
category_detail = OptionalField(
|
||||
serializer_class=CategorySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'sub_part.category',
|
||||
'label': _('Category'),
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
)
|
||||
|
||||
on_order = serializers.FloatField(
|
||||
@@ -1755,41 +1796,61 @@ class BomItemSerializer(
|
||||
label=_('In Production'), read_only=True, allow_null=True
|
||||
)
|
||||
|
||||
can_build = enable_filter(
|
||||
FilterableFloatField(label=_('Can Build'), read_only=True, allow_null=True),
|
||||
True,
|
||||
can_build = OptionalField(
|
||||
serializer_class=serializers.FloatField,
|
||||
serializer_kwargs={
|
||||
'label': _('Can Build'),
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
)
|
||||
|
||||
# Cached pricing fields
|
||||
pricing_min = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
pricing_min = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'sub_part.pricing_data.overall_min',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_max = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
|
||||
pricing_max = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'sub_part.pricing_data.overall_max',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_min_total = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
|
||||
True,
|
||||
|
||||
pricing_min_total = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={'allow_null': True, 'read_only': True},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_max_total = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
|
||||
True,
|
||||
|
||||
pricing_max_total = OptionalField(
|
||||
serializer_class=InvenTree.serializers.InvenTreeMoneySerializer,
|
||||
serializer_kwargs={'allow_null': True, 'read_only': True},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_updated = enable_filter(
|
||||
FilterableDateTimeField(
|
||||
source='sub_part.pricing_data.updated', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
|
||||
pricing_updated = OptionalField(
|
||||
serializer_class=serializers.DateTimeField,
|
||||
serializer_kwargs={
|
||||
'source': 'sub_part.pricing_data.updated',
|
||||
'allow_null': True,
|
||||
'read_only': True,
|
||||
},
|
||||
default_include=True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
@@ -1887,19 +1948,22 @@ class CategoryParameterTemplateSerializer(
|
||||
'default_value',
|
||||
]
|
||||
|
||||
template_detail = enable_filter(
|
||||
common.serializers.ParameterTemplateSerializer(
|
||||
source='template', many=False, read_only=True
|
||||
),
|
||||
True,
|
||||
template_detail = OptionalField(
|
||||
serializer_class=common.serializers.ParameterTemplateSerializer,
|
||||
serializer_kwargs={'source': 'template', 'many': False, 'read_only': True},
|
||||
default_include=True,
|
||||
prefetch_fields=['template'],
|
||||
)
|
||||
|
||||
category_detail = enable_filter(
|
||||
CategorySerializer(
|
||||
source='category', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
category_detail = OptionalField(
|
||||
serializer_class=CategorySerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'category',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
prefetch_fields=['category'],
|
||||
)
|
||||
|
||||
|
||||
@@ -1406,6 +1406,53 @@ class PartAPITest(PartAPITestBase):
|
||||
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):
|
||||
"""Tests for creating new Part instances via the API."""
|
||||
@@ -2707,8 +2754,28 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
|
||||
def test_get_bom_detail(self):
|
||||
"""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)
|
||||
|
||||
expected_values = [
|
||||
@@ -2735,6 +2802,15 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
|
||||
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
|
||||
data = response.data
|
||||
data['quantity'] = 57
|
||||
|
||||
@@ -33,10 +33,10 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
||||
from importer.registry import register_importer
|
||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.serializers import (
|
||||
FilterableListField,
|
||||
InvenTreeCurrencySerializer,
|
||||
InvenTreeDecimalField,
|
||||
enable_filter,
|
||||
OptionalField,
|
||||
TreePathSerializer,
|
||||
)
|
||||
from users.serializers import UserSerializer
|
||||
|
||||
@@ -231,8 +231,9 @@ class StockItemTestResultSerializer(
|
||||
self.fields['user'].read_only = True
|
||||
self.fields['date'].read_only = True
|
||||
|
||||
user_detail = enable_filter(
|
||||
UserSerializer(source='user', read_only=True, allow_null=True),
|
||||
user_detail = OptionalField(
|
||||
serializer_class=UserSerializer,
|
||||
serializer_kwargs={'source': 'user', 'read_only': True, 'allow_null': True},
|
||||
prefetch_fields=['user'],
|
||||
)
|
||||
|
||||
@@ -245,10 +246,9 @@ class StockItemTestResultSerializer(
|
||||
label=_('Test template for this result'),
|
||||
)
|
||||
|
||||
template_detail = enable_filter(
|
||||
part_serializers.PartTestTemplateSerializer(
|
||||
source='template', read_only=True, allow_null=True
|
||||
),
|
||||
template_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartTestTemplateSerializer,
|
||||
serializer_kwargs={'source': 'template', 'read_only': True, 'allow_null': True},
|
||||
prefetch_fields=['template'],
|
||||
)
|
||||
|
||||
@@ -377,20 +377,20 @@ class StockItemSerializer(
|
||||
'purchase_price_currency',
|
||||
'use_pack_size',
|
||||
'serial_numbers',
|
||||
'tests',
|
||||
# Annotated fields
|
||||
'allocated',
|
||||
'expired',
|
||||
'installed_items',
|
||||
'child_items',
|
||||
'location_path',
|
||||
'stale',
|
||||
'tracking_items',
|
||||
'tags',
|
||||
# Detail fields (FK relationships)
|
||||
'supplier_part_detail',
|
||||
'part_detail',
|
||||
# Optional fields (FK relationships)
|
||||
'location_detail',
|
||||
'location_path',
|
||||
'part_detail',
|
||||
'supplier_part_detail',
|
||||
'tags',
|
||||
'tests',
|
||||
'tracking_items',
|
||||
]
|
||||
read_only_fields = [
|
||||
'allocated',
|
||||
@@ -428,13 +428,16 @@ class StockItemSerializer(
|
||||
help_text=_('Parent stock item'),
|
||||
)
|
||||
|
||||
location_path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='location.get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
location_path = OptionalField(
|
||||
serializer_class=TreePathSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'location.get_path',
|
||||
'extra_fields': ['icon'],
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='path_detail',
|
||||
)
|
||||
|
||||
@@ -577,19 +580,20 @@ class StockItemSerializer(
|
||||
)
|
||||
|
||||
# Optional detail fields, which can be appended via query parameters
|
||||
supplier_part_detail = enable_filter(
|
||||
company_serializers.SupplierPartSerializer(
|
||||
label=_('Supplier Part'),
|
||||
source='supplier_part',
|
||||
brief=True,
|
||||
supplier_detail=False,
|
||||
manufacturer_detail=False,
|
||||
part_detail=False,
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
False,
|
||||
supplier_part_detail = OptionalField(
|
||||
serializer_class=company_serializers.SupplierPartSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Supplier Part'),
|
||||
'source': 'supplier_part',
|
||||
'brief': True,
|
||||
'supplier_detail': False,
|
||||
'manufacturer_detail': False,
|
||||
'part_detail': False,
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=[
|
||||
'supplier_part__supplier',
|
||||
'supplier_part__purchase_order_line_items',
|
||||
@@ -597,30 +601,40 @@ class StockItemSerializer(
|
||||
],
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
part_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Part'),
|
||||
'source': 'part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=True,
|
||||
)
|
||||
|
||||
location_detail = enable_filter(
|
||||
LocationBriefSerializer(
|
||||
label=_('Location'),
|
||||
source='location',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
False,
|
||||
location_detail = OptionalField(
|
||||
serializer_class=LocationBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'label': _('Location'),
|
||||
'source': 'location',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['location'],
|
||||
)
|
||||
|
||||
tests = enable_filter(
|
||||
StockItemTestResultSerializer(
|
||||
source='test_results', many=True, read_only=True, allow_null=True
|
||||
),
|
||||
False,
|
||||
tests = OptionalField(
|
||||
serializer_class=StockItemTestResultSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'test_results',
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=[
|
||||
'test_results',
|
||||
'test_results__user',
|
||||
@@ -1222,13 +1236,16 @@ class LocationSerializer(
|
||||
|
||||
tags = common.filters.enable_tags_filter()
|
||||
|
||||
path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
path = OptionalField(
|
||||
serializer_class=TreePathSerializer,
|
||||
serializer_kwargs={
|
||||
'many': True,
|
||||
'source': 'get_path',
|
||||
'extra_fields': ['icon'],
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
filter_name='path_detail',
|
||||
)
|
||||
|
||||
@@ -1273,21 +1290,37 @@ class StockTrackingSerializer(
|
||||
|
||||
label = serializers.CharField(read_only=True)
|
||||
|
||||
item_detail = enable_filter(
|
||||
StockItemSerializer(source='item', many=False, read_only=True, allow_null=True),
|
||||
item_detail = OptionalField(
|
||||
serializer_class=StockItemSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'item',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['item', 'item__part'],
|
||||
)
|
||||
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
source='part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
part_detail = OptionalField(
|
||||
serializer_class=part_serializers.PartBriefSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'part',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
default_include=False,
|
||||
prefetch_fields=['part'],
|
||||
)
|
||||
|
||||
user_detail = enable_filter(
|
||||
UserSerializer(source='user', many=False, read_only=True, allow_null=True),
|
||||
user_detail = OptionalField(
|
||||
serializer_class=UserSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'user',
|
||||
'many': False,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
prefetch_fields=['user'],
|
||||
)
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ class UserList(ListCreateAPI):
|
||||
- Otherwise authenticated users have read-only access
|
||||
"""
|
||||
|
||||
queryset = User.objects.all()
|
||||
queryset = User.objects.all().prefetch_related('groups')
|
||||
serializer_class = UserCreateSerializer
|
||||
|
||||
# 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 InvenTree.serializers import (
|
||||
FilterableListSerializer,
|
||||
FilterableSerializerMethodField,
|
||||
FilterableSerializerMixin,
|
||||
InvenTreeModelSerializer,
|
||||
enable_filter,
|
||||
OptionalField,
|
||||
)
|
||||
|
||||
from .models import ApiToken, Owner, RuleSet, UserProfile
|
||||
@@ -56,7 +54,6 @@ class RuleSetSerializer(InvenTreeModelSerializer):
|
||||
'can_delete',
|
||||
]
|
||||
read_only_fields = ['pk', 'name', 'label', 'group']
|
||||
list_serializer_class = FilterableListSerializer
|
||||
|
||||
|
||||
class RoleSerializer(InvenTreeModelSerializer):
|
||||
@@ -185,7 +182,6 @@ class UserSerializer(InvenTreeModelSerializer):
|
||||
model = User
|
||||
fields = ['pk', 'username', 'first_name', 'last_name', 'email']
|
||||
read_only_fields = ['username', 'email']
|
||||
list_serializer_class = FilterableListSerializer
|
||||
|
||||
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
||||
|
||||
@@ -267,8 +263,9 @@ class GroupSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
||||
model = Group
|
||||
fields = ['pk', 'name', 'permissions', 'roles', 'users']
|
||||
|
||||
permissions = enable_filter(
|
||||
FilterableSerializerMethodField(allow_null=True, read_only=True),
|
||||
permissions = OptionalField(
|
||||
serializer_class=serializers.SerializerMethodField,
|
||||
serializer_kwargs={'allow_null': True, 'read_only': True},
|
||||
filter_name='permission_detail',
|
||||
)
|
||||
|
||||
@@ -276,16 +273,26 @@ class GroupSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
||||
"""Return a list of permissions associated with the group."""
|
||||
return generate_permission_dict(group.permissions.all())
|
||||
|
||||
roles = enable_filter(
|
||||
RuleSetSerializer(
|
||||
source='rule_sets', many=True, read_only=True, allow_null=True
|
||||
),
|
||||
roles = OptionalField(
|
||||
serializer_class=RuleSetSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'rule_sets',
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
filter_name='role_detail',
|
||||
prefetch_fields=['rule_sets'],
|
||||
)
|
||||
|
||||
users = enable_filter(
|
||||
UserSerializer(source='user_set', many=True, read_only=True, allow_null=True),
|
||||
users = OptionalField(
|
||||
serializer_class=UserSerializer,
|
||||
serializer_kwargs={
|
||||
'source': 'user_set',
|
||||
'many': True,
|
||||
'read_only': True,
|
||||
'allow_null': True,
|
||||
},
|
||||
filter_name='user_detail',
|
||||
prefetch_fields=['user_set'],
|
||||
)
|
||||
|
||||
@@ -218,8 +218,11 @@ class UserAPITests(InvenTreeAPITestCase):
|
||||
expected_code=200,
|
||||
)
|
||||
self.assertIn('name', response.data)
|
||||
self.assertIn('roles', response.data)
|
||||
self.assertIn('permissions', response.data)
|
||||
|
||||
self.assertGreater(len(response.data['roles']), 0)
|
||||
|
||||
def test_login_redirect(self):
|
||||
"""Test login redirect endpoint."""
|
||||
response = self.get(reverse('api-login-redirect'), expected_code=302)
|
||||
|
||||
Reference in New Issue
Block a user