2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-02-19 13:18:03 +00:00
Files
InvenTree/src/backend/InvenTree/plugin/samples/integration/validation_sample.py
Oliver 7a646946c3 [bug] FIx for data migration (#11329)
* [bug] FIx for data migration

Prevent writing of default currency to database during migration or backup

* Ignore error
2026-02-15 11:37:13 +11:00

190 lines
6.8 KiB
Python

"""Sample plugin which demonstrates custom validation functionality."""
from datetime import datetime
from plugin import InvenTreePlugin
from plugin.mixins import SettingsMixin, ValidationMixin
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): # noqa: RUF069
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_parameter(self, parameter, data):
"""Validate 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