Coverage for src/backend/InvenTree/plugin/samples/integration/validation_sample.py: 0%
73 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Sample plugin which demonstrates custom validation functionality."""
3from datetime import datetime
5from plugin import InvenTreePlugin
6from plugin.mixins import SettingsMixin, ValidationMixin
9class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin):
10 """A sample plugin class for demonstrating custom validation functions.
12 Simple of examples of custom validator code.
13 """
15 NAME = 'SampleValidator'
16 SLUG = 'validator'
17 TITLE = 'Sample Validator Plugin'
18 DESCRIPTION = 'A sample plugin for demonstrating custom validation functionality'
19 VERSION = '0.3.0'
21 SETTINGS = {
22 'ILLEGAL_PART_CHARS': {
23 'name': 'Illegal Part Characters',
24 'description': 'Characters which are not allowed to appear in Part names',
25 'default': '!@#$%^&*()~`',
26 },
27 'IPN_MUST_CONTAIN_Q': {
28 'name': 'IPN Q Requirement',
29 'description': 'Part IPN field must contain the character Q',
30 'default': False,
31 'validator': bool,
32 },
33 'SERIAL_MUST_BE_PALINDROME': {
34 'name': 'Palindromic Serials',
35 'description': 'Serial numbers must be palindromic',
36 'default': False,
37 'validator': bool,
38 },
39 'SERIAL_MUST_MATCH_PART': {
40 'name': 'Serial must match part',
41 'description': 'First letter of serial number must match first letter of part name',
42 'default': False,
43 'validator': bool,
44 },
45 'BATCH_CODE_PREFIX': {
46 'name': 'Batch prefix',
47 'description': 'Required prefix for batch code',
48 'default': 'B',
49 },
50 'BOM_ITEM_INTEGER': {
51 'name': 'Integer Bom Quantity',
52 'description': 'Bom item quantity must be an integer',
53 'default': False,
54 'validator': bool,
55 },
56 }
58 def validate_model_instance(self, instance, deltas=None):
59 """Run validation against any saved model.
61 - Check if the instance is a BomItem object
62 - Test if the quantity is an integer
63 """
64 import part.models
66 # Print debug message to console (intentional)
67 print('Validating model instance:', instance.__class__, f'<{instance.pk}>')
69 if isinstance(instance, part.models.BomItem):
70 if self.get_setting('BOM_ITEM_INTEGER'):
71 if float(instance.quantity) != int(instance.quantity): # noqa: RUF069
72 self.raise_error({
73 'quantity': 'Bom item quantity must be an integer'
74 })
76 if isinstance(instance, part.models.Part):
77 # If the part description is being updated, prevent it from being reduced in length
79 if deltas and 'description' in deltas:
80 old_desc = deltas['description']['old']
81 new_desc = deltas['description']['new']
83 if len(new_desc) < len(old_desc):
84 self.raise_error({
85 'description': 'Part description cannot be shortened'
86 })
88 def validate_part_name(self, name: str, part):
89 """Custom validation for Part name field.
91 Rules:
92 - Name must be shorter than the description field
93 - Name cannot contain illegal characters
95 These examples are silly, but serve to demonstrate how the feature could be used.
96 """
97 if len(part.description) < len(name):
98 self.raise_error('Part description cannot be shorter than the name')
100 illegal_chars = self.get_setting('ILLEGAL_PART_CHARS')
102 for c in illegal_chars:
103 if c in name:
104 self.raise_error(f"Illegal character in part name: '{c}'")
106 def validate_part_ipn(self, ipn: str, part):
107 """Validate part IPN.
109 These examples are silly, but serve to demonstrate how the feature could be used
110 """
111 if self.get_setting('IPN_MUST_CONTAIN_Q') and 'Q' not in ipn:
112 self.raise_error("IPN must contain 'Q'")
114 def validate_parameter(self, parameter, data):
115 """Validate parameter data.
117 These examples are silly, but serve to demonstrate how the feature could be used
118 """
119 if parameter.template.name.lower() in ['length', 'width']:
120 d = int(data)
121 if d >= 100:
122 self.raise_error('Value must be less than 100')
124 def validate_serial_number(self, serial: str, part, stock_item=None):
125 """Validate serial number for a given StockItem.
127 These examples are silly, but serve to demonstrate how the feature could be used
128 """
129 if self.get_setting('SERIAL_MUST_BE_PALINDROME'):
130 if serial != serial[::-1]:
131 self.raise_error('Serial must be a palindrome')
133 if self.get_setting('SERIAL_MUST_MATCH_PART'):
134 # Serial must start with the same letter as the linked part, for some reason
135 if serial[0] != part.name[0]:
136 self.raise_error('Serial number must start with same letter as part')
138 # Prevent serial numbers which are a multiple of 5
139 try:
140 sn = int(serial)
141 if sn % 5 == 0:
142 self.raise_error('Serial number cannot be a multiple of 5')
143 except ValueError:
144 pass
146 def increment_serial_number(self, serial: str, part=None, **kwargs):
147 """Increment a serial number.
149 These examples are silly, but serve to demonstrate how the feature could be used
150 """
151 try:
152 sn = int(serial)
153 sn += 1
155 # Skip any serial number which is a multiple of 5
156 if sn % 5 == 0:
157 sn += 1
159 return str(sn)
160 except ValueError:
161 pass
163 # Return "None" to defer to the next plugin or builtin functionality
164 return None
166 def validate_batch_code(self, batch_code: str, item):
167 """Ensure that a particular batch code meets specification.
169 These examples are silly, but serve to demonstrate how the feature could be used
170 """
171 prefix = self.get_setting('BATCH_CODE_PREFIX')
173 if len(batch_code) > 0 and prefix and not batch_code.startswith(prefix):
174 self.raise_error(f"Batch code must start with '{prefix}'")
176 def generate_batch_code(self, **kwargs):
177 """Generate a new batch code."""
178 now = datetime.now()
179 batch = f'SAMPLE-BATCH-{now.year}:{now.month}:{now.day}'
181 # If a Part instance is provided, prepend the part name to the batch code
182 if part := kwargs.get('part'):
183 batch = f'{part.name}-{batch}'
185 # If a Build instance is provided, prepend the build number to the batch code
186 if build := kwargs.get('build_order'):
187 batch = f'{build.reference}-{batch}'
189 return batch