2
0
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:
Matthias Mair
2025-10-18 00:17:01 +02:00
committed by GitHub
parent d34f44221e
commit 5b7820eef0
11 changed files with 748 additions and 723 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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."""

View File

@@ -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
)
)

View File

@@ -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
)
)

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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',
)