2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 03:26:45 +00:00

Check for schema generation state when removing fields (#9236)

* Ensure notes are not removed when generating schema

* Skip remaining conditional field removals when generating schema, remove removable fields from required lists

* Update API version, add schema gen state check for api-doc endpoint

* Add test for generate schema state

* Add test for schema postprocessing function

* Filter nullable + read_only fields out of schema required lists

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Joe Rogers 2025-03-15 13:53:30 +01:00 committed by GitHub
parent cdb445583b
commit d7aa5e45b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 316 additions and 75 deletions

View File

@ -1,13 +1,17 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 320
INVENTREE_API_VERSION = 321
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v321 - 2025-03-06 : https://github.com/inventree/InvenTree/pull/9236
- Adds conditionally-returned fields to the schema to match API behavior
- Removes required flag for nullable read-only fields to match API behavior
v320 - 2025-03-05 : https://github.com/inventree/InvenTree/pull/9243
- Link fields are now up to 2000 chars long

View File

@ -1,5 +1,6 @@
"""Functions to check if certain parts of InvenTree are ready."""
import inspect
import os
import sys
@ -44,6 +45,14 @@ def isRunningBackup():
)
def isGeneratingSchema():
"""Return true if schema generation is being executed."""
if 'schema' in sys.argv:
return True
return any('drf_spectacular' in frame.filename for frame in inspect.stack())
def isInWorkerThread():
"""Returns True if the current thread is a background worker thread."""
return 'qcluster' in sys.argv

View File

@ -0,0 +1,23 @@
"""Schema processing functions for cleaning up generated schema."""
def postprocess_required_nullable(result, generator, request, public):
"""Un-require nullable fields.
Read-only values are all marked as required by spectacular, but InvenTree doesn't always include them in the response. This removes them from the required list to allow responses lacking read-only nullable fields to validate against the schema.
"""
# Process schema section
schemas = result.get('components', {}).get('schemas', {})
for schema in schemas.values():
required_fields = schema.get('required', [])
properties = schema.get('properties', {})
# copy list to allow removing from it while iterating
for field in list(required_fields):
field_dict = properties.get(field, {})
if field_dict.get('readOnly') and field_dict.get('nullable'):
required_fields.remove(field)
if 'required' in schema and len(required_fields) == 0:
schema.pop('required')
return result

View File

@ -22,6 +22,7 @@ from rest_framework.utils import model_meta
from taggit.serializers import TaggitSerializer
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
@ -468,7 +469,10 @@ class NotesFieldMixin:
if hasattr(self, 'context'):
if view := self.context.get('view', None):
if issubclass(view.__class__, ListModelMixin):
if (
issubclass(view.__class__, ListModelMixin)
and not InvenTree.ready.isGeneratingSchema()
):
self.fields.pop('notes', None)

View File

@ -1423,6 +1423,10 @@ SPECTACULAR_SETTINGS = {
'VERSION': str(inventreeApiVersion()),
'SERVE_INCLUDE_SCHEMA': False,
'SCHEMA_PATH_PREFIX': '/api/',
'POSTPROCESSING_HOOKS': [
'drf_spectacular.hooks.postprocess_schema_enums',
'InvenTree.schema.postprocess_required_nullable',
],
}
if SITE_URL and not TESTING:

View File

@ -37,7 +37,7 @@ from InvenTree.unit_test import InvenTreeTestCase, in_env_context
from part.models import Part, PartCategory
from stock.models import StockItem, StockLocation
from . import config, helpers, ready, status, version
from . import config, helpers, ready, schema, status, version
from .tasks import offload_task
from .validators import validate_overage
@ -1116,6 +1116,10 @@ class TestStatus(TestCase):
"""Test isImportingData check."""
self.assertEqual(ready.isImportingData(), False)
def test_GeneratingSchema(self):
"""Test isGeneratingSchema check."""
self.assertEqual(ready.isGeneratingSchema(), False)
class TestSettings(InvenTreeTestCase):
"""Unit tests for settings."""
@ -1631,3 +1635,62 @@ class ClassProviderMixinTest(TestCase):
def test_get_is_builtin(self):
"""Test the get_is_builtin function."""
self.assertTrue(self.TestClass.get_is_builtin())
class SchemaPostprocessingTest(TestCase):
"""Tests for schema postprocessing functions."""
def create_result_structure(self):
"""Create a schema dict structure representative of the spectacular-generated on."""
return {
'openapi': {},
'info': {},
'paths': {},
'components': {
'examples': {},
'parameters': {},
'requestBodies': {},
'responses': {},
'schemas': {},
'securitySchemes': {},
},
'servers': {},
'externalDocs': {},
}
def test_postprocess_required_nullable(self):
"""Verify that only selected elements are removed from required list."""
result_in = self.create_result_structure()
schemas_in = result_in.get('components').get('schemas')
schemas_in['SalesOrder'] = {
'properties': {
'pk': {'type': 'integer', 'readOnly': True, 'title': 'ID'},
'customer_detail': {
'allOf': [{'$ref': '#/components/schemas/CompanyBrief'}],
'readOnly': True,
'nullable': True,
},
},
'required': ['customer_detail', 'pk'],
}
schemas_in['SalesOrderShipment'] = {
'properties': {
'order_detail': {
'allOf': [{'$ref': '#/components/schemas/SalesOrder'}],
'readOnly': True,
'nullable': True,
}
},
'required': ['order_detail'],
}
result_out = schema.postprocess_required_nullable(result_in, {}, {}, {})
schemas_out = result_out.get('components').get('schemas')
# only intended elements removed (read-only, required, and object type)
self.assertIn('pk', schemas_out.get('SalesOrder')['required'])
self.assertNotIn('customer_detail', schemas_out.get('SalesOrder')['required'])
# required key removed when empty
self.assertNotIn('required', schemas_out.get('SalesOrderShipment'))

View File

@ -32,6 +32,7 @@ from common.serializers import ProjectCodeSerializer
from common.settings import get_global_setting
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from importer.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import (
InvenTreeDecimalField,
InvenTreeModelSerializer,
@ -126,7 +127,9 @@ class BuildSerializer(
issued_by_detail = UserSerializer(source='issued_by', read_only=True)
responsible_detail = OwnerSerializer(source='responsible', read_only=True)
responsible_detail = OwnerSerializer(
source='responsible', read_only=True, allow_null=True
)
barcode_hash = serializers.CharField(read_only=True)
@ -177,6 +180,9 @@ class BuildSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not create:
self.fields.pop('create_child_builds', None)
@ -1204,6 +1210,9 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not part_detail:
self.fields.pop('part_detail', None)
@ -1246,11 +1255,12 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
source='stock_item.part',
many=False,
read_only=True,
allow_null=True,
pricing=False,
)
stock_item_detail = StockItemSerializerBrief(
source='stock_item', read_only=True, label=_('Stock Item')
source='stock_item', read_only=True, allow_null=True, label=_('Stock Item')
)
location = serializers.PrimaryKeyRelatedField(
@ -1258,11 +1268,18 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
)
location_detail = LocationBriefSerializer(
label=_('Location'), source='stock_item.location', read_only=True
label=_('Location'),
source='stock_item.location',
read_only=True,
allow_null=True,
)
build_detail = BuildSerializer(
label=_('Build'), source='build_line.build', many=False, read_only=True
label=_('Build'),
source='build_line.build',
many=False,
read_only=True,
allow_null=True,
)
supplier_part_detail = company.serializers.SupplierPartSerializer(
@ -1270,6 +1287,7 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
source='stock_item.supplier_part',
many=False,
read_only=True,
allow_null=True,
brief=True,
)
@ -1339,6 +1357,9 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not part_detail:
self.fields.pop('part_detail', None)

View File

@ -333,7 +333,9 @@ class ProjectCodeSerializer(DataImportExportSerializerMixin, InvenTreeModelSeria
model = common_models.ProjectCode
fields = ['pk', 'code', 'description', 'responsible', 'responsible_detail']
responsible_detail = OwnerSerializer(source='responsible', read_only=True)
responsible_detail = OwnerSerializer(
source='responsible', read_only=True, allow_null=True
)
@register_importer()

View File

@ -14,6 +14,7 @@ import part.filters
import part.serializers as part_serializers
from importer.mixins import DataImportExportSerializerMixin
from importer.registry import register_importer
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import (
InvenTreeCurrencySerializer,
InvenTreeDecimalField,
@ -256,6 +257,9 @@ class ManufacturerPartSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if part_detail is not True:
self.fields.pop('part_detail', None)
@ -266,14 +270,14 @@ class ManufacturerPartSerializer(
self.fields.pop('pretty_name', None)
part_detail = part_serializers.PartBriefSerializer(
source='part', many=False, read_only=True
source='part', many=False, read_only=True, allow_null=True
)
manufacturer_detail = CompanyBriefSerializer(
source='manufacturer', many=False, read_only=True
source='manufacturer', many=False, read_only=True, allow_null=True
)
pretty_name = serializers.CharField(read_only=True)
pretty_name = serializers.CharField(read_only=True, allow_null=True)
manufacturer = serializers.PrimaryKeyRelatedField(
queryset=Company.objects.filter(is_manufacturer=True)
@ -306,11 +310,11 @@ class ManufacturerPartParameterSerializer(
super().__init__(*args, **kwargs)
if not man_detail:
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
source='manufacturer_part', many=False, read_only=True, allow_null=True
)
@ -389,6 +393,9 @@ class SupplierPartSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if part_detail is not True:
self.fields.pop('part_detail', None)
@ -409,20 +416,28 @@ class SupplierPartSerializer(
self.fields.pop('availability_updated')
# Annotated field showing total in-stock quantity
in_stock = serializers.FloatField(read_only=True, label=_('In Stock'))
in_stock = serializers.FloatField(
read_only=True, allow_null=True, label=_('In Stock')
)
on_order = serializers.FloatField(read_only=True, label=_('On Order'))
on_order = serializers.FloatField(
read_only=True, allow_null=True, label=_('On Order')
)
available = serializers.FloatField(required=False, label=_('Available'))
pack_quantity_native = serializers.FloatField(read_only=True)
part_detail = part_serializers.PartBriefSerializer(
label=_('Part'), source='part', many=False, read_only=True
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
)
supplier_detail = CompanyBriefSerializer(
label=_('Supplier'), source='supplier', many=False, read_only=True
label=_('Supplier'),
source='supplier',
many=False,
read_only=True,
allow_null=True,
)
manufacturer_detail = CompanyBriefSerializer(
@ -430,9 +445,10 @@ class SupplierPartSerializer(
source='manufacturer_part.manufacturer',
many=False,
read_only=True,
allow_null=True,
)
pretty_name = serializers.CharField(read_only=True)
pretty_name = serializers.CharField(read_only=True, allow_null=True)
supplier = serializers.PrimaryKeyRelatedField(
label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
@ -443,6 +459,7 @@ class SupplierPartSerializer(
source='manufacturer_part',
part_detail=False,
read_only=True,
allow_null=True,
)
MPN = serializers.CharField(
@ -529,6 +546,9 @@ class SupplierPriceBreakSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not supplier_detail:
self.fields.pop('supplier_detail', None)
@ -553,10 +573,10 @@ class SupplierPriceBreakSerializer(
)
supplier_detail = CompanyBriefSerializer(
source='part.supplier', many=False, read_only=True
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
source='part', brief=True, many=False, read_only=True, allow_null=True
)

View File

@ -44,6 +44,7 @@ from InvenTree.helpers import (
normalize,
str2bool,
)
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import (
InvenTreeCurrencySerializer,
InvenTreeDecimalField,
@ -128,11 +129,13 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
reference = serializers.CharField(required=True)
# Detail for point-of-contact field
contact_detail = ContactSerializer(source='contact', many=False, read_only=True)
contact_detail = ContactSerializer(
source='contact', many=False, read_only=True, allow_null=True
)
# Detail for responsible field
responsible_detail = OwnerSerializer(
source='responsible', read_only=True, many=False
source='responsible', read_only=True, allow_null=True, many=False
)
project_code_label = serializers.CharField(
@ -149,7 +152,7 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
# Detail for address field
address_detail = AddressBriefSerializer(
source='address', many=False, read_only=True
source='address', many=False, read_only=True, allow_null=True
)
# Boolean field indicating if this order is overdue (Note: must be annotated)
@ -275,7 +278,7 @@ class AbstractExtraLineSerializer(
super().__init__(*args, **kwargs)
if order_detail is not True:
if order_detail is not True and not isGeneratingSchema():
self.fields.pop('order_detail', None)
quantity = serializers.FloatField()
@ -343,7 +346,7 @@ class PurchaseOrderSerializer(
super().__init__(*args, **kwargs)
if supplier_detail is not True:
if supplier_detail is not True and not isGeneratingSchema():
self.fields.pop('supplier_detail', None)
def skip_create_fields(self):
@ -384,7 +387,7 @@ class PurchaseOrderSerializer(
)
supplier_detail = CompanyBriefSerializer(
source='supplier', many=False, read_only=True
source='supplier', many=False, read_only=True, allow_null=True
)
@ -518,6 +521,9 @@ class PurchaseOrderLineItemSerializer(
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)
@ -608,11 +614,11 @@ class PurchaseOrderLineItemSerializer(
total_price = serializers.FloatField(read_only=True)
part_detail = PartBriefSerializer(
source='get_base_part', many=False, read_only=True
source='get_base_part', many=False, read_only=True, allow_null=True
)
supplier_part_detail = SupplierPartSerializer(
source='part', brief=True, many=False, read_only=True
source='part', brief=True, many=False, read_only=True, allow_null=True
)
purchase_price = InvenTreeMoneySerializer(allow_null=True)
@ -633,7 +639,9 @@ class PurchaseOrderLineItemSerializer(
help_text=_('Purchase price currency')
)
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
order_detail = PurchaseOrderSerializer(
source='order', read_only=True, allow_null=True, many=False
)
merge_items = serializers.BooleanField(
label=_('Merge Items'),
@ -699,7 +707,9 @@ class PurchaseOrderExtraLineSerializer(
):
"""Serializer for a PurchaseOrderExtraLine object."""
order_detail = PurchaseOrderSerializer(source='order', many=False, read_only=True)
order_detail = PurchaseOrderSerializer(
source='order', many=False, read_only=True, allow_null=True
)
class Meta(AbstractExtraLineMeta):
"""Metaclass options."""
@ -1025,7 +1035,7 @@ class SalesOrderSerializer(
super().__init__(*args, **kwargs)
if customer_detail is not True:
if customer_detail is not True and not isGeneratingSchema():
self.fields.pop('customer_detail', None)
def skip_create_fields(self):
@ -1069,7 +1079,7 @@ class SalesOrderSerializer(
return queryset
customer_detail = CompanyBriefSerializer(
source='customer', many=False, read_only=True
source='customer', many=False, read_only=True, allow_null=True
)
shipments_count = serializers.IntegerField(read_only=True, label=_('Shipments'))
@ -1135,6 +1145,9 @@ class SalesOrderLineItemSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if part_detail is not True:
self.fields.pop('part_detail', None)
@ -1233,10 +1246,14 @@ class SalesOrderLineItemSerializer(
return queryset
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
order_detail = SalesOrderSerializer(
source='order', many=False, read_only=True, allow_null=True
)
part_detail = PartBriefSerializer(
source='part', many=False, read_only=True, allow_null=True
)
customer_detail = CompanyBriefSerializer(
source='order.customer', many=False, read_only=True
source='order.customer', many=False, read_only=True, allow_null=True
)
# Annotated fields
@ -1289,7 +1306,7 @@ class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
super().__init__(*args, **kwargs)
if not order_detail:
if not order_detail and not isGeneratingSchema():
self.fields.pop('order_detail', None)
@staticmethod
@ -1306,7 +1323,9 @@ class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
read_only=True, label=_('Allocated Items')
)
order_detail = SalesOrderSerializer(source='order', read_only=True, many=False)
order_detail = SalesOrderSerializer(
source='order', read_only=True, allow_null=True, many=False
)
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
@ -1352,6 +1371,9 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not order_detail:
self.fields.pop('order_detail', None)
@ -1378,16 +1400,20 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
)
# Extra detail fields
order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True)
part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
order_detail = 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
)
item_detail = stock.serializers.StockItemSerializerBrief(
source='item', many=False, read_only=True
source='item', many=False, read_only=True, allow_null=True
)
location_detail = stock.serializers.LocationBriefSerializer(
source='item.location', many=False, read_only=True
source='item.location', many=False, read_only=True, allow_null=True
)
customer_detail = CompanyBriefSerializer(
source='line.order.customer', many=False, read_only=True
source='line.order.customer', many=False, read_only=True, allow_null=True
)
shipment_detail = SalesOrderShipmentSerializer(
@ -1833,7 +1859,9 @@ class SalesOrderExtraLineSerializer(
model = order.models.SalesOrderExtraLine
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
order_detail = SalesOrderSerializer(
source='order', many=False, read_only=True, allow_null=True
)
@register_importer()
@ -1869,7 +1897,7 @@ class ReturnOrderSerializer(
super().__init__(*args, **kwargs)
if customer_detail is not True:
if customer_detail is not True and not isGeneratingSchema():
self.fields.pop('customer_detail', None)
def skip_create_fields(self):
@ -1902,7 +1930,7 @@ class ReturnOrderSerializer(
return queryset
customer_detail = CompanyBriefSerializer(
source='customer', many=False, read_only=True
source='customer', many=False, read_only=True, allow_null=True
)
@ -2081,6 +2109,9 @@ class ReturnOrderLineItemSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not order_detail:
self.fields.pop('order_detail', None)
@ -2090,17 +2121,21 @@ class ReturnOrderLineItemSerializer(
if not part_detail:
self.fields.pop('part_detail', None)
order_detail = ReturnOrderSerializer(source='order', many=False, read_only=True)
order_detail = 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
source='item', many=False, read_only=True, allow_null=True
)
part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
part_detail = PartBriefSerializer(
source='item.part', many=False, read_only=True, allow_null=True
)
price = InvenTreeMoneySerializer(allow_null=True)
price_currency = InvenTreeCurrencySerializer(help_text=_('Line price currency'))
@ -2117,4 +2152,6 @@ class ReturnOrderExtraLineSerializer(
model = order.models.ReturnOrderExtraLine
order_detail = ReturnOrderSerializer(source='order', many=False, read_only=True)
order_detail = ReturnOrderSerializer(
source='order', many=False, read_only=True, allow_null=True
)

View File

@ -35,6 +35,7 @@ import users.models
from build.status_codes import BuildStatusGroups
from importer.mixins import DataImportExportSerializerMixin
from importer.registry import register_importer
from InvenTree.ready import isGeneratingSchema
from InvenTree.tasks import offload_task
from users.serializers import UserSerializer
@ -94,7 +95,7 @@ class CategorySerializer(
super().__init__(*args, **kwargs)
if not path_detail:
if not path_detail and not isGeneratingSchema():
self.fields.pop('path', None)
def get_starred(self, category) -> bool:
@ -133,7 +134,10 @@ class CategorySerializer(
starred = serializers.SerializerMethodField()
path = serializers.ListField(
child=serializers.DictField(), source='get_path', read_only=True
child=serializers.DictField(),
source='get_path',
read_only=True,
allow_null=True,
)
icon = serializers.CharField(
@ -383,7 +387,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
super().__init__(*args, **kwargs)
if not pricing:
if not pricing and not isGeneratingSchema():
self.fields.pop('pricing_min', None)
self.fields.pop('pricing_max', None)
@ -444,15 +448,20 @@ class PartParameterSerializer(
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)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
part_detail = PartBriefSerializer(
source='part', many=False, read_only=True, allow_null=True
)
template_detail = PartParameterTemplateSerializer(
source='template', many=False, read_only=True
source='template', many=False, read_only=True, allow_null=True
)
@ -782,6 +791,9 @@ class PartSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not category_detail:
self.fields.pop('category_detail', None)
@ -923,14 +935,19 @@ class PartSerializer(
return part in self.starred_parts
# Extra detail for the category
category_detail = CategorySerializer(source='category', many=False, read_only=True)
category_detail = 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
child=serializers.DictField(),
source='category.get_path',
read_only=True,
allow_null=True,
)
default_location_detail = DefaultLocationSerializer(
source='default_location', many=False, read_only=True
source='default_location', many=False, read_only=True, allow_null=True
)
category_name = serializers.CharField(
@ -1003,7 +1020,7 @@ class PartSerializer(
source='pricing_data.updated', allow_null=True, read_only=True
)
parameters = PartParameterSerializer(many=True, read_only=True)
parameters = PartParameterSerializer(many=True, read_only=True, allow_null=True)
# Extra fields used only for creation of a new Part instance
duplicate = DuplicatePartSerializer(
@ -1617,6 +1634,9 @@ class BomItemSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not part_detail:
self.fields.pop('part_detail', None)
@ -1648,10 +1668,12 @@ class BomItemSerializer(
help_text=_('Select the parent assembly'),
)
substitutes = BomItemSubstituteSerializer(many=True, read_only=True)
substitutes = BomItemSubstituteSerializer(
many=True, read_only=True, allow_null=True
)
part_detail = PartBriefSerializer(
source='part', label=_('Assembly'), many=False, read_only=True
source='part', label=_('Assembly'), many=False, read_only=True, allow_null=True
)
sub_part = serializers.PrimaryKeyRelatedField(
@ -1661,7 +1683,11 @@ class BomItemSerializer(
)
sub_part_detail = PartBriefSerializer(
source='sub_part', label=_('Component'), many=False, read_only=True
source='sub_part',
label=_('Component'),
many=False,
read_only=True,
allow_null=True,
)
on_order = serializers.FloatField(label=_('On Order'), read_only=True)
@ -1867,7 +1893,9 @@ class CategoryParameterTemplateSerializer(
source='parameter_template', many=False, read_only=True
)
category_detail = CategorySerializer(source='category', many=False, read_only=True)
category_detail = CategorySerializer(
source='category', many=False, read_only=True, allow_null=True
)
class PartCopyBOMSerializer(serializers.Serializer):

View File

@ -30,6 +30,7 @@ from common.settings import get_global_setting
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from importer.mixins import DataImportExportSerializerMixin
from importer.registry import register_importer
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField
from users.serializers import UserSerializer
@ -218,13 +219,16 @@ class StockItemTestResultSerializer(
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)
user_detail = UserSerializer(source='user', read_only=True, allow_null=True)
template = serializers.PrimaryKeyRelatedField(
queryset=part_models.PartTestTemplate.objects.all(),
@ -236,7 +240,7 @@ class StockItemTestResultSerializer(
)
template_detail = part_serializers.PartTestTemplateSerializer(
source='template', read_only=True
source='template', read_only=True, allow_null=True
)
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(
@ -442,6 +446,9 @@ class StockItemSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not part_detail:
self.fields.pop('part_detail', None)
@ -473,7 +480,10 @@ class StockItemSerializer(
)
location_path = serializers.ListField(
child=serializers.DictField(), source='location.get_path', read_only=True
child=serializers.DictField(),
source='location.get_path',
read_only=True,
allow_null=True,
)
in_stock = serializers.BooleanField(read_only=True, label=_('In Stock'))
@ -617,18 +627,23 @@ class StockItemSerializer(
part_detail=False,
many=False,
read_only=True,
allow_null=True,
)
part_detail = part_serializers.PartBriefSerializer(
label=_('Part'), source='part', many=False, read_only=True
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
)
location_detail = LocationBriefSerializer(
label=_('Location'), source='location', many=False, read_only=True
label=_('Location'),
source='location',
many=False,
read_only=True,
allow_null=True,
)
tests = StockItemTestResultSerializer(
source='test_results', many=True, read_only=True
source='test_results', many=True, read_only=True, allow_null=True
)
quantity = InvenTreeDecimalField()
@ -1184,7 +1199,7 @@ class LocationSerializer(
super().__init__(*args, **kwargs)
if not path_detail:
if not path_detail and not isGeneratingSchema():
self.fields.pop('path', None)
@staticmethod
@ -1219,7 +1234,10 @@ class LocationSerializer(
tags = TagListSerializerField(required=False)
path = serializers.ListField(
child=serializers.DictField(), source='get_path', read_only=True
child=serializers.DictField(),
source='get_path',
read_only=True,
allow_null=True,
)
# explicitly set this field, so it gets included for AutoSchema
@ -1263,6 +1281,9 @@ class StockTrackingSerializer(
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if item_detail is not True:
self.fields.pop('item_detail', None)
@ -1271,9 +1292,13 @@ class StockTrackingSerializer(
label = serializers.CharField(read_only=True)
item_detail = StockItemSerializerBrief(source='item', many=False, read_only=True)
item_detail = StockItemSerializerBrief(
source='item', many=False, read_only=True, allow_null=True
)
user_detail = UserSerializer(source='user', many=False, read_only=True)
user_detail = UserSerializer(
source='user', many=False, read_only=True, allow_null=True
)
deltas = serializers.JSONField(read_only=True)

View File

@ -8,6 +8,7 @@ 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 .models import ApiToken, Owner, RuleSet, UserProfile, check_user_role
@ -44,12 +45,12 @@ class GroupSerializer(InvenTreeModelSerializer):
super().__init__(*args, **kwargs)
try:
if not permission_detail:
if not permission_detail and not isGeneratingSchema():
self.fields.pop('permissions', None)
except AppRegistryNotReady:
pass
permissions = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField(allow_null=True)
def get_permissions(self, group: Group):
"""Return a list of permissions associated with the group."""
@ -74,7 +75,7 @@ class RoleSerializer(InvenTreeModelSerializer):
user = serializers.IntegerField(source='pk')
roles = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField(allow_null=True)
def get_roles(self, user: User) -> dict:
"""Roles associated with the user."""