mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +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:
parent
cdb445583b
commit
d7aa5e45b9
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
23
src/backend/InvenTree/InvenTree/schema.py
Normal file
23
src/backend/InvenTree/InvenTree/schema.py
Normal 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
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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'))
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user