diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py
index 2d4fb95024..a526cf152e 100644
--- a/InvenTree/InvenTree/metadata.py
+++ b/InvenTree/InvenTree/metadata.py
@@ -10,6 +10,7 @@ from rest_framework.utils import model_meta
 import InvenTree.permissions
 import users.models
 from InvenTree.helpers import str2bool
+from InvenTree.serializers import DependentField
 
 logger = logging.getLogger('inventree')
 
@@ -242,6 +243,10 @@ class InvenTreeMetadata(SimpleMetadata):
 
         We take the regular DRF metadata and add our own unique flavor
         """
+        # Try to add the child property to the dependent field to be used by the super call
+        if self.label_lookup[field] == 'dependent field':
+            field.get_child(raise_exception=True)
+
         # Run super method first
         field_info = super().get_field_info(field)
 
@@ -275,4 +280,11 @@ class InvenTreeMetadata(SimpleMetadata):
                 else:
                     field_info['api_url'] = model.get_api_url()
 
+        # Add more metadata about dependent fields
+        if field_info['type'] == 'dependent field':
+            field_info['depends_on'] = field.depends_on
+
         return field_info
+
+
+InvenTreeMetadata.label_lookup[DependentField] = "dependent field"
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index 1e2ab59515..96956ec427 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -2,6 +2,7 @@
 
 import os
 from collections import OrderedDict
+from copy import deepcopy
 from decimal import Decimal
 
 from django.conf import settings
@@ -94,6 +95,93 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
         super().__init__(*args, **kwargs)
 
 
+class DependentField(serializers.Field):
+    """A dependent field can be used to dynamically return child fields based on the value of other fields."""
+    child = None
+
+    def __init__(self, *args, depends_on, field_serializer, **kwargs):
+        """A dependent field can be used to dynamically return child fields based on the value of other fields.
+
+        Example:
+        This example adds two fields. If the client selects integer, an integer field will be shown, but if he
+        selects char, an char field will be shown. For any other value, nothing will be shown.
+
+        class TestSerializer(serializers.Serializer):
+            select_type = serializers.ChoiceField(choices=[
+                ("integer", "Integer"),
+                ("char", "Char"),
+            ])
+            my_field = DependentField(depends_on=["select_type"], field_serializer="get_my_field")
+
+            def get_my_field(self, fields):
+                if fields["select_type"] == "integer":
+                    return serializers.IntegerField()
+                if fields["select_type"] == "char":
+                    return serializers.CharField()
+        """
+        super().__init__(*args, **kwargs)
+
+        self.depends_on = depends_on
+        self.field_serializer = field_serializer
+
+    def get_child(self, raise_exception=False):
+        """This method tries to extract the child based on the provided data in the request by the client."""
+        data = deepcopy(self.context["request"].data)
+
+        def visit_parent(node):
+            """Recursively extract the data for the parent field/serializer in reverse."""
+            nonlocal data
+
+            if node.parent:
+                visit_parent(node.parent)
+
+            # only do for composite fields and stop right before the current field
+            if hasattr(node, "child") and node is not self and isinstance(data, dict):
+                data = data.get(node.field_name, None)
+        visit_parent(self)
+
+        # ensure that data is a dictionary and that a parent exists
+        if not isinstance(data, dict) or self.parent is None:
+            return
+
+        # check if the request data contains the dependent fields, otherwise skip getting the child
+        for f in self.depends_on:
+            if not data.get(f, None):
+                return
+
+        # partially validate the data for options requests that set raise_exception while calling .get_child(...)
+        if raise_exception:
+            validation_data = {k: v for k, v in data.items() if k in self.depends_on}
+            serializer = self.parent.__class__(context=self.context, data=validation_data, partial=True)
+            serializer.is_valid(raise_exception=raise_exception)
+
+        # try to get the field serializer
+        field_serializer = getattr(self.parent, self.field_serializer)
+        child = field_serializer(data)
+
+        if not child:
+            return
+
+        self.child = child
+        self.child.bind(field_name='', parent=self)
+
+    def to_internal_value(self, data):
+        """This method tries to convert the data to an internal representation based on the defined to_internal_value method on the child."""
+        self.get_child()
+        if self.child:
+            return self.child.to_internal_value(data)
+
+        return None
+
+    def to_representation(self, value):
+        """This method tries to convert the data to representation based on the defined to_representation method on the child."""
+        self.get_child()
+        if self.child:
+            return self.child.to_representation(value)
+
+        return None
+
+
 class InvenTreeModelSerializer(serializers.ModelSerializer):
     """Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
 
diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py
index 0371b2220f..a76b8019e7 100644
--- a/InvenTree/label/api.py
+++ b/InvenTree/label/api.py
@@ -159,7 +159,8 @@ class LabelPrintMixin(LabelFilterMixin):
         # Check the request to determine if the user has selected a label printing plugin
         plugin = self.get_plugin(self.request)
 
-        serializer = plugin.get_printing_options_serializer(self.request)
+        kwargs.setdefault('context', self.get_serializer_context())
+        serializer = plugin.get_printing_options_serializer(self.request, *args, **kwargs)
 
         # if no serializer is defined, return an empty serializer
         if not serializer:
@@ -226,7 +227,7 @@ class LabelPrintMixin(LabelFilterMixin):
             raise ValidationError('Label has invalid dimensions')
 
         # if the plugin returns a serializer, validate the data
-        if serializer := plugin.get_printing_options_serializer(request, data=request.data):
+        if serializer := plugin.get_printing_options_serializer(request, data=request.data, context=self.get_serializer_context()):
             serializer.is_valid(raise_exception=True)
 
         # At this point, we offload the label(s) to the selected plugin.
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index 69ab8cbd89..c357bb6a94 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -298,7 +298,7 @@ function constructDeleteForm(fields, options) {
  * - closeText: Text for the "close" button
  * - fields: list of fields to display, with the following options
  *      - filters: API query filters
- *      - onEdit: callback when field is edited
+ *      - onEdit: callback or array of callbacks which get fired when field is edited
  *      - secondary: Define a secondary modal form for this field
  *      - label: Specify custom label
  *      - help_text: Specify custom help_text
@@ -493,6 +493,30 @@ function constructFormBody(fields, options) {
         html += options.header_html;
     }
 
+    // process every field by recursively walking down nested fields
+    const processField = (name, field, optionsField) => {
+        if (field.type === "nested object") {
+            for (const [k, v] of Object.entries(field.children)) {
+                processField(`${name}__${k}`, v, optionsField.children[k]);
+            }
+        }
+
+        if (field.type === "dependent field") {
+            if(field.child) {
+                // copy child attribute from parameters to options
+                optionsField.child = field.child;
+
+                processField(name, field.child, optionsField.child);
+            } else {
+                delete optionsField.child;
+            }
+        }
+    }
+
+    for (const [k,v] of Object.entries(fields)) {
+        processField(k, v, options.fields[k]);
+    }
+
     // Client must provide set of fields to be displayed,
     // otherwise *all* fields will be displayed
     var displayed_fields = options.fields || fields;
@@ -599,14 +623,6 @@ function constructFormBody(fields, options) {
 
         var field = fields[field_name];
 
-        switch (field.type) {
-        // Skip field types which are simply not supported
-        case 'nested object':
-            continue;
-        default:
-            break;
-        }
-
         html += constructField(field_name, field, options);
     }
 
@@ -810,7 +826,7 @@ function insertSecondaryButtons(options) {
 /*
  * Extract all specified form values as a single object
  */
-function extractFormData(fields, options) {
+function extractFormData(fields, options, includeLocal = true) {
 
     var data = {};
 
@@ -823,6 +839,7 @@ function extractFormData(fields, options) {
         if (!field) continue;
 
         if (field.type == 'candy') continue;
+        if (!includeLocal && field.localOnly) continue;
 
         data[name] = getFormFieldValue(name, field, options);
     }
@@ -1031,6 +1048,17 @@ function updateFieldValue(name, value, field, options) {
         }
         // TODO - Specify an actual value!
         break;
+    case 'nested object':
+        for (const [k, v] of Object.entries(value)) {
+            if (!(k in field.children)) continue;
+            updateFieldValue(`${name}__${k}`, v, field.children[k], options);
+        }
+        break;
+    case 'dependent field':
+        if (field.child) {
+            updateFieldValue(name, value, field.child, options);
+        }
+        break;
     case 'file upload':
     case 'image upload':
         break;
@@ -1165,6 +1193,17 @@ function getFormFieldValue(name, field={}, options={}) {
     case 'email':
         value = sanitizeInputString(el.val());
         break;
+    case 'nested object':
+        value = {};
+        for (const [name, subField] of Object.entries(field.children)) {
+            value[name] = getFormFieldValue(subField.name, subField, options);
+        }
+        break;
+    case 'dependent field':
+        if(!field.child) return undefined;
+
+        value = getFormFieldValue(name, field.child, options);
+        break;
     default:
         value = el.val();
         break;
@@ -1449,19 +1488,28 @@ function handleFormErrors(errors, fields={}, options={}) {
         var field = fields[field_name] || {};
         var field_errors = errors[field_name];
 
-        if ((field.type == 'nested object') && ('children' in field)) {
+        // for nested objects with children and dependent fields with a child defined, extract nested errors
+        if (((field.type == 'nested object') && ('children' in field)) || ((field.type == 'dependent field') && ('child' in field))) {
             // Handle multi-level nested errors
+            const handleNestedError = (parent_name, sub_field_errors) => {
+                for (const sub_field in sub_field_errors) {
+                    const sub_sub_field_name = `${parent_name}__${sub_field}`;
+                    const sub_sub_field_errors = sub_field_errors[sub_field];
 
-            for (var sub_field in field_errors) {
-                var sub_field_name = `${field_name}__${sub_field}`;
-                var sub_field_errors = field_errors[sub_field];
+                    if (!first_error_field && sub_sub_field_errors && isFieldVisible(sub_sub_field_name, options)) {
+                        first_error_field = sub_sub_field_name;
+                    }
 
-                if (!first_error_field && sub_field_errors && isFieldVisible(sub_field_name, options)) {
-                    first_error_field = sub_field_name;
+                    // if the error is an object, its a nested object, recursively handle the errors
+                    if (typeof sub_sub_field_errors === "object" && !Array.isArray(sub_sub_field_errors)) {
+                        handleNestedError(sub_sub_field_name, sub_sub_field_errors)
+                    } else {
+                        addFieldErrorMessage(sub_sub_field_name, sub_sub_field_errors, options);
+                    }
                 }
-
-                addFieldErrorMessage(sub_field_name, sub_field_errors, options);
             }
+
+            handleNestedError(field_name, field_errors);
         } else if ((field.type == 'field') && ('child' in field)) {
             // This is a "nested" array field
             handleNestedArrayErrors(errors, field_name, options);
@@ -1556,7 +1604,7 @@ function addFieldCallbacks(fields, options) {
 
         var field = fields[name];
 
-        if (!field || !field.onEdit) continue;
+        if (!field || field.type === "candy") continue;
 
         addFieldCallback(name, field, options);
     }
@@ -1564,15 +1612,34 @@ function addFieldCallbacks(fields, options) {
 
 
 function addFieldCallback(name, field, options) {
+    const el = getFormFieldElement(name, options);
 
-    var el = getFormFieldElement(name, options);
+    if (field.onEdit) {
+        el.change(function() {
 
-    el.change(function() {
+            var value = getFormFieldValue(name, field, options);
+            let onEditHandlers = field.onEdit;
 
-        var value = getFormFieldValue(name, field, options);
+            if (!Array.isArray(onEditHandlers)) {
+                onEditHandlers = [onEditHandlers];
+            }
 
-        field.onEdit(value, name, field, options);
-    });
+            for (const onEdit of onEditHandlers) {
+                onEdit(value, name, field, options);
+            }
+        });
+    }
+
+    // attach field callback for nested fields
+    if(field.type === "nested object") {
+        for (const [c_name, c_field] of Object.entries(field.children)) {
+            addFieldCallback(`${name}__${c_name}`, c_field, options);
+        }
+    }
+
+    if(field.type === "dependent field" && field.child) {
+        addFieldCallback(name, field.child, options);
+    }
 }
 
 
@@ -1727,16 +1794,32 @@ function initializeRelatedFields(fields, options={}) {
 
         if (!field || field.hidden) continue;
 
-        switch (field.type) {
-        case 'related field':
-            initializeRelatedField(field, fields, options);
-            break;
-        case 'choice':
-            initializeChoiceField(field, fields, options);
-            break;
-        default:
-            break;
+        initializeRelatedFieldsRecursively(field, fields, options);
+    }
+}
+
+function initializeRelatedFieldsRecursively(field, fields, options) {
+    switch (field.type) {
+    case 'related field':
+        initializeRelatedField(field, fields, options);
+        break;
+    case 'choice':
+        initializeChoiceField(field, fields, options);
+        break;
+    case 'nested object':
+        for (const [c_name, c_field] of Object.entries(field.children)) {
+            if(!c_field.name) c_field.name = `${field.name}__${c_name}`;
+            initializeRelatedFieldsRecursively(c_field, field.children, options);
         }
+        break;
+    case 'dependent field':
+        if (field.child) {
+            if(!field.child.name) field.child.name = field.name;
+            initializeRelatedFieldsRecursively(field.child, fields, options);
+        }
+        break;
+    default:
+        break;
     }
 }
 
@@ -2346,7 +2429,7 @@ function constructField(name, parameters, options={}) {
     html += `<div id='div_id_${field_name}' class='${form_classes}' ${hover_title} ${css}>`;
 
     // Add a label
-    if (!options.hideLabels) {
+    if (!options.hideLabels && parameters.type !== "nested object" && parameters.type !== "dependent field") {
         html += constructLabel(name, parameters);
     }
 
@@ -2501,6 +2584,12 @@ function constructInput(name, parameters, options={}) {
     case 'raw':
         func = constructRawInput;
         break;
+    case 'nested object':
+        func = constructNestedObject;
+        break;
+    case 'dependent field':
+        func = constructDependentField;
+        break;
     default:
         // Unsupported field type!
         break;
@@ -2780,6 +2869,129 @@ function constructRawInput(name, parameters) {
 
 }
 
+/*
+ * Construct a nested object input
+ */
+function constructNestedObject(name, parameters, options) {
+    let html = `
+        <div id="div_id_${name}" class='panel form-panel' style="margin-bottom: 0; padding-bottom: 0;">
+            <div class='panel-heading form-panel-heading'>
+                <div>
+                    <h6 style='display: inline;'>${parameters.label}</h6>
+                </div>
+            </div>
+            <div class='panel-content form-panel-content' id="id_${name}">
+    `;
+
+    parameters.field_names = [];
+
+    for (const [key, field] of Object.entries(parameters.children)) {
+        const subFieldName = `${name}__${key}`;
+        field.name = subFieldName;
+        parameters.field_names.push(subFieldName);
+
+        html += constructField(subFieldName, field, options);
+    }
+
+    html += "</div></div>";
+
+    return html;
+}
+
+function getFieldByNestedPath(name, fields) {
+    if (typeof name === "string") {
+        name = name.split("__");
+    }
+
+    if (name.length === 0) return fields;
+
+    if (fields.type === "nested object") fields = fields.children;
+
+    if (!(name[0] in fields)) return null;
+    let field = fields[name[0]];
+
+    if (field.type === "dependent field" && field.child) {
+        field = field.child;
+    }
+
+    return getFieldByNestedPath(name.slice(1), field);
+}
+
+/*
+ * Construct a dependent field input
+ */
+function constructDependentField(name, parameters, options) {
+    // add onEdit handler to all fields this dependent field depends on
+    for (let d_field_name of parameters.depends_on) {
+        const d_field = getFieldByNestedPath([...name.split("__").slice(0, -1), d_field_name], options.fields);
+        if (!d_field) continue;
+
+        const onEdit = (value, name, field, options) => {
+            if(value === undefined) return;
+
+            // extract the current form data to include in OPTIONS request
+            const data = extractFormData(options.fields, options, false)
+
+            $.ajax({
+                url: options.url,
+                type: "OPTIONS",
+                data: JSON.stringify(data),
+                contentType: "application/json",
+                dataType: "json",
+                accepts: { json: "application/json" },
+                success: (res) => {
+                    const fields = res.actions[options.method];
+
+                    // merge already entered values in the newly constructed form
+                    options.data = extractFormData(options.fields, options);
+
+                    // remove old submit handlers
+                    $(options.modal).off('click', '#modal-form-submit');
+
+                    if (options.method === "POST") {
+                        constructCreateForm(fields, options);
+                    }
+
+                    if (options.method === "PUT" || options.method === "PATCH") {
+                        constructChangeForm(fields, options);
+                    }
+
+                    if (options.method === "DELETE") {
+                        constructDeleteForm(fields, options);
+                    }
+                },
+                error: (xhr) => showApiError(xhr, options.url)
+            });
+        }
+
+        // attach on edit handler
+        const originalOnEdit = d_field.onEdit;
+        d_field.onEdit = [onEdit];
+
+        if(typeof originalOnEdit === "function") {
+            d_field.onEdit.push(originalOnEdit);
+        } else if (Array.isArray(originalOnEdit)) {
+            // push old onEdit handlers, but omit the old
+            d_field.onEdit.push(...originalOnEdit.filter(h => h !== d_field._currentDependentFieldOnEdit));
+        }
+
+        // track current onEdit handler function
+        d_field._currentDependentFieldOnEdit = onEdit;
+    }
+
+    // child is not specified already, return a dummy div with id so no errors can happen
+    if (!parameters.child) {
+        return `<div id="id_${name}" hidden></div>`;
+    }
+
+    // copy label to child if not already provided
+    if(!parameters.child.label) {
+        parameters.child.label = parameters.label;
+    }
+
+    // construct the provided child field
+    return constructField(name, parameters.child, options);
+}
 
 /*
  * Construct a 'help text' div based on the field parameters
diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js
index 57ee46e919..a9a75f0f56 100644
--- a/InvenTree/templates/js/translated/label.js
+++ b/InvenTree/templates/js/translated/label.js
@@ -137,6 +137,11 @@ function printLabels(options) {
 
         // update form
         updateForm(formOptions);
+
+        // workaround to fix a bug where one cannot scroll after changing the plugin
+        // without opening and closing the select box again manually
+        $("#id__plugin").select2("open");
+        $("#id__plugin").select2("close");
     }
 
     const printingFormOptions = {