2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-28 03:49:20 +00:00

Plugin validation tweak (#12013)

* Prevent plugin validation actions during data import/export

* Simplify logic

* Further checks
This commit is contained in:
Oliver
2026-05-27 20:35:36 +10:00
committed by GitHub
parent 19182bacd0
commit 33483a3824
5 changed files with 128 additions and 102 deletions
+17 -15
View File
@@ -550,29 +550,31 @@ def increment_serial_number(serial, part=None):
incremented value, or None if incrementing could not be performed. incremented value, or None if incrementing could not be performed.
""" """
from InvenTree.exceptions import log_error from InvenTree.exceptions import log_error
from InvenTree.ready import isReadOnlyCommand
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
# Ensure we start with a string value # Ensure we start with a string value
if serial is not None: if serial is not None:
serial = str(serial).strip() serial = str(serial).strip()
# First, let any plugins attempt to increment the serial number if not isReadOnlyCommand():
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): # First, let any plugins attempt to increment the serial number
try: for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
if not hasattr(plugin, 'increment_serial_number'): try:
continue if not hasattr(plugin, 'increment_serial_number'):
continue
signature = inspect.signature(plugin.increment_serial_number) signature = inspect.signature(plugin.increment_serial_number)
# Note: 2024-08-21 - The 'part' parameter has been added to the signature # Note: 2024-08-21 - The 'part' parameter has been added to the signature
if 'part' in signature.parameters: if 'part' in signature.parameters:
result = plugin.increment_serial_number(serial, part=part) result = plugin.increment_serial_number(serial, part=part)
else: else:
result = plugin.increment_serial_number(serial) result = plugin.increment_serial_number(serial)
if result is not None: if result is not None:
return str(result) return str(result)
except Exception: except Exception:
log_error('increment_serial_number', plugin=plugin.slug) log_error('increment_serial_number', plugin=plugin.slug)
# If we get to here, no plugins were able to "increment" the provided serial value # If we get to here, no plugins were able to "increment" the provided serial value
# Attempt to perform increment according to some basic rules # Attempt to perform increment according to some basic rules
+23 -9
View File
@@ -92,10 +92,23 @@ class PluginValidationMixin(DiffMixin):
Any model class which inherits from this mixin will be exposed to the plugin validation system. Any model class which inherits from this mixin will be exposed to the plugin validation system.
""" """
def should_plugin_validate(self):
"""Return True if this model instance should be validated by plugins.
The default implementation returns True, but this can be overridden in the implementing class if required.
"""
from InvenTree.ready import isReadOnlyCommand
# Prevent plugin validation when importing or exporting data
return not isReadOnlyCommand()
def run_plugin_validation(self): def run_plugin_validation(self):
"""Throw this model against the plugin validation interface.""" """Throw this model against the plugin validation interface."""
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
if not self.should_plugin_validate():
return
deltas = self.get_field_deltas() deltas = self.get_field_deltas()
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
@@ -139,15 +152,16 @@ class PluginValidationMixin(DiffMixin):
from InvenTree.exceptions import log_error from InvenTree.exceptions import log_error
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): if self.should_plugin_validate():
try: for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
plugin.validate_model_deletion(self) try:
except ValidationError as e: plugin.validate_model_deletion(self)
# Plugin might raise a ValidationError to prevent deletion except ValidationError as e:
raise e # Plugin might raise a ValidationError to prevent deletion
except Exception: raise e
log_error('validate_model_deletion', plugin=plugin.slug) except Exception:
continue log_error('validate_model_deletion', plugin=plugin.slug)
continue
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
+4
View File
@@ -2875,6 +2875,10 @@ class Parameter(
except ValidationError as e: except ValidationError as e:
raise ValidationError({'data': e.message}) raise ValidationError({'data': e.message})
if InvenTree.ready.isReadOnlyCommand():
# Skip plugin validation checks during read-only management commands
return
# Finally, run custom validation checks (via plugins) # Finally, run custom validation checks (via plugins)
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
+57 -52
View File
@@ -724,19 +724,21 @@ class Part(
""" """
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): # Skip plugin validation checks during read-only management commands
# Run the name through each custom validator if not InvenTree.ready.isReadOnlyCommand():
# If the plugin returns 'True' we will skip any subsequent validation for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
# Run the name through each custom validator
# If the plugin returns 'True' we will skip any subsequent validation
try: try:
result = plugin.validate_part_name(self.name, self) result = plugin.validate_part_name(self.name, self)
if result: if result:
return return
except ValidationError as exc: except ValidationError as exc:
if raise_error: if raise_error:
raise ValidationError({'name': exc.message}) raise ValidationError({'name': exc.message})
except Exception: except Exception:
log_error('validate_part_name', plugin=plugin.slug) log_error('validate_part_name', plugin=plugin.slug)
def validate_ipn(self, raise_error=True): def validate_ipn(self, raise_error=True):
"""Ensure that the IPN (internal part number) is valid for this Part". """Ensure that the IPN (internal part number) is valid for this Part".
@@ -746,18 +748,20 @@ class Part(
""" """
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): # Skip plugin validation checks during read-only management commands
try: if not InvenTree.ready.isReadOnlyCommand():
result = plugin.validate_part_ipn(self.IPN, self) for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
try:
result = plugin.validate_part_ipn(self.IPN, self)
if result: if result:
# A "true" result force skips any subsequent checks # A "true" result force skips any subsequent checks
break break
except ValidationError as exc: except ValidationError as exc:
if raise_error: if raise_error:
raise ValidationError({'IPN': exc.message}) raise ValidationError({'IPN': exc.message})
except Exception: except Exception:
log_error('validate_part_ipn', plugin=plugin.slug) log_error('validate_part_ipn', plugin=plugin.slug)
# If we get to here, none of the plugins have raised an error # If we get to here, none of the plugins have raised an error
pattern = get_global_setting('PART_IPN_REGEX', '', create=False).strip() pattern = get_global_setting('PART_IPN_REGEX', '', create=False).strip()
@@ -835,40 +839,41 @@ class Part(
Raises: Raises:
ValidationError if serial number is invalid and raise_error = True ValidationError if serial number is invalid and raise_error = True
""" """
serial = str(serial).strip()
# First, throw the serial number against each of the loaded validation plugins
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): serial = str(serial).strip()
# Run the serial number through each custom validator
# If the plugin returns 'True' we will skip any subsequent validation
try: if not InvenTree.ready.isReadOnlyCommand():
result = False # First, throw the serial number against each of the loaded validation plugins
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
# Run the serial number through each custom validator
# If the plugin returns 'True' we will skip any subsequent validation
if hasattr(plugin, 'validate_serial_number'): try:
signature = inspect.signature(plugin.validate_serial_number) result = False
if 'stock_item' in signature.parameters: if hasattr(plugin, 'validate_serial_number'):
# 2024-08-21: New method signature accepts a 'stock_item' parameter signature = inspect.signature(plugin.validate_serial_number)
result = plugin.validate_serial_number(
serial, self, stock_item=stock_item if 'stock_item' in signature.parameters:
) # 2024-08-21: New method signature accepts a 'stock_item' parameter
result = plugin.validate_serial_number(
serial, self, stock_item=stock_item
)
else:
# Old method signature - does not accept a 'stock_item' parameter
result = plugin.validate_serial_number(serial, self)
if result is True:
return True
except ValidationError as exc:
if raise_error:
# Re-throw the error
raise exc
else: else:
# Old method signature - does not accept a 'stock_item' parameter return False
result = plugin.validate_serial_number(serial, self) except Exception:
log_error('validate_serial_number', plugin=plugin.slug)
if result is True:
return True
except ValidationError as exc:
if raise_error:
# Re-throw the error
raise exc
else:
return False
except Exception:
log_error('validate_serial_number', plugin=plugin.slug)
""" """
If we are here, none of the loaded plugins (if any) threw an error or exited early If we are here, none of the loaded plugins (if any) threw an error or exited early
@@ -960,7 +965,7 @@ class Part(
""" """
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
if allow_plugins: if allow_plugins and not InvenTree.ready.isReadOnlyCommand():
# Check with plugin system # Check with plugin system
# If any plugin returns a non-null result, that takes priority # If any plugin returns a non-null result, that takes priority
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
+27 -26
View File
@@ -779,24 +779,24 @@ class StockItem(
# First, let any plugins convert this serial number to an integer value # First, let any plugins convert this serial number to an integer value
# If a non-null value is returned (by any plugin) we will use that # If a non-null value is returned (by any plugin) we will use that
if not InvenTree.ready.isReadOnlyCommand():
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
try:
serial_int = plugin.convert_serial_to_int(serial)
except Exception:
InvenTree.exceptions.log_error(
'convert_serial_to_int', plugin=plugin.slug
)
serial_int = None
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): # Save the first returned result
try: if serial_int is not None:
serial_int = plugin.convert_serial_to_int(serial) # Ensure that it is clipped within a range allowed in the database schema
except Exception: clip = 0x7FFFFFFF
InvenTree.exceptions.log_error( serial_int = abs(serial_int)
'convert_serial_to_int', plugin=plugin.slug serial_int = min(serial_int, clip)
) # Return the first non-null value
serial_int = None return serial_int
# Save the first returned result
if serial_int is not None:
# Ensure that it is clipped within a range allowed in the database schema
clip = 0x7FFFFFFF
serial_int = abs(serial_int)
serial_int = min(serial_int, clip)
# Return the first non-null value
return serial_int
# None of the plugins provided a valid integer value # None of the plugins provided a valid integer value
if serial not in [None, '']: if serial not in [None, '']:
@@ -922,15 +922,16 @@ class StockItem(
""" """
from plugin import PluginMixinEnum, registry from plugin import PluginMixinEnum, registry
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION): if not InvenTree.ready.isReadOnlyCommand():
try: for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
plugin.validate_batch_code(self.batch, self) try:
except ValidationError as exc: plugin.validate_batch_code(self.batch, self)
raise ValidationError({'batch': exc.message}) except ValidationError as exc:
except Exception: raise ValidationError({'batch': exc.message})
InvenTree.exceptions.log_error( except Exception:
'validate_batch_code', plugin=plugin.slug InvenTree.exceptions.log_error(
) 'validate_batch_code', plugin=plugin.slug
)
def clean(self): def clean(self):
"""Validate the StockItem object (separate to field validation). """Validate the StockItem object (separate to field validation).