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

1"""Sample plugin which demonstrates custom validation functionality.""" 

2 

3from datetime import datetime 

4 

5from plugin import InvenTreePlugin 

6from plugin.mixins import SettingsMixin, ValidationMixin 

7 

8 

9class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin): 

10 """A sample plugin class for demonstrating custom validation functions. 

11 

12 Simple of examples of custom validator code. 

13 """ 

14 

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' 

20 

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 } 

57 

58 def validate_model_instance(self, instance, deltas=None): 

59 """Run validation against any saved model. 

60 

61 - Check if the instance is a BomItem object 

62 - Test if the quantity is an integer 

63 """ 

64 import part.models 

65 

66 # Print debug message to console (intentional) 

67 print('Validating model instance:', instance.__class__, f'<{instance.pk}>') 

68 

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 }) 

75 

76 if isinstance(instance, part.models.Part): 

77 # If the part description is being updated, prevent it from being reduced in length 

78 

79 if deltas and 'description' in deltas: 

80 old_desc = deltas['description']['old'] 

81 new_desc = deltas['description']['new'] 

82 

83 if len(new_desc) < len(old_desc): 

84 self.raise_error({ 

85 'description': 'Part description cannot be shortened' 

86 }) 

87 

88 def validate_part_name(self, name: str, part): 

89 """Custom validation for Part name field. 

90 

91 Rules: 

92 - Name must be shorter than the description field 

93 - Name cannot contain illegal characters 

94 

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') 

99 

100 illegal_chars = self.get_setting('ILLEGAL_PART_CHARS') 

101 

102 for c in illegal_chars: 

103 if c in name: 

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

105 

106 def validate_part_ipn(self, ipn: str, part): 

107 """Validate part IPN. 

108 

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'") 

113 

114 def validate_parameter(self, parameter, data): 

115 """Validate parameter data. 

116 

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') 

123 

124 def validate_serial_number(self, serial: str, part, stock_item=None): 

125 """Validate serial number for a given StockItem. 

126 

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') 

132 

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') 

137 

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 

145 

146 def increment_serial_number(self, serial: str, part=None, **kwargs): 

147 """Increment a serial number. 

148 

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 

154 

155 # Skip any serial number which is a multiple of 5 

156 if sn % 5 == 0: 

157 sn += 1 

158 

159 return str(sn) 

160 except ValueError: 

161 pass 

162 

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

164 return None 

165 

166 def validate_batch_code(self, batch_code: str, item): 

167 """Ensure that a particular batch code meets specification. 

168 

169 These examples are silly, but serve to demonstrate how the feature could be used 

170 """ 

171 prefix = self.get_setting('BATCH_CODE_PREFIX') 

172 

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}'") 

175 

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}' 

180 

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}' 

184 

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}' 

188 

189 return batch