mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-15 00:38:12 +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:
@@ -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