mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +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:
		@@ -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."""
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user