Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
21e4370
Lazy evaluation of optional serializer fields
SchrodingersGat Jan 1, 2026
5a166f8
Refactor BuildLineSerializer class
SchrodingersGat Jan 1, 2026
f798307
Simplify gathering
SchrodingersGat Jan 1, 2026
b397ab9
Refactor BuildSerializer
SchrodingersGat Jan 1, 2026
09d35d2
Refactor other Build serializers
SchrodingersGat Jan 1, 2026
8f47288
Refactor Part serializers
SchrodingersGat Jan 1, 2026
70489cd
Refactoring more serializers to use OptionalField
SchrodingersGat Jan 1, 2026
25dd239
More refactoring
SchrodingersGat Jan 1, 2026
e71268a
Cleanup for mixin class
SchrodingersGat Jan 1, 2026
34a7474
Ensure any optional fields we added in are not missed
SchrodingersGat Jan 1, 2026
dcf6a55
Fixes
SchrodingersGat Jan 1, 2026
31c21a5
Rehydrate optional fields for metadata
SchrodingersGat Jan 1, 2026
b62a89a
Add TreePathSerializer class
SchrodingersGat Jan 1, 2026
fc6bb24
Further improvements:
SchrodingersGat Jan 2, 2026
6ad980a
Adjust unit tests
SchrodingersGat Jan 2, 2026
9e46356
Fix for "build_relational_field"
SchrodingersGat Jan 2, 2026
cddf922
Fix case where multiple fields can share same filter
SchrodingersGat Jan 2, 2026
61251f4
additional unit tests
SchrodingersGat Jan 2, 2026
0386a6a
Bump API version
SchrodingersGat Jan 2, 2026
b18a3e3
Remove top-level detection
SchrodingersGat Jan 2, 2026
ea049ab
Cache serializer to prevent multiple __init__ calls
SchrodingersGat Jan 2, 2026
4785f79
Revert caching change
SchrodingersGat Jan 2, 2026
7123efc
Simplify field removal
SchrodingersGat Jan 2, 2026
95e0aa8
Adjust unit test
SchrodingersGat Jan 2, 2026
a18dffe
Remove docstring comment which is no longer true
SchrodingersGat Jan 3, 2026
30f7760
Merge branch 'master' into filter-refactor
SchrodingersGat Jan 3, 2026
cae2644
Ensure read-only fields are skipped for data import
SchrodingersGat Jan 3, 2026
8e77006
Use SAFE_METHODS
SchrodingersGat Jan 3, 2026
853726e
Do not convert to lowercase
SchrodingersGat Jan 3, 2026
61afb97
Updated docstring
SchrodingersGat Jan 4, 2026
fa3f080
Remove FilterableSerializerField mixin
SchrodingersGat Jan 4, 2026
4bbd3dd
Merge branch 'master' into filter-refactor
SchrodingersGat Jan 4, 2026
4244367
Ensure all fields are returned when generating schema
SchrodingersGat Jan 4, 2026
eb8ce92
Fix order of operations
SchrodingersGat Jan 4, 2026
f8cab0e
Merge branch 'master' into filter-refactor
matmair Jan 6, 2026
3793a75
Merge commit 'e1b5fbd38da3b6e7e96272532278fa9f52da5e69' into filter-r…
SchrodingersGat Jan 10, 2026
d07a77d
Add assertion to unit test
SchrodingersGat Jan 10, 2026
e84b50e
Merge branch 'master' into filter-refactor
matmair Jan 14, 2026
e688e4c
Merge branch 'master' into filter-refactor
matmair Jan 16, 2026
65b944e
Merge branch 'master' into filter-refactor
matmair Jan 16, 2026
e75ebd4
Merge branch 'master' into filter-refactor
SchrodingersGat Jan 17, 2026
111eb87
Merge branch 'master' into filter-refactor
matmair Jan 27, 2026
9b3a40b
Merge branch 'master' into filter-refactor
matmair Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 165 additions & 149 deletions src/backend/InvenTree/InvenTree/serializers.py

Large diffs are not rendered by default.

44 changes: 16 additions & 28 deletions src/backend/InvenTree/InvenTree/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import InvenTree.serializers
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin
from InvenTree.serializers import OptionalField
from InvenTree.unit_test import InvenTreeAPITestCase
from InvenTree.urls import backendpatterns

Expand All @@ -25,21 +26,25 @@ class Meta:
fields = ['field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'id']

field_a = SerializerMethodField(method_name='sample')
field_b = InvenTree.serializers.enable_filter(
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample')
field_b = OptionalField(
serializer_class=InvenTree.serializers.FilterableSerializerMethodField,
serializer_kwargs={'method_name': 'sample'},
)
field_c = InvenTree.serializers.enable_filter(
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
True,
field_c = OptionalField(
serializer_class=InvenTree.serializers.FilterableSerializerMethodField,
serializer_kwargs={'method_name': 'sample'},
default_include=True,
filter_name='crazy_name',
)
field_d = InvenTree.serializers.enable_filter(
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
True,
field_d = OptionalField(
serializer_class=InvenTree.serializers.FilterableSerializerMethodField,
serializer_kwargs={'method_name': 'sample'},
default_include=True,
filter_name='crazy_name',
)
field_e = InvenTree.serializers.enable_filter(
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample'),
field_e = OptionalField(
serializer_class=InvenTree.serializers.FilterableSerializerMethodField,
serializer_kwargs={'method_name': 'sample'},
filter_name='field_e',
filter_by_query=False,
)
Expand Down Expand Up @@ -107,24 +112,7 @@ def test_basic_setup(self):
self.assertContains(response, 'field_d')
self.assertNotContains(response, 'field_e')

def test_failiure_enable_filter(self):
"""Test sanity check for enable_filter."""
# Allowed usage
field_b = InvenTree.serializers.enable_filter( # noqa: F841
InvenTree.serializers.FilterableSerializerMethodField(method_name='sample')
)

# Disallowed usage
with self.assertRaises(Exception) as cm:
field_a = InvenTree.serializers.enable_filter( # noqa: F841
SerializerMethodField(method_name='sample')
)
self.assertIn(
'INVE-I2: `enable_filter` can only be applied to serializer fields',
str(cm.exception),
)

def test_failiure_FilterableSerializerMixin(self):
def test_failure_FilterableSerializerMixin(self):
"""Test failure case for FilteredSerializerMixin."""

class BadSerializer(
Expand Down
255 changes: 137 additions & 118 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
InvenTreeDecimalField,
InvenTreeModelSerializer,
NotesFieldMixin,
enable_filter,
OptionalField,
)
from InvenTree.tasks import offload_task
from stock.generators import generate_batch_code
Expand Down Expand Up @@ -119,9 +119,10 @@ class Meta:

status_text = serializers.CharField(source='get_status_display', read_only=True)

part_detail = enable_filter(
part_serializers.PartBriefSerializer(source='part', many=False, read_only=True),
True,
part_detail = OptionalField(
serializer_class=part_serializers.PartBriefSerializer,
serializer_kwargs={'source': 'part', 'many': False, 'read_only': True},
default_include=True,
prefetch_fields=['part', 'part__category', 'part__pricing_data'],
)

Expand All @@ -135,16 +136,22 @@ class Meta:

overdue = serializers.BooleanField(read_only=True, default=False)

issued_by_detail = enable_filter(
UserSerializer(source='issued_by', read_only=True),
True,
issued_by_detail = OptionalField(
serializer_class=UserSerializer,
serializer_kwargs={'source': 'issued_by', 'read_only': True},
default_include=True,
filter_name='user_detail',
prefetch_fields=['issued_by'],
)

responsible_detail = enable_filter(
OwnerSerializer(source='responsible', read_only=True, allow_null=True),
True,
responsible_detail = OptionalField(
serializer_class=OwnerSerializer,
serializer_kwargs={
'source': 'responsible',
'read_only': True,
'allow_null': True,
},
default_include=True,
filter_name='user_detail',
prefetch_fields=['responsible'],
)
Expand Down Expand Up @@ -1210,31 +1217,33 @@ class Meta:
)

# Extra (optional) detail fields
part_detail = enable_filter(
part_serializers.PartBriefSerializer(
label=_('Part'),
source='stock_item.part',
many=False,
read_only=True,
allow_null=True,
pricing=False,
),
True,
part_detail = OptionalField(
serializer_class=part_serializers.PartBriefSerializer,
serializer_kwargs={
'label': _('Part'),
'source': 'stock_item.part',
'many': False,
'read_only': True,
'allow_null': True,
'pricing': False,
},
default_include=True,
prefetch_fields=['stock_item__part'],
)

stock_item_detail = enable_filter(
StockItemSerializer(
source='stock_item',
read_only=True,
allow_null=True,
label=_('Stock Item'),
part_detail=False,
location_detail=False,
supplier_part_detail=False,
path_detail=False,
),
True,
stock_item_detail = OptionalField(
serializer_class=StockItemSerializer,
serializer_kwargs={
'source': 'stock_item',
'read_only': True,
'allow_null': True,
'label': _('Stock Item'),
'part_detail': False,
'location_detail': False,
'supplier_part_detail': False,
'path_detail': False,
},
default_include=True,
filter_name='stock_detail',
prefetch_fields=[
'stock_item',
Expand All @@ -1248,26 +1257,28 @@ class Meta:
label=_('Location'), source='stock_item.location', many=False, read_only=True
)

location_detail = enable_filter(
LocationBriefSerializer(
label=_('Location'),
source='stock_item.location',
read_only=True,
allow_null=True,
),
True,
location_detail = OptionalField(
serializer_class=LocationBriefSerializer,
serializer_kwargs={
'label': _('Location'),
'source': 'stock_item.location',
'read_only': True,
'allow_null': True,
},
default_include=True,
prefetch_fields=['stock_item__location'],
)

build_detail = enable_filter(
BuildSerializer(
label=_('Build'),
source='build_line.build',
many=False,
read_only=True,
allow_null=True,
),
True,
build_detail = OptionalField(
serializer_class=BuildSerializer,
serializer_kwargs={
'label': _('Build'),
'source': 'build_line.build',
'many': False,
'read_only': True,
'allow_null': True,
},
default_include=True,
prefetch_fields=[
'build_line__build',
'build_line__build__part',
Expand All @@ -1278,16 +1289,17 @@ class Meta:
],
)

supplier_part_detail = enable_filter(
company.serializers.SupplierPartSerializer(
label=_('Supplier Part'),
source='stock_item.supplier_part',
many=False,
read_only=True,
allow_null=True,
brief=True,
),
False,
supplier_part_detail = OptionalField(
serializer_class=company.serializers.SupplierPartSerializer,
serializer_kwargs={
'label': _('Supplier Part'),
'source': 'stock_item.supplier_part',
'many': False,
'read_only': True,
'allow_null': True,
'brief': True,
},
default_include=False,
prefetch_fields=[
'stock_item__supplier_part',
'stock_item__supplier_part__supplier',
Expand Down Expand Up @@ -1377,9 +1389,10 @@ class Meta:
read_only=True,
)

allocations = enable_filter(
BuildItemSerializer(many=True, read_only=True, build_detail=False),
True,
allocations = OptionalField(
serializer_class=BuildItemSerializer,
serializer_kwargs={'many': True, 'read_only': True, 'build_detail': False},
default_include=True,
prefetch_fields=[
'allocations',
'allocations__stock_item',
Expand Down Expand Up @@ -1420,71 +1433,77 @@ class Meta:
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)

# Foreign key fields
bom_item_detail = enable_filter(
part_serializers.BomItemSerializer(
label=_('BOM Item'),
source='bom_item',
many=False,
read_only=True,
pricing=False,
substitutes=False,
sub_part_detail=False,
part_detail=False,
can_build=False,
),
False,
prefetch_fields=['bom_item'],
)

assembly_detail = enable_filter(
part_serializers.PartBriefSerializer(
label=_('Assembly'),
source='bom_item.part',
many=False,
read_only=True,
allow_null=True,
pricing=False,
),
False,
bom_item_detail = OptionalField(
serializer_class=part_serializers.BomItemSerializer,
serializer_kwargs={
'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,
},
default_include=False,
prefetch_fields=['bom_item', 'bom_item__substitutes'],
)

assembly_detail = OptionalField(
serializer_class=part_serializers.PartBriefSerializer,
serializer_kwargs={
'label': _('Assembly'),
'source': 'bom_item.part',
'many': False,
'read_only': True,
'allow_null': True,
'pricing': False,
},
default_include=False,
prefetch_fields=['bom_item__part', 'bom_item__part__pricing_data'],
)

part_detail = enable_filter(
part_serializers.PartBriefSerializer(
label=_('Part'),
source='bom_item.sub_part',
many=False,
read_only=True,
pricing=False,
),
False,
part_detail = OptionalField(
serializer_class=part_serializers.PartBriefSerializer,
serializer_kwargs={
'label': _('Part'),
'source': 'bom_item.sub_part',
'many': False,
'read_only': True,
'pricing': False,
},
default_include=False,
prefetch_fields=['bom_item__sub_part', 'bom_item__sub_part__pricing_data'],
)

category_detail = enable_filter(
part_serializers.CategorySerializer(
label=_('Category'),
source='bom_item.sub_part.category',
many=False,
read_only=True,
allow_null=True,
),
False,
category_detail = OptionalField(
serializer_class=part_serializers.CategorySerializer,
serializer_kwargs={
'label': _('Category'),
'source': 'bom_item.sub_part.category',
'many': False,
'read_only': True,
'allow_null': True,
'path_detail': False,
},
default_include=False,
prefetch_fields=['bom_item__sub_part__category'],
)

build_detail = enable_filter(
BuildSerializer(
label=_('Build'),
source='build',
many=False,
read_only=True,
allow_null=True,
part_detail=False,
user_detail=False,
project_code_detail=False,
),
True,
build_detail = OptionalField(
serializer_class=BuildSerializer,
serializer_kwargs={
'label': _('Build'),
'source': 'build',
'many': False,
'read_only': True,
'allow_null': True,
'part_detail': False,
'user_detail': False,
'project_code_detail': False,
},
default_include=True,
)

# Annotated (calculated) fields
Expand Down
Loading
Loading