Skip to content

Validation Mixin

ValidationMixin

The ValidationMixin class enables plugins to perform custom validation of objects within the database.

Any of the methods described below can be implemented in a custom plugin to provide functionality as required.

More Info

For more information on any of the methods described below, refer to the InvenTree source code. A working example is available as a starting point.

Multi Plugin Support

It is possible to have multiple plugins loaded simultaneously which support validation methods. For example when validating a field, if one plugin returns a null value (None) then the next plugin (if available) will be queried.

Model Deletion

Any model which inherits the PluginValidationMixin class is exposed to the plugin system for custom deletion validation. Before the model is deleted from the database, it is first passed to the plugin ecosystem to check if it really should be deleted.

A custom plugin may implement the validate_model_deletion method to perform custom validation on the model instance before it is deleted.

Run custom validation when a model instance is being deleted.

This method is called when a model instance is being deleted. It allows the plugin to raise a ValidationError if the instance cannot be deleted.

Parameters:

Name Type Description Default
instance Model

The model instance to validate

required

Returns:

Name Type Description
None None

or True (refer to class docstring)

Raises:

Type Description
ValidationError

If the instance cannot be deleted

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def validate_model_deletion(self, instance: Model) -> None:
    """Run custom validation when a model instance is being deleted.

    This method is called when a model instance is being deleted.
    It allows the plugin to raise a ValidationError if the instance cannot be deleted.

    Arguments:
        instance: The model instance to validate

    Returns:
        None: or True (refer to class docstring)

    Raises:
        ValidationError: If the instance cannot be deleted
    """
    return None

Model Validation

Any model which inherits the PluginValidationMixin mixin class is exposed to the plugin system for custom validation. Before the model is saved to the database (either when created, or updated), it is first passed to the plugin ecosystem for validation.

Any plugin which inherits the ValidationMixin can implement the validate_model_instance method, and run a custom validation routine.

Run custom validation on a database model instance.

This method is called when a model instance is being validated. It allows the plugin to raise a ValidationError on any field in the model.

Parameters:

Name Type Description Default
instance Model

The model instance to validate

required
deltas Optional[dict]

A dictionary of field names and updated values (if the instance is being updated)

None

Returns:

Name Type Description
None None

or True (refer to class docstring)

Raises:

Type Description
ValidationError

If the instance is invalid

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def validate_model_instance(
    self, instance: Model, deltas: Optional[dict] = None
) -> None:
    """Run custom validation on a database model instance.

    This method is called when a model instance is being validated.
    It allows the plugin to raise a ValidationError on any field in the model.

    Arguments:
        instance: The model instance to validate
        deltas: A dictionary of field names and updated values (if the instance is being updated)

    Returns:
        None: or True (refer to class docstring)

    Raises:
        ValidationError: If the instance is invalid
    """
    return None

Error Messages

Any error messages must be raised as a ValidationError. The ValidationMixin class provides the raise_error method, which is a simple wrapper method which raises a ValidationError

Instance Errors

To indicate an instance validation error (i.e. the validation error applies to the entire model instance), the body of the error should be either a string, or a list of strings.

Field Errors

To indicate a field validation error (i.e. the validation error applies only to a single field on the model instance), the body of the error should be a dict, where the key(s) of the dict correspond to the model fields.

Note that an error can be which corresponds to multiple model instance fields.

Example Plugin

Presented below is a simple working example for a plugin which implements the validate_model_instance method:

from plugin import InvenTreePlugin
from plugin.mixins import ValidationMixin

import part.models


class MyValidationMixin(Validationixin, InvenTreePlugin):
    """Custom validation plugin."""

    def validate_model_instance(self, instance, deltas=None):
        """Custom model validation example.

        - A part name and category name must have the same starting letter
        - A PartCategory description field cannot be shortened after it has been created
        """

        if isinstance(instance, part.models.Part):
            if category := instance.category:
                if category.name[0] != part.name[0]:
                    self.raise_error({
                        "name": "Part name and category name must start with the same letter"
                    })

        if isinstance(instance, part.models.PartCategory):
            if deltas and 'description' in deltas:
                d_new = deltas['description']['new']
                d_old = deltas['description']['old']

                if len(d_new) < len(d_old):
                    self.raise_error({
                        "description": "Description cannot be shortened"
                    })

Field Validation

In addition to the general purpose model instance validation routine provided above, the following fields support custom validation routines:

Part Name

By default, part names are not subject to any particular naming conventions or requirements. However if custom validation is required, the validate_part_name method can be implemented to ensure that a part name conforms to a required convention.

If the custom method determines that the part name is objectionable, it should throw a ValidationError which will be handled upstream by parent calling methods.

Perform validation on a proposed Part name.

Parameters:

Name Type Description Default
name str

The proposed part name

required
part Part

The part instance we are validating against

required

Returns:

Type Description
None

None or True (refer to class docstring)

Raises:

Type Description
ValidationError

If the proposed name is objectionable

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def validate_part_name(self, name: str, part: part.models.Part) -> None:
    """Perform validation on a proposed Part name.

    Arguments:
        name: The proposed part name
        part: The part instance we are validating against

    Returns:
        None or True (refer to class docstring)

    Raises:
        ValidationError: If the proposed name is objectionable
    """
    return None

Part IPN

Validation of the Part IPN (Internal Part Number) field is exposed to custom plugins via the validate_part_ipn method. Any plugins which extend the ValidationMixin class can implement this method, and raise a ValidationError if the IPN value does not match a required convention.

Perform validation on a proposed Part IPN (internal part number).

Parameters:

Name Type Description Default
ipn str

The proposed part IPN

required
part Part

The Part instance we are validating against

required

Returns:

Type Description
None

None or True (refer to class docstring)

Raises:

Type Description
ValidationError

If the proposed IPN is objectionable

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def validate_part_ipn(self, ipn: str, part: part.models.Part) -> None:
    """Perform validation on a proposed Part IPN (internal part number).

    Arguments:
        ipn: The proposed part IPN
        part: The Part instance we are validating against

    Returns:
        None or True (refer to class docstring)

    Raises:
        ValidationError: If the proposed IPN is objectionable
    """
    return None

Part Parameter Values

Part parameters can also have custom validation rules applied, by implementing the validate_part_parameter method. A plugin which implements this method should raise a ValidationError with an appropriate message if the part parameter value does not match a required convention.

Validate a parameter value.

Parameters:

Name Type Description Default
parameter PartParameter

The parameter we are validating

required
data str

The proposed parameter value

required

Returns:

Type Description
None

None or True (refer to class docstring)

Raises:

Type Description
ValidationError

If the proposed parameter value is objectionable

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def validate_part_parameter(
    self, parameter: part.models.PartParameter, data: str
) -> None:
    """Validate a parameter value.

    Arguments:
        parameter: The parameter we are validating
        data: The proposed parameter value

    Returns:
        None or True (refer to class docstring)

    Raises:
        ValidationError: If the proposed parameter value is objectionable
    """

Batch Codes

Batch codes can be generated and/or validated by custom plugins.

Validate Batch Code

The validate_batch_code method allows plugins to raise an error if a batch code input by the user does not meet a particular pattern.

Validate the supplied batch code.

Parameters:

Name Type Description Default
batch_code str

The proposed batch code (string)

required
item StockItem

The StockItem instance we are validating against

required

Returns:

Type Description
None

None or True (refer to class docstring)

Raises:

Type Description
ValidationError

If the proposed batch code is objectionable

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def validate_batch_code(
    self, batch_code: str, item: stock.models.StockItem
) -> None:
    """Validate the supplied batch code.

    Arguments:
        batch_code: The proposed batch code (string)
        item: The StockItem instance we are validating against

    Returns:
        None or True (refer to class docstring)

    Raises:
        ValidationError: If the proposed batch code is objectionable
    """
    return None

Generate Batch Code

The generate_batch_code method can be implemented to generate a new batch code, based on a set of provided information.

Generate a new batch code.

This method is called when a new batch code is required.

kwargs

Any additional keyword arguments which are passed through to the plugin, based on the context of the caller

Returns:

Type Description
str

A new batch code (string) or None

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
139
140
141
142
143
144
145
146
147
148
149
150
def generate_batch_code(self, **kwargs) -> str:
    """Generate a new batch code.

    This method is called when a new batch code is required.

    kwargs:
        Any additional keyword arguments which are passed through to the plugin, based on the context of the caller

    Returns:
        A new batch code (string) or None
    """
    return None

Serial Numbers

Requirements for serial numbers can vary greatly depending on the application. Rather than attempting to provide a "one size fits all" serial number implementation, InvenTree allows custom serial number schemes to be implemented via plugins.

The default InvenTree serial numbering system uses a simple algorithm to validate and increment serial numbers. More complex behaviors can be implemented using the ValidationMixin plugin class and the following custom methods:

Serial Number Validation

Custom serial number validation can be implemented using the validate_serial_number method. A proposed serial number is passed to this method, which then has the opportunity to raise a ValidationError to indicate that the serial number is not valid.

Validate the supplied serial number.

Parameters:

Name Type Description Default
serial str

The proposed serial number (string)

required
part Part

The Part instance for which this serial number is being validated

required
stock_item StockItem

The StockItem instance for which this serial number is being validated (if applicable)

None

Returns:

Type Description
None

None or True (refer to class docstring)

Raises:

Type Description
ValidationError

If the proposed serial is objectionable

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def validate_serial_number(
    self,
    serial: str,
    part: part.models.Part,
    stock_item: stock.models.StockItem = None,
) -> None:
    """Validate the supplied serial number.

    Arguments:
        serial: The proposed serial number (string)
        part: The Part instance for which this serial number is being validated
        stock_item: The StockItem instance for which this serial number is being validated (if applicable)

    Returns:
        None or True (refer to class docstring)

    Raises:
        ValidationError: If the proposed serial is objectionable
    """
    return None

Stock Item

If the stock_item argument is provided, then this stock item has already been assigned with the provided serial number. This stock item should be excluded from any subsequent checks for uniqueness. The stock_item parameter is optional, and may be None if the serial number is being validated in a context where no stock item is available.

Example

A plugin which requires all serial numbers to be valid hexadecimal values may implement this method as follows:

def validate_serial_number(self, serial: str, part: Part, stock_item: StockItem = None):
    """Validate the supplied serial number

    Arguments:
        serial: The proposed serial number (string)
        part: The Part instance for which this serial number is being validated
        stock_item: The StockItem instance for which this serial number is being validated
    """

    try:
        # Attempt integer conversion
        int(serial, 16)
    except ValueError:
        raise ValidationError("Serial number must be a valid hex value")

Serial Number Sorting

While InvenTree supports arbitrary text values in the serial number fields, behind the scenes it attempts to "coerce" these values into an integer representation for more efficient sorting.

A custom plugin can implement the convert_serial_to_int method to determine how a particular serial number is converted to an integer representation.

Convert a serial number (string) into an integer representation.

This integer value is used for efficient sorting based on serial numbers.

A plugin which implements this method can either return:

  • An integer based on the serial string, according to some algorithm
  • A fixed value, such that serial number sorting reverts to the string representation
  • None (null value) to let any other plugins perform the conversion

Note that there is no requirement for the returned integer value to be unique.

Parameters:

Name Type Description Default
serial str

Serial value (string)

required

Returns:

Type Description
int

integer representation of the serial number, or None

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def convert_serial_to_int(self, serial: str) -> int:
    """Convert a serial number (string) into an integer representation.

    This integer value is used for efficient sorting based on serial numbers.

    A plugin which implements this method can either return:

    - An integer based on the serial string, according to some algorithm
    - A fixed value, such that serial number sorting reverts to the string representation
    - None (null value) to let any other plugins perform the conversion

    Note that there is no requirement for the returned integer value to be unique.

    Arguments:
        serial: Serial value (string)

    Returns:
        integer representation of the serial number, or None
    """
    return None

Not Required

If this method is not implemented, or the serial number cannot be converted to an integer, then the sorting algorithm falls back to the text (string) value

Serial Number Incrementing

A core component of the InvenTree serial number system is the ability to increment serial numbers - to determine the next serial number value in a sequence.

For custom serial number schemes, it is important to provide a method to generate the next serial number given a current value. The increment_serial_number method can be implemented by a plugin to achieve this.

Return the next sequential serial based on the provided value.

A plugin which implements this method can either return:

  • A string which represents the "next" serial number in the sequence
  • None (null value) if the next value could not be determined

Parameters:

Name Type Description Default
serial str

Current serial value (string)

required
part Part

The Part instance for which this serial number is being incremented

None

Returns:

Type Description
str

The next serial number in the sequence (string), or None

Source code in src/backend/InvenTree/plugin/base/integration/ValidationMixin.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def increment_serial_number(
    self, serial: str, part: part.models.Part = None, **kwargs
) -> str:
    """Return the next sequential serial based on the provided value.

    A plugin which implements this method can either return:

    - A string which represents the "next" serial number in the sequence
    - None (null value) if the next value could not be determined

    Arguments:
        serial: Current serial value (string)
        part: The Part instance for which this serial number is being incremented

    Returns:
        The next serial number in the sequence (string), or None
    """
    return None

Invalid Increment

If the provided number cannot be incremented (or an error occurs) the method should return None

Example

Continuing with the hexadecimal example as above, the method may be implemented as follows:

def increment_serial_number(self, serial: str):
    """Provide the next hexadecimal number in sequence"""

    try:
        val = int(serial, 16) + 1
        val = hex(val).upper()[2:]
    except ValueError:
        val = None

    return val

Sample Plugin

A sample plugin which implements custom validation routines is provided in the InvenTree source code:

A sample plugin class for demonstrating custom validation functions.

Simple of examples of custom validator code.

Source code in src/backend/InvenTree/plugin/samples/integration/validation_sample.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin):
    """A sample plugin class for demonstrating custom validation functions.

    Simple of examples of custom validator code.
    """

    NAME = 'SampleValidator'
    SLUG = 'validator'
    TITLE = 'Sample Validator Plugin'
    DESCRIPTION = 'A sample plugin for demonstrating custom validation functionality'
    VERSION = '0.3.0'

    SETTINGS = {
        'ILLEGAL_PART_CHARS': {
            'name': 'Illegal Part Characters',
            'description': 'Characters which are not allowed to appear in Part names',
            'default': '!@#$%^&*()~`',
        },
        'IPN_MUST_CONTAIN_Q': {
            'name': 'IPN Q Requirement',
            'description': 'Part IPN field must contain the character Q',
            'default': False,
            'validator': bool,
        },
        'SERIAL_MUST_BE_PALINDROME': {
            'name': 'Palindromic Serials',
            'description': 'Serial numbers must be palindromic',
            'default': False,
            'validator': bool,
        },
        'SERIAL_MUST_MATCH_PART': {
            'name': 'Serial must match part',
            'description': 'First letter of serial number must match first letter of part name',
            'default': False,
            'validator': bool,
        },
        'BATCH_CODE_PREFIX': {
            'name': 'Batch prefix',
            'description': 'Required prefix for batch code',
            'default': 'B',
        },
        'BOM_ITEM_INTEGER': {
            'name': 'Integer Bom Quantity',
            'description': 'Bom item quantity must be an integer',
            'default': False,
            'validator': bool,
        },
    }

    def validate_model_instance(self, instance, deltas=None):
        """Run validation against any saved model.

        - Check if the instance is a BomItem object
        - Test if the quantity is an integer
        """
        import part.models

        # Print debug message to console (intentional)
        print('Validating model instance:', instance.__class__, f'<{instance.pk}>')

        if isinstance(instance, part.models.BomItem):
            if self.get_setting('BOM_ITEM_INTEGER'):
                if float(instance.quantity) != int(instance.quantity):
                    self.raise_error({
                        'quantity': 'Bom item quantity must be an integer'
                    })

        if isinstance(instance, part.models.Part):
            # If the part description is being updated, prevent it from being reduced in length

            if deltas and 'description' in deltas:
                old_desc = deltas['description']['old']
                new_desc = deltas['description']['new']

                if len(new_desc) < len(old_desc):
                    self.raise_error({
                        'description': 'Part description cannot be shortened'
                    })

    def validate_part_name(self, name: str, part):
        """Custom validation for Part name field.

        Rules:
        - Name must be shorter than the description field
        - Name cannot contain illegal characters

        These examples are silly, but serve to demonstrate how the feature could be used.
        """
        if len(part.description) < len(name):
            self.raise_error('Part description cannot be shorter than the name')

        illegal_chars = self.get_setting('ILLEGAL_PART_CHARS')

        for c in illegal_chars:
            if c in name:
                self.raise_error(f"Illegal character in part name: '{c}'")

    def validate_part_ipn(self, ipn: str, part):
        """Validate part IPN.

        These examples are silly, but serve to demonstrate how the feature could be used
        """
        if self.get_setting('IPN_MUST_CONTAIN_Q') and 'Q' not in ipn:
            self.raise_error("IPN must contain 'Q'")

    def validate_part_parameter(self, parameter, data):
        """Validate part parameter data.

        These examples are silly, but serve to demonstrate how the feature could be used
        """
        if parameter.template.name.lower() in ['length', 'width']:
            d = int(data)
            if d >= 100:
                self.raise_error('Value must be less than 100')

    def validate_serial_number(self, serial: str, part, stock_item=None):
        """Validate serial number for a given StockItem.

        These examples are silly, but serve to demonstrate how the feature could be used
        """
        if self.get_setting('SERIAL_MUST_BE_PALINDROME'):
            if serial != serial[::-1]:
                self.raise_error('Serial must be a palindrome')

        if self.get_setting('SERIAL_MUST_MATCH_PART'):
            # Serial must start with the same letter as the linked part, for some reason
            if serial[0] != part.name[0]:
                self.raise_error('Serial number must start with same letter as part')

        # Prevent serial numbers which are a multiple of 5
        try:
            sn = int(serial)
            if sn % 5 == 0:
                self.raise_error('Serial number cannot be a multiple of 5')
        except ValueError:
            pass

    def increment_serial_number(self, serial: str, part=None, **kwargs):
        """Increment a serial number.

        These examples are silly, but serve to demonstrate how the feature could be used
        """
        try:
            sn = int(serial)
            sn += 1

            # Skip any serial number which is a multiple of 5
            if sn % 5 == 0:
                sn += 1

            return str(sn)
        except ValueError:
            pass

        # Return "None" to defer to the next plugin or builtin functionality
        return None

    def validate_batch_code(self, batch_code: str, item):
        """Ensure that a particular batch code meets specification.

        These examples are silly, but serve to demonstrate how the feature could be used
        """
        prefix = self.get_setting('BATCH_CODE_PREFIX')

        if len(batch_code) > 0 and prefix and not batch_code.startswith(prefix):
            self.raise_error(f"Batch code must start with '{prefix}'")

    def generate_batch_code(self, **kwargs):
        """Generate a new batch code."""
        now = datetime.now()
        batch = f'SAMPLE-BATCH-{now.year}:{now.month}:{now.day}'

        # If a Part instance is provided, prepend the part name to the batch code
        if part := kwargs.get('part'):
            batch = f'{part.name}-{batch}'

        # If a Build instance is provided, prepend the build number to the batch code
        if build := kwargs.get('build_order'):
            batch = f'{build.reference}-{batch}'

        return batch