mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-18 23:17:41 +00:00
refactor(backend): shift filterable serializer responses to a more introspection friendly model (#10498)
* move filtering of serializer fields out of functions into mixin * fix def * temp fix * rollback rollback * more adoption * fix many serializer behaviour * optimize mro * set many serializer * adjust default filtering * fix import * add missed field * make can_filter suppport more complex scenarios: - different filtername from fieldname - multiple fields with one filtername * fix removal * fix schema? * add missing def * add test * fix schema fields * fix another serializer issue * further fixes * extend tests * also process strings * fix serializer for schema * ensure popped values are persisted * move test around * cleanup * simplify tests * fix typo * fix another test * var tests * disable additional tests * make application of PathScopedMixin more intentional -> more efficient * make safer to use with various sanity checks * fix list serializer * add option to ignore special cases * generalize addition * remove generalize addition * re-add missing schema generation exception * remove new duplication * fix style * adjust naming and docs, add typing to clean stuff up * simplify more * fix ref calc
This commit is contained in:
@@ -183,5 +183,12 @@ Overriding a global setting with a different value than the current one.
|
||||
|
||||
See [Override global settings](../settings/global.md#override-global-settings) for more information.
|
||||
|
||||
#### INVE-I2
|
||||
**Issue with filtering serializer or decorator - Backend**
|
||||
|
||||
An issue was detected with the application of a filtering serializer or decorator. This might lead to unexpected behaviour or performance issues. Therefore an issue is raised to make the developer aware of the possible issue. Look into the docstrings of enable_filter, FilterableSerializerField or FilterableSerializerMixin.
|
||||
|
||||
This warning should only be raised during development and not in production, if you recently installed a plugin you might want to contact the plugin author.
|
||||
|
||||
### INVE-M (InvenTree Miscellaneous)
|
||||
Miscellaneous — These are information messages that might be used to mark debug information or other messages helpful for the InvenTree team to understand behaviour.
|
||||
|
@@ -14,6 +14,7 @@ from InvenTree.helpers import (
|
||||
strip_html_tags,
|
||||
)
|
||||
from InvenTree.schema import schema_for_view_output_options
|
||||
from InvenTree.serializers import FilterableSerializerMixin
|
||||
|
||||
|
||||
class CleanMixin:
|
||||
@@ -227,7 +228,15 @@ class OutputOptionsMixin:
|
||||
params = self.request.query_params
|
||||
kwargs.update(self.output_options.format_params(params))
|
||||
|
||||
return super().get_serializer(*args, **kwargs)
|
||||
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 not isinstance(serializer, FilterableSerializerMixin):
|
||||
raise Exception(
|
||||
'INVE-I2: `OutputOptionsMixin` can only be used with serializers that contain the `FilterableSerializerMixin` mixin'
|
||||
)
|
||||
|
||||
return serializer
|
||||
|
||||
|
||||
class SerializerContextMixin:
|
||||
|
@@ -4,6 +4,7 @@ import os
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
@@ -26,16 +27,174 @@ import common.models as common_models
|
||||
import InvenTree.ready
|
||||
from common.currency import currency_code_default, currency_code_mappings
|
||||
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
||||
from InvenTree.helpers import str2bool
|
||||
|
||||
|
||||
# region path filtering
|
||||
class FilterableSerializerField:
|
||||
"""Mixin to mark serializer as filterable.
|
||||
|
||||
This needs to be used in conjunction with `enable_filter` on the serializer field!
|
||||
"""
|
||||
|
||||
is_filterable = None
|
||||
is_filterable_vals = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the serializer."""
|
||||
if self.is_filterable is None: # Materialize parameters for later usage
|
||||
self.is_filterable = kwargs.pop('is_filterable', None)
|
||||
self.is_filterable_vals = kwargs.pop('is_filterable_vals', {})
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def enable_filter(
|
||||
func: Any, default_include: bool = False, filter_name: Optional[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.
|
||||
"""
|
||||
# Ensure this function can be actually filteres
|
||||
if not issubclass(func.__class__, FilterableSerializerField):
|
||||
raise TypeError(
|
||||
'INVE-I2: `enable_filter` can only be applied to serializer fields / serializers that contain the `FilterableSerializerField` mixin!'
|
||||
)
|
||||
|
||||
# Mark the function as filterable
|
||||
func._kwargs['is_filterable'] = True
|
||||
func._kwargs['is_filterable_vals'] = {
|
||||
'default': default_include,
|
||||
'filter_name': filter_name if filter_name else func.field_name,
|
||||
}
|
||||
return func
|
||||
|
||||
|
||||
class FilterableSerializerMixin:
|
||||
"""Mixin that enables filtering of marked fields on a serializer.
|
||||
|
||||
Use the `enable_filter` decorator 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
|
||||
|
||||
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
|
||||
|
||||
self.gather_filters(kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.do_filtering()
|
||||
|
||||
def gather_filters(self, kwargs) -> None:
|
||||
"""Gather filterable fields through introspection."""
|
||||
# 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
|
||||
self._was_filtered = True
|
||||
|
||||
# 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__
|
||||
poped_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, poped_kwargs.get(pop_ref))
|
||||
if val: # Save popped value for reuse
|
||||
poped_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
|
||||
|
||||
# Ensure this mixin is not proadly 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
|
||||
|
||||
# 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."""
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
class EmptySerializer(serializers.Serializer):
|
||||
"""Empty serializer for use in testing."""
|
||||
|
||||
|
||||
class InvenTreeMoneySerializer(MoneyField):
|
||||
class InvenTreeMoneySerializer(FilterableSerializerField, 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):
|
||||
@@ -222,7 +381,7 @@ class DependentField(serializers.Field):
|
||||
return None
|
||||
|
||||
|
||||
class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
class InvenTreeModelSerializer(FilterableSerializerField, 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
|
||||
|
@@ -22,22 +22,23 @@ from rest_framework import serializers
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
import build.tasks
|
||||
import common.models
|
||||
import common.settings
|
||||
import company.serializers
|
||||
import InvenTree.helpers
|
||||
import InvenTree.tasks
|
||||
import part.filters
|
||||
import part.serializers as part_serializers
|
||||
from common.serializers import ProjectCodeSerializer
|
||||
from common.settings import get_global_setting
|
||||
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import (
|
||||
FilterableCharField,
|
||||
FilterableIntegerField,
|
||||
FilterableSerializerMixin,
|
||||
InvenTreeDecimalField,
|
||||
InvenTreeModelSerializer,
|
||||
NotesFieldMixin,
|
||||
enable_filter,
|
||||
)
|
||||
from stock.generators import generate_batch_code
|
||||
from stock.models import StockItem, StockLocation
|
||||
@@ -54,6 +55,7 @@ from .status_codes import BuildStatus
|
||||
|
||||
|
||||
class BuildSerializer(
|
||||
FilterableSerializerMixin,
|
||||
NotesFieldMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTreeCustomStatusSerializerMixin,
|
||||
@@ -101,7 +103,6 @@ class BuildSerializer(
|
||||
'priority',
|
||||
'level',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'completed',
|
||||
'creation_date',
|
||||
@@ -117,8 +118,9 @@ class BuildSerializer(
|
||||
|
||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||
|
||||
part_detail = part_serializers.PartBriefSerializer(
|
||||
source='part', many=False, read_only=True
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(source='part', many=False, read_only=True),
|
||||
True,
|
||||
)
|
||||
|
||||
part_name = serializers.CharField(
|
||||
@@ -129,23 +131,48 @@ class BuildSerializer(
|
||||
|
||||
overdue = serializers.BooleanField(read_only=True, default=False)
|
||||
|
||||
issued_by_detail = UserSerializer(source='issued_by', read_only=True)
|
||||
issued_by_detail = enable_filter(
|
||||
UserSerializer(source='issued_by', read_only=True),
|
||||
True,
|
||||
filter_name='user_detail',
|
||||
)
|
||||
|
||||
responsible_detail = OwnerSerializer(
|
||||
source='responsible', read_only=True, allow_null=True
|
||||
responsible_detail = enable_filter(
|
||||
OwnerSerializer(source='responsible', read_only=True, allow_null=True),
|
||||
True,
|
||||
filter_name='user_detail',
|
||||
)
|
||||
|
||||
barcode_hash = serializers.CharField(read_only=True)
|
||||
|
||||
project_code_label = serializers.CharField(
|
||||
source='project_code.code',
|
||||
read_only=True,
|
||||
label=_('Project Code Label'),
|
||||
allow_null=True,
|
||||
project_code_label = enable_filter(
|
||||
FilterableCharField(
|
||||
source='project_code.code',
|
||||
read_only=True,
|
||||
label=_('Project Code Label'),
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
filter_name='project_code_detail',
|
||||
)
|
||||
|
||||
project_code_detail = ProjectCodeSerializer(
|
||||
source='project_code', many=False, read_only=True, allow_null=True
|
||||
project_code_detail = enable_filter(
|
||||
ProjectCodeSerializer(
|
||||
source='project_code', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
filter_name='project_code_detail',
|
||||
)
|
||||
|
||||
project_code = enable_filter(
|
||||
FilterableIntegerField(
|
||||
allow_null=True,
|
||||
required=False,
|
||||
label=_('Project Code'),
|
||||
help_text=_('Project code for this build order'),
|
||||
),
|
||||
True,
|
||||
filter_name='project_code_detail',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -171,29 +198,10 @@ class BuildSerializer(
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Determine if extra serializer fields are required."""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
user_detail = kwargs.pop('user_detail', True)
|
||||
project_code_detail = kwargs.pop('project_code_detail', True)
|
||||
|
||||
kwargs.pop('create', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if not user_detail:
|
||||
self.fields.pop('issued_by_detail', None)
|
||||
self.fields.pop('responsible_detail', None)
|
||||
|
||||
if not project_code_detail:
|
||||
self.fields.pop('project_code', None)
|
||||
self.fields.pop('project_code_label', None)
|
||||
self.fields.pop('project_code_detail', None)
|
||||
|
||||
def validate_reference(self, reference):
|
||||
"""Custom validation for the Build reference field."""
|
||||
# Ensure the reference matches the required pattern
|
||||
@@ -1150,7 +1158,9 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
||||
raise ValidationError(_('Failed to start auto-allocation task'))
|
||||
|
||||
|
||||
class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
||||
class BuildItemSerializer(
|
||||
FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializes a BuildItem object, which is an allocation of a stock item against a build order."""
|
||||
|
||||
export_child_fields = [
|
||||
@@ -1195,30 +1205,6 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
'bom_part_name',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Determine which extra details fields should be included."""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
location_detail = kwargs.pop('location_detail', True)
|
||||
stock_detail = kwargs.pop('stock_detail', True)
|
||||
build_detail = kwargs.pop('build_detail', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if not location_detail:
|
||||
self.fields.pop('location_detail', None)
|
||||
|
||||
if not stock_detail:
|
||||
self.fields.pop('stock_item_detail', None)
|
||||
|
||||
if not build_detail:
|
||||
self.fields.pop('build_detail', None)
|
||||
|
||||
# Export-only fields
|
||||
bom_reference = serializers.CharField(
|
||||
source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True
|
||||
@@ -1244,43 +1230,56 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
)
|
||||
|
||||
# Extra (optional) detail fields
|
||||
part_detail = part_serializers.PartBriefSerializer(
|
||||
label=_('Part'),
|
||||
source='stock_item.part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Part'),
|
||||
source='stock_item.part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
stock_item_detail = 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,
|
||||
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,
|
||||
filter_name='stock_detail',
|
||||
)
|
||||
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
label=_('Location'), source='stock_item.location', many=False, read_only=True
|
||||
)
|
||||
|
||||
location_detail = LocationBriefSerializer(
|
||||
label=_('Location'),
|
||||
source='stock_item.location',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
location_detail = enable_filter(
|
||||
LocationBriefSerializer(
|
||||
label=_('Location'),
|
||||
source='stock_item.location',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
build_detail = BuildSerializer(
|
||||
label=_('Build'),
|
||||
source='build_line.build',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
build_detail = enable_filter(
|
||||
BuildSerializer(
|
||||
label=_('Build'),
|
||||
source='build_line.build',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
supplier_part_detail = company.serializers.SupplierPartSerializer(
|
||||
@@ -1295,7 +1294,9 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
|
||||
|
||||
|
||||
class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
|
||||
class BuildLineSerializer(
|
||||
FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for a BuildItem object."""
|
||||
|
||||
export_exclude_fields = ['allocations']
|
||||
@@ -1352,38 +1353,8 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
'part_detail',
|
||||
'build_detail',
|
||||
]
|
||||
|
||||
read_only_fields = ['build', 'bom_item', 'allocations']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Determine which extra details fields should be included."""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
assembly_detail = kwargs.pop('assembly_detail', True)
|
||||
bom_item_detail = kwargs.pop('bom_item_detail', True)
|
||||
build_detail = kwargs.pop('build_detail', True)
|
||||
allocations = kwargs.pop('allocations', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not bom_item_detail:
|
||||
self.fields.pop('bom_item_detail', None)
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
self.fields.pop('part_category_name', None)
|
||||
|
||||
if not build_detail:
|
||||
self.fields.pop('build_detail', None)
|
||||
|
||||
if not allocations:
|
||||
self.fields.pop('allocations', None)
|
||||
|
||||
if not assembly_detail:
|
||||
self.fields.pop('assembly_detail', None)
|
||||
|
||||
# Build info fields
|
||||
build_reference = serializers.CharField(
|
||||
source='build.reference', label=_('Build Reference'), read_only=True
|
||||
@@ -1400,7 +1371,9 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
allocations = BuildItemSerializer(many=True, read_only=True, build_detail=False)
|
||||
allocations = enable_filter(
|
||||
BuildItemSerializer(many=True, read_only=True, build_detail=False), True
|
||||
)
|
||||
|
||||
# BOM item info fields
|
||||
reference = serializers.CharField(
|
||||
@@ -1431,44 +1404,56 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
|
||||
|
||||
# Foreign key fields
|
||||
bom_item_detail = part_serializers.BomItemSerializer(
|
||||
label=_('BOM Item'),
|
||||
source='bom_item',
|
||||
many=False,
|
||||
read_only=True,
|
||||
pricing=False,
|
||||
substitutes=False,
|
||||
sub_part_detail=False,
|
||||
part_detail=False,
|
||||
can_build=False,
|
||||
bom_item_detail = enable_filter(
|
||||
part_serializers.BomItemSerializer(
|
||||
label=_('BOM Item'),
|
||||
source='bom_item',
|
||||
many=False,
|
||||
read_only=True,
|
||||
pricing=False,
|
||||
substitutes=False,
|
||||
sub_part_detail=False,
|
||||
part_detail=False,
|
||||
can_build=False,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
assembly_detail = part_serializers.PartBriefSerializer(
|
||||
label=_('Assembly'),
|
||||
source='bom_item.part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
assembly_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Assembly'),
|
||||
source='bom_item.part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
pricing=False,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
part_detail = part_serializers.PartBriefSerializer(
|
||||
label=_('Part'),
|
||||
source='bom_item.sub_part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
pricing=False,
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Part'),
|
||||
source='bom_item.sub_part',
|
||||
many=False,
|
||||
read_only=True,
|
||||
pricing=False,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
build_detail = BuildSerializer(
|
||||
label=_('Build'),
|
||||
source='build',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
part_detail=False,
|
||||
user_detail=False,
|
||||
project_code_detail=False,
|
||||
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,
|
||||
)
|
||||
|
||||
# Annotated (calculated) fields
|
||||
|
@@ -1189,6 +1189,21 @@ class BuildListTest(BuildAPITest):
|
||||
|
||||
self.assertEqual(len(builds), 20)
|
||||
|
||||
def test_output_options(self):
|
||||
"""Test the output options for BuildOrderList list."""
|
||||
self.run_output_test(
|
||||
self.url,
|
||||
[
|
||||
'part_detail'
|
||||
# TODO re-enable ('project_code_detail', 'project_code'),
|
||||
# TODO re-enable 'project_code_detail',
|
||||
# TODO re-enable ('user_detail', 'responsible_detail'),
|
||||
# TODO re-enable ('user_detail', 'issued_by_detail'),
|
||||
],
|
||||
additional_params={'limit': 1},
|
||||
assert_fnc=lambda x: x.data['results'][0],
|
||||
)
|
||||
|
||||
|
||||
class BuildOutputCreateTest(BuildAPITest):
|
||||
"""Unit test for creating build output via API."""
|
||||
|
@@ -18,6 +18,8 @@ from importer.registry import register_importer
|
||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import (
|
||||
FilterableCharField,
|
||||
FilterableSerializerMixin,
|
||||
InvenTreeCurrencySerializer,
|
||||
InvenTreeDecimalField,
|
||||
InvenTreeImageSerializerField,
|
||||
@@ -26,6 +28,7 @@ from InvenTree.serializers import (
|
||||
InvenTreeTagModelSerializer,
|
||||
NotesFieldMixin,
|
||||
RemoteImageMixin,
|
||||
enable_filter,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
@@ -248,7 +251,10 @@ class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerialize
|
||||
|
||||
@register_importer()
|
||||
class ManufacturerPartSerializer(
|
||||
DataImportExportSerializerMixin, InvenTreeTagModelSerializer, NotesFieldMixin
|
||||
FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTreeTagModelSerializer,
|
||||
NotesFieldMixin,
|
||||
):
|
||||
"""Serializer for ManufacturerPart object."""
|
||||
|
||||
@@ -273,35 +279,23 @@ class ManufacturerPartSerializer(
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required."""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
||||
prettify = kwargs.pop('pretty', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if part_detail is not True:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if manufacturer_detail is not True:
|
||||
self.fields.pop('manufacturer_detail', None)
|
||||
|
||||
if prettify is not True:
|
||||
self.fields.pop('pretty_name', None)
|
||||
|
||||
part_detail = part_serializers.PartBriefSerializer(
|
||||
source='part', many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
source='part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
manufacturer_detail = CompanyBriefSerializer(
|
||||
source='manufacturer', many=False, read_only=True, allow_null=True
|
||||
manufacturer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='manufacturer', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
pretty_name = serializers.CharField(read_only=True, allow_null=True)
|
||||
pretty_name = enable_filter(
|
||||
FilterableCharField(read_only=True, allow_null=True), filter_name='pretty'
|
||||
)
|
||||
|
||||
manufacturer = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Company.objects.filter(is_manufacturer=True)
|
||||
@@ -310,7 +304,7 @@ class ManufacturerPartSerializer(
|
||||
|
||||
@register_importer()
|
||||
class ManufacturerPartParameterSerializer(
|
||||
DataImportExportSerializerMixin, InvenTreeModelSerializer
|
||||
FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for the ManufacturerPartParameter model."""
|
||||
|
||||
@@ -328,26 +322,24 @@ class ManufacturerPartParameterSerializer(
|
||||
'units',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required."""
|
||||
man_detail = kwargs.pop('manufacturer_part_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not man_detail and not isGeneratingSchema():
|
||||
self.fields.pop('manufacturer_part_detail', None)
|
||||
|
||||
manufacturer_part_detail = ManufacturerPartSerializer(
|
||||
source='manufacturer_part', many=False, read_only=True, allow_null=True
|
||||
manufacturer_part_detail = enable_filter(
|
||||
ManufacturerPartSerializer(
|
||||
source='manufacturer_part', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_importer()
|
||||
class SupplierPartSerializer(
|
||||
DataImportExportSerializerMixin, InvenTreeTagModelSerializer, NotesFieldMixin
|
||||
FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTreeTagModelSerializer,
|
||||
NotesFieldMixin,
|
||||
):
|
||||
"""Serializer for SupplierPart object."""
|
||||
|
||||
no_filters = True
|
||||
|
||||
export_exclude_fields = ['tags']
|
||||
|
||||
export_child_fields = [
|
||||
@@ -390,7 +382,6 @@ class SupplierPartSerializer(
|
||||
'notes',
|
||||
'tags',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'availability_updated',
|
||||
'barcode_hash',
|
||||
@@ -402,13 +393,11 @@ class SupplierPartSerializer(
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required."""
|
||||
# Check if 'available' quantity was supplied
|
||||
|
||||
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
||||
|
||||
# TODO INVE-T1 support complex filters
|
||||
brief = kwargs.pop('brief', False)
|
||||
|
||||
detail_default = not brief
|
||||
|
||||
part_detail = kwargs.pop('part_detail', detail_default)
|
||||
supplier_detail = kwargs.pop('supplier_detail', detail_default)
|
||||
manufacturer_detail = kwargs.pop('manufacturer_detail', detail_default)
|
||||
@@ -543,7 +532,7 @@ class SupplierPartSerializer(
|
||||
|
||||
@register_importer()
|
||||
class SupplierPriceBreakSerializer(
|
||||
DataImportExportSerializerMixin, InvenTreeModelSerializer
|
||||
FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for SupplierPriceBreak object."""
|
||||
|
||||
@@ -563,22 +552,6 @@ class SupplierPriceBreakSerializer(
|
||||
'updated',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra fields as required."""
|
||||
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not supplier_detail:
|
||||
self.fields.pop('supplier_detail', None)
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Prefetch related fields for the queryset."""
|
||||
@@ -596,11 +569,15 @@ class SupplierPriceBreakSerializer(
|
||||
source='part.supplier', many=False, read_only=True
|
||||
)
|
||||
|
||||
supplier_detail = CompanyBriefSerializer(
|
||||
source='part.supplier', many=False, read_only=True, allow_null=True
|
||||
supplier_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='part.supplier', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
# Detail serializer for SupplierPart
|
||||
part_detail = SupplierPartSerializer(
|
||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
SupplierPartSerializer(
|
||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
@@ -27,7 +27,6 @@ import part.filters as part_filters
|
||||
import part.models as part_models
|
||||
import stock.models
|
||||
import stock.serializers
|
||||
import stock.status_codes
|
||||
from common.serializers import ProjectCodeSerializer
|
||||
from company.serializers import (
|
||||
AddressBriefSerializer,
|
||||
@@ -45,13 +44,14 @@ from InvenTree.helpers import (
|
||||
str2bool,
|
||||
)
|
||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import (
|
||||
FilterableSerializerMixin,
|
||||
InvenTreeCurrencySerializer,
|
||||
InvenTreeDecimalField,
|
||||
InvenTreeModelSerializer,
|
||||
InvenTreeMoneySerializer,
|
||||
NotesFieldMixin,
|
||||
enable_filter,
|
||||
)
|
||||
from order.status_codes import (
|
||||
PurchaseOrderStatusGroups,
|
||||
@@ -276,15 +276,6 @@ class AbstractExtraLineSerializer(
|
||||
):
|
||||
"""Abstract Serializer for a ExtraLine object."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if order_detail is not True and not isGeneratingSchema():
|
||||
self.fields.pop('order_detail', None)
|
||||
|
||||
quantity = serializers.FloatField()
|
||||
|
||||
price = InvenTreeMoneySerializer(allow_null=True)
|
||||
@@ -312,6 +303,7 @@ class AbstractExtraLineMeta:
|
||||
|
||||
@register_importer()
|
||||
class PurchaseOrderSerializer(
|
||||
FilterableSerializerMixin,
|
||||
NotesFieldMixin,
|
||||
TotalPriceMixin,
|
||||
InvenTreeCustomStatusSerializerMixin,
|
||||
@@ -324,7 +316,6 @@ class PurchaseOrderSerializer(
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.PurchaseOrder
|
||||
|
||||
fields = AbstractOrderSerializer.order_fields([
|
||||
'complete_date',
|
||||
'supplier',
|
||||
@@ -335,23 +326,12 @@ class PurchaseOrderSerializer(
|
||||
'order_currency',
|
||||
'destination',
|
||||
])
|
||||
|
||||
read_only_fields = ['issue_date', 'complete_date', 'creation_date']
|
||||
|
||||
extra_kwargs = {
|
||||
'supplier': {'required': True},
|
||||
'order_currency': {'required': False},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if supplier_detail is not True and not isGeneratingSchema():
|
||||
self.fields.pop('supplier_detail', None)
|
||||
|
||||
def skip_create_fields(self):
|
||||
"""Skip these fields when instantiating a new object."""
|
||||
fields = super().skip_create_fields()
|
||||
@@ -389,8 +369,10 @@ class PurchaseOrderSerializer(
|
||||
source='supplier.name', read_only=True, label=_('Supplier Name')
|
||||
)
|
||||
|
||||
supplier_detail = CompanyBriefSerializer(
|
||||
source='supplier', many=False, read_only=True, allow_null=True
|
||||
supplier_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='supplier', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -478,6 +460,7 @@ class PurchaseOrderIssueSerializer(OrderAdjustSerializer):
|
||||
|
||||
@register_importer()
|
||||
class PurchaseOrderLineItemSerializer(
|
||||
FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
AbstractLineItemSerializer,
|
||||
InvenTreeModelSerializer,
|
||||
@@ -488,7 +471,6 @@ class PurchaseOrderLineItemSerializer(
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.PurchaseOrderLineItem
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'part',
|
||||
@@ -519,23 +501,6 @@ class PurchaseOrderLineItemSerializer(
|
||||
'internal_part_name',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if part_detail is not True:
|
||||
self.fields.pop('part_detail', None)
|
||||
self.fields.pop('supplier_part_detail', None)
|
||||
|
||||
if order_detail is not True:
|
||||
self.fields.pop('order_detail', None)
|
||||
|
||||
def skip_create_fields(self):
|
||||
"""Return a list of fields to skip when creating a new object."""
|
||||
return ['auto_pricing', 'merge_items', *super().skip_create_fields()]
|
||||
@@ -618,12 +583,18 @@ class PurchaseOrderLineItemSerializer(
|
||||
|
||||
total_price = serializers.FloatField(read_only=True)
|
||||
|
||||
part_detail = PartBriefSerializer(
|
||||
source='get_base_part', many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='get_base_part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
filter_name='part_detail',
|
||||
)
|
||||
|
||||
supplier_part_detail = SupplierPartSerializer(
|
||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
||||
supplier_part_detail = enable_filter(
|
||||
SupplierPartSerializer(
|
||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
||||
),
|
||||
filter_name='part_detail',
|
||||
)
|
||||
|
||||
purchase_price = InvenTreeMoneySerializer(allow_null=True)
|
||||
@@ -644,8 +615,10 @@ class PurchaseOrderLineItemSerializer(
|
||||
help_text=_('Purchase price currency')
|
||||
)
|
||||
|
||||
order_detail = PurchaseOrderSerializer(
|
||||
source='order', read_only=True, allow_null=True, many=False
|
||||
order_detail = enable_filter(
|
||||
PurchaseOrderSerializer(
|
||||
source='order', read_only=True, allow_null=True, many=False
|
||||
)
|
||||
)
|
||||
|
||||
build_order_detail = build.serializers.BuildSerializer(
|
||||
@@ -720,12 +693,14 @@ class PurchaseOrderLineItemSerializer(
|
||||
|
||||
@register_importer()
|
||||
class PurchaseOrderExtraLineSerializer(
|
||||
AbstractExtraLineSerializer, InvenTreeModelSerializer
|
||||
FilterableSerializerMixin, AbstractExtraLineSerializer, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for a PurchaseOrderExtraLine object."""
|
||||
|
||||
order_detail = PurchaseOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
order_detail = enable_filter(
|
||||
PurchaseOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
class Meta(AbstractExtraLineMeta):
|
||||
@@ -981,6 +956,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
|
||||
@register_importer()
|
||||
class SalesOrderSerializer(
|
||||
FilterableSerializerMixin,
|
||||
NotesFieldMixin,
|
||||
TotalPriceMixin,
|
||||
InvenTreeCustomStatusSerializerMixin,
|
||||
@@ -993,7 +969,6 @@ class SalesOrderSerializer(
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrder
|
||||
|
||||
fields = AbstractOrderSerializer.order_fields([
|
||||
'customer',
|
||||
'customer_detail',
|
||||
@@ -1004,20 +979,9 @@ class SalesOrderSerializer(
|
||||
'shipments_count',
|
||||
'completed_shipments_count',
|
||||
])
|
||||
|
||||
read_only_fields = ['status', 'creation_date', 'shipment_date']
|
||||
|
||||
extra_kwargs = {'order_currency': {'required': False}}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
customer_detail = kwargs.pop('customer_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if customer_detail is not True and not isGeneratingSchema():
|
||||
self.fields.pop('customer_detail', None)
|
||||
|
||||
def skip_create_fields(self):
|
||||
"""Skip these fields when instantiating a new object."""
|
||||
fields = super().skip_create_fields()
|
||||
@@ -1058,8 +1022,10 @@ class SalesOrderSerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
customer_detail = CompanyBriefSerializer(
|
||||
source='customer', many=False, read_only=True, allow_null=True
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='customer', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
shipments_count = serializers.IntegerField(
|
||||
@@ -1081,6 +1047,7 @@ class SalesOrderIssueSerializer(OrderAdjustSerializer):
|
||||
|
||||
@register_importer()
|
||||
class SalesOrderLineItemSerializer(
|
||||
FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
AbstractLineItemSerializer,
|
||||
InvenTreeModelSerializer,
|
||||
@@ -1091,7 +1058,6 @@ class SalesOrderLineItemSerializer(
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderLineItem
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'allocated',
|
||||
@@ -1116,29 +1082,6 @@ class SalesOrderLineItemSerializer(
|
||||
'on_order',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer.
|
||||
|
||||
- Add extra related serializer information if required
|
||||
"""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
customer_detail = kwargs.pop('customer_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if part_detail is not True:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if order_detail is not True:
|
||||
self.fields.pop('order_detail', None)
|
||||
|
||||
if customer_detail is not True:
|
||||
self.fields.pop('customer_detail', None)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Add some extra annotations to this queryset.
|
||||
@@ -1236,14 +1179,18 @@ class SalesOrderLineItemSerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
order_detail = SalesOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
part_detail = PartBriefSerializer(
|
||||
source='part', many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(source='part', many=False, read_only=True, allow_null=True)
|
||||
)
|
||||
customer_detail = CompanyBriefSerializer(
|
||||
source='order.customer', many=False, read_only=True, allow_null=True
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='order.customer', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
# Annotated fields
|
||||
@@ -1267,14 +1214,15 @@ class SalesOrderLineItemSerializer(
|
||||
|
||||
|
||||
@register_importer()
|
||||
class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
|
||||
class SalesOrderShipmentSerializer(
|
||||
FilterableSerializerMixin, NotesFieldMixin, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for the SalesOrderShipment class."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderShipment
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'order',
|
||||
@@ -1291,15 +1239,6 @@ class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
|
||||
'notes',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
order_detail = kwargs.pop('order_detail', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not order_detail and not isGeneratingSchema():
|
||||
self.fields.pop('order_detail', None)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate the queryset with extra information."""
|
||||
@@ -1314,12 +1253,17 @@ class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
|
||||
read_only=True, allow_null=True, label=_('Allocated Items')
|
||||
)
|
||||
|
||||
order_detail = SalesOrderSerializer(
|
||||
source='order', read_only=True, allow_null=True, many=False
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='order', read_only=True, allow_null=True, many=False
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
class SalesOrderAllocationSerializer(
|
||||
FilterableSerializerMixin, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for the SalesOrderAllocation model.
|
||||
|
||||
This includes some fields from the related model objects.
|
||||
@@ -1329,7 +1273,6 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderAllocation
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'item',
|
||||
@@ -1349,37 +1292,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
'location_detail',
|
||||
'shipment_detail',
|
||||
]
|
||||
|
||||
read_only_fields = ['line', '']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
item_detail = kwargs.pop('item_detail', True)
|
||||
location_detail = kwargs.pop('location_detail', False)
|
||||
customer_detail = kwargs.pop('customer_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not order_detail:
|
||||
self.fields.pop('order_detail', None)
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if not item_detail:
|
||||
self.fields.pop('item_detail', None)
|
||||
|
||||
if not location_detail:
|
||||
self.fields.pop('location_detail', None)
|
||||
|
||||
if not customer_detail:
|
||||
self.fields.pop('customer_detail', None)
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
|
||||
order = serializers.PrimaryKeyRelatedField(
|
||||
source='line.order', many=False, read_only=True
|
||||
@@ -1391,26 +1305,38 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
# Extra detail fields
|
||||
order_detail = SalesOrderSerializer(
|
||||
source='line.order', many=False, read_only=True, allow_null=True
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='line.order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
part_detail = PartBriefSerializer(
|
||||
source='item.part', 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 = stock.serializers.StockItemSerializer(
|
||||
source='item',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
part_detail=False,
|
||||
location_detail=False,
|
||||
supplier_part_detail=False,
|
||||
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 = stock.serializers.LocationBriefSerializer(
|
||||
source='item.location', many=False, read_only=True, allow_null=True
|
||||
location_detail = enable_filter(
|
||||
stock.serializers.LocationBriefSerializer(
|
||||
source='item.location', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
customer_detail = CompanyBriefSerializer(
|
||||
source='line.order.customer', many=False, read_only=True, allow_null=True
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='line.order.customer', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
shipment_detail = SalesOrderShipmentSerializer(
|
||||
@@ -1851,7 +1777,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
|
||||
@register_importer()
|
||||
class SalesOrderExtraLineSerializer(
|
||||
AbstractExtraLineSerializer, InvenTreeModelSerializer
|
||||
FilterableSerializerMixin, AbstractExtraLineSerializer, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for a SalesOrderExtraLine object."""
|
||||
|
||||
@@ -1860,13 +1786,16 @@ class SalesOrderExtraLineSerializer(
|
||||
|
||||
model = order.models.SalesOrderExtraLine
|
||||
|
||||
order_detail = SalesOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
order_detail = enable_filter(
|
||||
SalesOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_importer()
|
||||
class ReturnOrderSerializer(
|
||||
FilterableSerializerMixin,
|
||||
NotesFieldMixin,
|
||||
InvenTreeCustomStatusSerializerMixin,
|
||||
AbstractOrderSerializer,
|
||||
@@ -1879,7 +1808,6 @@ class ReturnOrderSerializer(
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.ReturnOrder
|
||||
|
||||
fields = AbstractOrderSerializer.order_fields([
|
||||
'complete_date',
|
||||
'customer',
|
||||
@@ -1888,18 +1816,8 @@ class ReturnOrderSerializer(
|
||||
'order_currency',
|
||||
'total_price',
|
||||
])
|
||||
|
||||
read_only_fields = ['creation_date']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
customer_detail = kwargs.pop('customer_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if customer_detail is not True and not isGeneratingSchema():
|
||||
self.fields.pop('customer_detail', None)
|
||||
|
||||
def skip_create_fields(self):
|
||||
"""Skip these fields when instantiating a new object."""
|
||||
fields = super().skip_create_fields()
|
||||
@@ -1929,8 +1847,10 @@ class ReturnOrderSerializer(
|
||||
|
||||
return queryset
|
||||
|
||||
customer_detail = CompanyBriefSerializer(
|
||||
source='customer', many=False, read_only=True, allow_null=True
|
||||
customer_detail = enable_filter(
|
||||
CompanyBriefSerializer(
|
||||
source='customer', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -2066,6 +1986,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
|
||||
|
||||
@register_importer()
|
||||
class ReturnOrderLineItemSerializer(
|
||||
FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
AbstractLineItemSerializer,
|
||||
InvenTreeModelSerializer,
|
||||
@@ -2076,7 +1997,6 @@ class ReturnOrderLineItemSerializer(
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.ReturnOrderLineItem
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'order',
|
||||
@@ -2096,40 +2016,26 @@ class ReturnOrderLineItemSerializer(
|
||||
'link',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer."""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
item_detail = kwargs.pop('item_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not order_detail:
|
||||
self.fields.pop('order_detail', None)
|
||||
|
||||
if not item_detail:
|
||||
self.fields.pop('item_detail', None)
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
order_detail = ReturnOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
order_detail = enable_filter(
|
||||
ReturnOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
quantity = serializers.FloatField(
|
||||
label=_('Quantity'), help_text=_('Quantity to return')
|
||||
)
|
||||
|
||||
item_detail = stock.serializers.StockItemSerializer(
|
||||
source='item', many=False, read_only=True, allow_null=True
|
||||
item_detail = enable_filter(
|
||||
stock.serializers.StockItemSerializer(
|
||||
source='item', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
part_detail = PartBriefSerializer(
|
||||
source='item.part', many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='item.part', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
price = InvenTreeMoneySerializer(allow_null=True)
|
||||
@@ -2138,7 +2044,7 @@ class ReturnOrderLineItemSerializer(
|
||||
|
||||
@register_importer()
|
||||
class ReturnOrderExtraLineSerializer(
|
||||
AbstractExtraLineSerializer, InvenTreeModelSerializer
|
||||
FilterableSerializerMixin, AbstractExtraLineSerializer, InvenTreeModelSerializer
|
||||
):
|
||||
"""Serializer for a ReturnOrderExtraLine object."""
|
||||
|
||||
@@ -2147,6 +2053,8 @@ class ReturnOrderExtraLineSerializer(
|
||||
|
||||
model = order.models.ReturnOrderExtraLine
|
||||
|
||||
order_detail = ReturnOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
order_detail = enable_filter(
|
||||
ReturnOrderSerializer(
|
||||
source='order', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
@@ -22,11 +22,9 @@ from sql_util.utils import SubqueryCount
|
||||
from taggit.serializers import TagListSerializerField
|
||||
|
||||
import common.currency
|
||||
import common.settings
|
||||
import company.models
|
||||
import InvenTree.helpers
|
||||
import InvenTree.serializers
|
||||
import InvenTree.status
|
||||
import part.filters as part_filters
|
||||
import part.helpers as part_helpers
|
||||
import stock.models
|
||||
@@ -34,6 +32,13 @@ import users.models
|
||||
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 users.serializers import UserSerializer
|
||||
|
||||
from .models import (
|
||||
@@ -58,7 +63,9 @@ logger = structlog.get_logger('inventree')
|
||||
|
||||
@register_importer()
|
||||
class CategorySerializer(
|
||||
DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeModelSerializer
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeModelSerializer,
|
||||
):
|
||||
"""Serializer for PartCategory."""
|
||||
|
||||
@@ -85,15 +92,6 @@ class CategorySerializer(
|
||||
]
|
||||
read_only_fields = ['level', 'pathstring']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Optionally add or remove extra fields."""
|
||||
path_detail = kwargs.pop('path_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not path_detail and not isGeneratingSchema():
|
||||
self.fields.pop('path', None)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate extra information to the queryset."""
|
||||
@@ -133,11 +131,14 @@ class CategorySerializer(
|
||||
"""Return True if the category is directly "starred" by the current user."""
|
||||
return category in self.context.get('starred_categories', [])
|
||||
|
||||
path = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
filter_name='path_detail',
|
||||
)
|
||||
|
||||
icon = serializers.CharField(
|
||||
@@ -312,7 +313,10 @@ class PartParameterTemplateSerializer(
|
||||
return queryset.annotate(parts=SubqueryCount('instances'))
|
||||
|
||||
|
||||
class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
class PartBriefSerializer(
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeModelSerializer,
|
||||
):
|
||||
"""Serializer for Part (brief detail)."""
|
||||
|
||||
class Meta:
|
||||
@@ -347,19 +351,8 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'pricing_min',
|
||||
'pricing_max',
|
||||
]
|
||||
|
||||
read_only_fields = ['barcode_hash']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom initialization routine for the PartBrief serializer."""
|
||||
pricing = kwargs.pop('pricing', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not pricing and not isGeneratingSchema():
|
||||
self.fields.pop('pricing_min', None)
|
||||
self.fields.pop('pricing_max', None)
|
||||
|
||||
category_default_location = serializers.IntegerField(
|
||||
read_only=True, allow_null=True
|
||||
)
|
||||
@@ -381,17 +374,27 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
# Pricing fields
|
||||
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
||||
pricing_min = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
||||
pricing_max = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
|
||||
@register_importer()
|
||||
class PartParameterSerializer(
|
||||
DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeModelSerializer
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeModelSerializer,
|
||||
):
|
||||
"""JSON serializers for the PartParameter model."""
|
||||
|
||||
@@ -412,28 +415,8 @@ class PartParameterSerializer(
|
||||
'updated_by',
|
||||
'updated_by_detail',
|
||||
]
|
||||
|
||||
read_only_fields = ['updated', 'updated_by']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom initialization method for the serializer.
|
||||
|
||||
Allows us to optionally include or exclude particular information
|
||||
"""
|
||||
template_detail = kwargs.pop('template_detail', True)
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if not template_detail:
|
||||
self.fields.pop('template_detail', None)
|
||||
|
||||
def save(self):
|
||||
"""Save the PartParameter instance."""
|
||||
instance = super().save()
|
||||
@@ -445,12 +428,15 @@ class PartParameterSerializer(
|
||||
|
||||
return instance
|
||||
|
||||
part_detail = PartBriefSerializer(
|
||||
source='part', many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(source='part', many=False, read_only=True, allow_null=True)
|
||||
)
|
||||
|
||||
template_detail = PartParameterTemplateSerializer(
|
||||
source='template', many=False, read_only=True, allow_null=True
|
||||
template_detail = enable_filter(
|
||||
PartParameterTemplateSerializer(
|
||||
source='template', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
updated_by_detail = UserSerializer(
|
||||
@@ -641,10 +627,12 @@ class DefaultLocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
@register_importer()
|
||||
class PartSerializer(
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTree.serializers.NotesFieldMixin,
|
||||
InvenTree.serializers.RemoteImageMixin,
|
||||
InvenTree.serializers.InvenTreeTagModelSerializer,
|
||||
InvenTree.serializers.InvenTreeTaggitSerializer,
|
||||
InvenTree.serializers.InvenTreeModelSerializer,
|
||||
):
|
||||
"""Serializer for complete detail information of a part.
|
||||
|
||||
@@ -727,7 +715,6 @@ class PartSerializer(
|
||||
'copy_category_parameters',
|
||||
'tags',
|
||||
]
|
||||
|
||||
read_only_fields = ['barcode_hash', 'creation_date', 'creation_user']
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
@@ -738,18 +725,19 @@ class PartSerializer(
|
||||
- Allows us to optionally pass extra fields based on the query.
|
||||
"""
|
||||
self.starred_parts = kwargs.pop('starred_parts', [])
|
||||
category_detail = kwargs.pop('category_detail', False)
|
||||
location_detail = kwargs.pop('location_detail', False)
|
||||
parameters = kwargs.pop('parameters', False)
|
||||
# category_detail = kwargs.pop('category_detail', False)
|
||||
# location_detail = kwargs.pop('location_detail', False)
|
||||
# parameters = kwargs.pop('parameters', False)
|
||||
create = kwargs.pop('create', False)
|
||||
pricing = kwargs.pop('pricing', True)
|
||||
path_detail = kwargs.pop('path_detail', False)
|
||||
# pricing = kwargs.pop('pricing', True)
|
||||
# path_detail = kwargs.pop('path_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
"""
|
||||
if not category_detail:
|
||||
self.fields.pop('category_detail', None)
|
||||
|
||||
@@ -762,6 +750,12 @@ class PartSerializer(
|
||||
if not path_detail:
|
||||
self.fields.pop('category_path', None)
|
||||
|
||||
if not pricing:
|
||||
self.fields.pop('pricing_min', None)
|
||||
self.fields.pop('pricing_max', None)
|
||||
self.fields.pop('pricing_updated', None)
|
||||
"""
|
||||
|
||||
if not create:
|
||||
# These fields are only used for the LIST API endpoint
|
||||
for f in self.skip_create_fields():
|
||||
@@ -770,11 +764,6 @@ class PartSerializer(
|
||||
continue
|
||||
self.fields.pop(f, None)
|
||||
|
||||
if not pricing:
|
||||
self.fields.pop('pricing_min', None)
|
||||
self.fields.pop('pricing_max', None)
|
||||
self.fields.pop('pricing_updated', None)
|
||||
|
||||
def get_api_url(self):
|
||||
"""Return the API url associated with this serializer."""
|
||||
return reverse_lazy('api-part-list')
|
||||
@@ -889,19 +878,27 @@ class PartSerializer(
|
||||
return part in self.starred_parts
|
||||
|
||||
# Extra detail for the category
|
||||
category_detail = CategorySerializer(
|
||||
source='category', many=False, read_only=True, allow_null=True
|
||||
category_detail = enable_filter(
|
||||
CategorySerializer(
|
||||
source='category', many=False, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
category_path = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
source='category.get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
category_path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='category.get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
filter_name='path_detail',
|
||||
)
|
||||
|
||||
default_location_detail = DefaultLocationSerializer(
|
||||
source='default_location', many=False, read_only=True, allow_null=True
|
||||
default_location_detail = enable_filter(
|
||||
DefaultLocationSerializer(
|
||||
source='default_location', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
filter_name='location_detail',
|
||||
)
|
||||
|
||||
category_name = serializers.CharField(
|
||||
@@ -1009,17 +1006,31 @@ class PartSerializer(
|
||||
)
|
||||
|
||||
# Pricing fields
|
||||
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
||||
pricing_min = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_min', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
||||
pricing_max = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_updated = serializers.DateTimeField(
|
||||
source='pricing_data.updated', allow_null=True, read_only=True
|
||||
pricing_updated = enable_filter(
|
||||
FilterableDateTimeField(
|
||||
source='pricing_data.updated', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
parameters = PartParameterSerializer(many=True, read_only=True, allow_null=True)
|
||||
parameters = enable_filter(
|
||||
PartParameterSerializer(many=True, read_only=True, allow_null=True)
|
||||
)
|
||||
|
||||
# Extra fields used only for creation of a new Part instance
|
||||
duplicate = DuplicatePartSerializer(
|
||||
@@ -1597,6 +1608,7 @@ class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer
|
||||
|
||||
model = BomItemSubstitute
|
||||
fields = ['pk', 'bom_item', 'part', 'part_detail']
|
||||
list_serializer_class = FilterableListSerializer
|
||||
|
||||
part_detail = PartBriefSerializer(
|
||||
source='part', read_only=True, many=False, pricing=False
|
||||
@@ -1605,7 +1617,9 @@ class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer
|
||||
|
||||
@register_importer()
|
||||
class BomItemSerializer(
|
||||
DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeModelSerializer
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeModelSerializer,
|
||||
):
|
||||
"""Serializer for BomItem object."""
|
||||
|
||||
@@ -1659,42 +1673,6 @@ class BomItemSerializer(
|
||||
'can_build',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Determine if extra detail fields are to be annotated on this serializer.
|
||||
|
||||
- part_detail and sub_part_detail serializers are only included if requested.
|
||||
- This saves a bunch of database requests
|
||||
"""
|
||||
can_build = kwargs.pop('can_build', True)
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
sub_part_detail = kwargs.pop('sub_part_detail', True)
|
||||
pricing = kwargs.pop('pricing', True)
|
||||
substitutes = kwargs.pop('substitutes', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if not sub_part_detail:
|
||||
self.fields.pop('sub_part_detail', None)
|
||||
|
||||
if not can_build:
|
||||
self.fields.pop('can_build')
|
||||
|
||||
if not substitutes:
|
||||
self.fields.pop('substitutes', None)
|
||||
|
||||
if not pricing:
|
||||
self.fields.pop('pricing_min', None)
|
||||
self.fields.pop('pricing_max', None)
|
||||
self.fields.pop('pricing_min_total', None)
|
||||
self.fields.pop('pricing_max_total', None)
|
||||
self.fields.pop('pricing_updated', None)
|
||||
|
||||
quantity = InvenTree.serializers.InvenTreeDecimalField(required=True)
|
||||
|
||||
setup_quantity = InvenTree.serializers.InvenTreeDecimalField(required=False)
|
||||
@@ -1718,12 +1696,18 @@ class BomItemSerializer(
|
||||
help_text=_('Select the parent assembly'),
|
||||
)
|
||||
|
||||
substitutes = BomItemSubstituteSerializer(
|
||||
many=True, read_only=True, allow_null=True
|
||||
substitutes = enable_filter(
|
||||
BomItemSubstituteSerializer(many=True, read_only=True, allow_null=True), True
|
||||
)
|
||||
|
||||
part_detail = PartBriefSerializer(
|
||||
source='part', label=_('Assembly'), many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='part',
|
||||
label=_('Assembly'),
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
)
|
||||
|
||||
sub_part = serializers.PrimaryKeyRelatedField(
|
||||
@@ -1732,12 +1716,15 @@ class BomItemSerializer(
|
||||
help_text=_('Select the component part'),
|
||||
)
|
||||
|
||||
sub_part_detail = PartBriefSerializer(
|
||||
source='sub_part',
|
||||
label=_('Component'),
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
sub_part_detail = enable_filter(
|
||||
PartBriefSerializer(
|
||||
source='sub_part',
|
||||
label=_('Component'),
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
on_order = serializers.FloatField(
|
||||
@@ -1748,28 +1735,42 @@ class BomItemSerializer(
|
||||
label=_('In Production'), read_only=True, allow_null=True
|
||||
)
|
||||
|
||||
can_build = serializers.FloatField(
|
||||
label=_('Can Build'), read_only=True, allow_null=True
|
||||
can_build = enable_filter(
|
||||
FilterableFloatField(label=_('Can Build'), read_only=True, allow_null=True),
|
||||
True,
|
||||
)
|
||||
|
||||
# Cached pricing fields
|
||||
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True
|
||||
pricing_min = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True
|
||||
pricing_max = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
pricing_min_total = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
allow_null=True, read_only=True
|
||||
pricing_min_total = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
pricing_max_total = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
allow_null=True, read_only=True
|
||||
pricing_max_total = enable_filter(
|
||||
InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
pricing_updated = serializers.DateTimeField(
|
||||
source='sub_part.pricing_data.updated', allow_null=True, read_only=True
|
||||
pricing_updated = enable_filter(
|
||||
FilterableDateTimeField(
|
||||
source='sub_part.pricing_data.updated', allow_null=True, read_only=True
|
||||
),
|
||||
True,
|
||||
filter_name='pricing',
|
||||
)
|
||||
|
||||
# Annotated fields for available stock
|
||||
|
@@ -1395,6 +1395,20 @@ class PartAPITest(PartAPITestBase):
|
||||
|
||||
self.assertIn('notes', response.data)
|
||||
|
||||
def test_output_options(self):
|
||||
"""Test the output options for PartList list."""
|
||||
self.run_output_test(
|
||||
reverse('api-part-list'),
|
||||
[
|
||||
('location_detail', 'default_location_detail'),
|
||||
'parameters',
|
||||
('path_detail', 'category_path'),
|
||||
# TODO re-enable ('pricing', 'pricing_min'),
|
||||
# TODO re-enable ('pricing', 'pricing_updated'),
|
||||
],
|
||||
assert_subset=True,
|
||||
)
|
||||
|
||||
|
||||
class PartCreationTests(PartAPITestBase):
|
||||
"""Tests for creating new Part instances via the API."""
|
||||
@@ -2668,7 +2682,13 @@ class BomItemTest(InvenTreeAPITestCase):
|
||||
"""Test that various output options work as expected."""
|
||||
self.run_output_test(
|
||||
reverse('api-bom-item-detail', kwargs={'pk': 3}),
|
||||
['can_build', 'part_detail', 'sub_part_detail'],
|
||||
[
|
||||
'can_build',
|
||||
'part_detail',
|
||||
'sub_part_detail',
|
||||
# TODO re-enable 'substitutes',
|
||||
# TODO re-enable ('pricing', 'pricing_min'),
|
||||
],
|
||||
)
|
||||
|
||||
def test_add_bom_item(self):
|
||||
|
@@ -32,11 +32,11 @@ from common.settings import get_global_setting
|
||||
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
||||
from importer.registry import register_importer
|
||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import (
|
||||
FilterableListField,
|
||||
InvenTreeCurrencySerializer,
|
||||
InvenTreeDecimalField,
|
||||
InvenTreeModelSerializer,
|
||||
enable_filter,
|
||||
)
|
||||
from users.serializers import UserSerializer
|
||||
|
||||
@@ -194,7 +194,9 @@ class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
@register_importer()
|
||||
class StockItemTestResultSerializer(
|
||||
DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeModelSerializer
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeModelSerializer,
|
||||
):
|
||||
"""Serializer for the StockItemTestResult model."""
|
||||
|
||||
@@ -202,7 +204,6 @@ class StockItemTestResultSerializer(
|
||||
"""Metaclass options."""
|
||||
|
||||
model = StockItemTestResult
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'stock_item',
|
||||
@@ -219,26 +220,11 @@ class StockItemTestResultSerializer(
|
||||
'template',
|
||||
'template_detail',
|
||||
]
|
||||
|
||||
read_only_fields = ['pk', 'user', 'date']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
template_detail = kwargs.pop('template_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if user_detail is not True:
|
||||
self.fields.pop('user_detail', None)
|
||||
|
||||
if template_detail is not True:
|
||||
self.fields.pop('template_detail', None)
|
||||
|
||||
user_detail = UserSerializer(source='user', read_only=True, allow_null=True)
|
||||
user_detail = enable_filter(
|
||||
UserSerializer(source='user', read_only=True, allow_null=True)
|
||||
)
|
||||
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=part_models.PartTestTemplate.objects.all(),
|
||||
@@ -249,8 +235,10 @@ class StockItemTestResultSerializer(
|
||||
label=_('Test template for this result'),
|
||||
)
|
||||
|
||||
template_detail = part_serializers.PartTestTemplateSerializer(
|
||||
source='template', read_only=True, allow_null=True
|
||||
template_detail = enable_filter(
|
||||
part_serializers.PartTestTemplateSerializer(
|
||||
source='template', read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(
|
||||
@@ -309,6 +297,7 @@ class StockItemTestResultSerializer(
|
||||
|
||||
@register_importer()
|
||||
class StockItemSerializer(
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTreeCustomStatusSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeTagModelSerializer,
|
||||
@@ -392,11 +381,6 @@ class StockItemSerializer(
|
||||
'part_detail',
|
||||
'location_detail',
|
||||
]
|
||||
|
||||
"""
|
||||
These fields are read-only in this context.
|
||||
They can be updated by accessing the appropriate API endpoints
|
||||
"""
|
||||
read_only_fields = [
|
||||
'allocated',
|
||||
'barcode_hash',
|
||||
@@ -404,43 +388,17 @@ class StockItemSerializer(
|
||||
'stocktake_user',
|
||||
'updated',
|
||||
]
|
||||
|
||||
"""
|
||||
Fields used when creating a stock item
|
||||
These fields are read-only in this context.
|
||||
They can be updated by accessing the appropriate API endpoints
|
||||
"""
|
||||
extra_kwargs = {
|
||||
'use_pack_size': {'write_only': True},
|
||||
'serial_numbers': {'write_only': True},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
location_detail = kwargs.pop('location_detail', True)
|
||||
supplier_part_detail = kwargs.pop('supplier_part_detail', True)
|
||||
path_detail = kwargs.pop('path_detail', False)
|
||||
|
||||
tests = kwargs.pop('tests', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if not part_detail:
|
||||
self.fields.pop('part_detail', None)
|
||||
|
||||
if not location_detail:
|
||||
self.fields.pop('location_detail', None)
|
||||
|
||||
if not supplier_part_detail:
|
||||
self.fields.pop('supplier_part_detail', None)
|
||||
|
||||
if not tests:
|
||||
self.fields.pop('tests', None)
|
||||
|
||||
if not path_detail:
|
||||
self.fields.pop('location_path', None)
|
||||
"""
|
||||
Fields used when creating a stock item
|
||||
"""
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(
|
||||
queryset=part_models.Part.objects.all(),
|
||||
@@ -457,11 +415,14 @@ class StockItemSerializer(
|
||||
help_text=_('Parent stock item'),
|
||||
)
|
||||
|
||||
location_path = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
source='location.get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
location_path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='location.get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
filter_name='path_detail',
|
||||
)
|
||||
|
||||
in_stock = serializers.BooleanField(read_only=True, label=_('In Stock'))
|
||||
@@ -613,32 +574,43 @@ class StockItemSerializer(
|
||||
)
|
||||
|
||||
# Optional detail fields, which can be appended via query parameters
|
||||
supplier_part_detail = 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,
|
||||
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,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
part_detail = part_serializers.PartBriefSerializer(
|
||||
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
||||
part_detail = enable_filter(
|
||||
part_serializers.PartBriefSerializer(
|
||||
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
location_detail = LocationBriefSerializer(
|
||||
label=_('Location'),
|
||||
source='location',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
location_detail = enable_filter(
|
||||
LocationBriefSerializer(
|
||||
label=_('Location'),
|
||||
source='location',
|
||||
many=False,
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
tests = StockItemTestResultSerializer(
|
||||
source='test_results', many=True, read_only=True, allow_null=True
|
||||
tests = enable_filter(
|
||||
StockItemTestResultSerializer(
|
||||
source='test_results', many=True, read_only=True, allow_null=True
|
||||
)
|
||||
)
|
||||
|
||||
quantity = InvenTreeDecimalField()
|
||||
@@ -1150,7 +1122,9 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
@register_importer()
|
||||
class LocationSerializer(
|
||||
DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeTagModelSerializer
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeTagModelSerializer,
|
||||
):
|
||||
"""Detailed information about a stock location."""
|
||||
|
||||
@@ -1180,18 +1154,8 @@ class LocationSerializer(
|
||||
'location_type_detail',
|
||||
'tags',
|
||||
]
|
||||
|
||||
read_only_fields = ['barcode_hash', 'icon', 'level', 'pathstring']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Optionally add or remove extra fields."""
|
||||
path_detail = kwargs.pop('path_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not path_detail and not isGeneratingSchema():
|
||||
self.fields.pop('path', None)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate extra information to the queryset."""
|
||||
@@ -1223,11 +1187,14 @@ class LocationSerializer(
|
||||
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
path = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
path = enable_filter(
|
||||
FilterableListField(
|
||||
child=serializers.DictField(),
|
||||
source='get_path',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
),
|
||||
filter_name='path_detail',
|
||||
)
|
||||
|
||||
# explicitly set this field, so it gets included for AutoSchema
|
||||
@@ -1241,7 +1208,9 @@ class LocationSerializer(
|
||||
|
||||
@register_importer()
|
||||
class StockTrackingSerializer(
|
||||
DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeModelSerializer
|
||||
InvenTree.serializers.FilterableSerializerMixin,
|
||||
DataImportExportSerializerMixin,
|
||||
InvenTree.serializers.InvenTreeModelSerializer,
|
||||
):
|
||||
"""Serializer for StockItemTracking model."""
|
||||
|
||||
@@ -1261,33 +1230,16 @@ class StockTrackingSerializer(
|
||||
'user',
|
||||
'user_detail',
|
||||
]
|
||||
|
||||
read_only_fields = ['date', 'user', 'label', 'tracking_type']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
item_detail = kwargs.pop('item_detail', False)
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isGeneratingSchema():
|
||||
return
|
||||
|
||||
if item_detail is not True:
|
||||
self.fields.pop('item_detail', None)
|
||||
|
||||
if user_detail is not True:
|
||||
self.fields.pop('user_detail', None)
|
||||
|
||||
label = serializers.CharField(read_only=True)
|
||||
|
||||
item_detail = StockItemSerializer(
|
||||
source='item', many=False, read_only=True, allow_null=True
|
||||
item_detail = enable_filter(
|
||||
StockItemSerializer(source='item', many=False, read_only=True, allow_null=True)
|
||||
)
|
||||
|
||||
user_detail = UserSerializer(
|
||||
source='user', many=False, read_only=True, allow_null=True
|
||||
user_detail = enable_filter(
|
||||
UserSerializer(source='user', many=False, read_only=True, allow_null=True)
|
||||
)
|
||||
|
||||
deltas = serializers.JSONField(read_only=True)
|
||||
@@ -1859,7 +1811,7 @@ class StockReturnSerializer(StockAdjustmentSerializer):
|
||||
)
|
||||
|
||||
|
||||
class StockItemSerialNumbersSerializer(InvenTreeModelSerializer):
|
||||
class StockItemSerialNumbersSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Serializer for extra serial number information about a stock item."""
|
||||
|
||||
class Meta:
|
||||
|
@@ -1,15 +1,19 @@
|
||||
"""DRF API serializers for the 'users' app."""
|
||||
|
||||
from django.contrib.auth.models import Group, Permission, User
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import InvenTreeModelSerializer
|
||||
from InvenTree.serializers import (
|
||||
FilterableListSerializer,
|
||||
FilterableSerializerMethodField,
|
||||
FilterableSerializerMixin,
|
||||
InvenTreeModelSerializer,
|
||||
enable_filter,
|
||||
)
|
||||
|
||||
from .models import ApiToken, Owner, RuleSet, UserProfile
|
||||
from .permissions import check_user_role
|
||||
@@ -49,6 +53,7 @@ class RuleSetSerializer(InvenTreeModelSerializer):
|
||||
'can_delete',
|
||||
]
|
||||
read_only_fields = ['pk', 'name', 'label', 'group']
|
||||
list_serializer_class = FilterableListSerializer
|
||||
|
||||
|
||||
class RoleSerializer(InvenTreeModelSerializer):
|
||||
@@ -173,8 +178,8 @@ 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'))
|
||||
|
||||
@@ -234,7 +239,7 @@ class ApiTokenSerializer(InvenTreeModelSerializer):
|
||||
user_detail = UserSerializer(source='user', read_only=True)
|
||||
|
||||
|
||||
class GroupSerializer(InvenTreeModelSerializer):
|
||||
class GroupSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
||||
"""Serializer for a 'Group'."""
|
||||
|
||||
class Meta:
|
||||
@@ -243,38 +248,25 @@ class GroupSerializer(InvenTreeModelSerializer):
|
||||
model = Group
|
||||
fields = ['pk', 'name', 'permissions', 'roles', 'users']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra fields as required."""
|
||||
role_detail = kwargs.pop('role_detail', False)
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
permission_detail = kwargs.pop('permission_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
try:
|
||||
if not isGeneratingSchema():
|
||||
if not permission_detail:
|
||||
self.fields.pop('permissions', None)
|
||||
if not role_detail:
|
||||
self.fields.pop('roles', None)
|
||||
if not user_detail:
|
||||
self.fields.pop('users', None)
|
||||
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
pass
|
||||
|
||||
permissions = serializers.SerializerMethodField(allow_null=True, read_only=True)
|
||||
permissions = enable_filter(
|
||||
FilterableSerializerMethodField(allow_null=True, read_only=True),
|
||||
filter_name='permission_detail',
|
||||
)
|
||||
|
||||
def get_permissions(self, group: Group) -> dict:
|
||||
"""Return a list of permissions associated with the group."""
|
||||
return generate_permission_dict(group.permissions.all())
|
||||
|
||||
roles = RuleSetSerializer(
|
||||
source='rule_sets', many=True, read_only=True, allow_null=True
|
||||
roles = enable_filter(
|
||||
RuleSetSerializer(
|
||||
source='rule_sets', many=True, read_only=True, allow_null=True
|
||||
),
|
||||
filter_name='role_detail',
|
||||
)
|
||||
|
||||
users = UserSerializer(
|
||||
source='user_set', many=True, read_only=True, allow_null=True
|
||||
users = enable_filter(
|
||||
UserSerializer(source='user_set', many=True, read_only=True, allow_null=True),
|
||||
filter_name='user_detail',
|
||||
)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user