Enhance plugin SN validation (#7942)

* Update function signature for 'validate_serial_number'

- Pass through stock item parameter
- Required if we want to exclude a particular item from that test

* Update documentation

* Docs fixes

* Add type annotations
This commit is contained in:
Oliver 2024-08-21 15:45:47 +10:00 committed by GitHub
parent f2f90dd1e4
commit e837e5d7d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 129 additions and 20 deletions

View File

@ -108,22 +108,71 @@ By default, part names are not subject to any particular naming conventions or r
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. 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.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_part_name
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Part IPN ### 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. 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.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_part_ipn
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Part Parameter Values ### Part Parameter Values
[Part parameters](../../part/parameter.md) 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. [Part parameters](../../part/parameter.md) 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.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_part_parameter
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Batch Codes ### Batch Codes
[Batch codes](../../stock/tracking.md#batch-codes) can be generated and/or validated by custom plugins. [Batch codes](../../stock/tracking.md#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. 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.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_batch_code
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Generate Batch Code
The `generate_batch_code` method can be implemented to generate a new batch code, based on a set of provided information. The `generate_batch_code` method can be implemented to generate a new batch code, based on a set of provided information.
::: plugin.base.integration.ValidationMixin.ValidationMixin.generate_batch_code
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Serial Numbers ### 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. 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.
@ -134,17 +183,30 @@ The default InvenTree [serial numbering system](../../stock/tracking.md#serial-n
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. 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.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_serial_number
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
!!! info "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 ##### Example
A plugin which requires all serial numbers to be valid hexadecimal values may implement this method as follows: A plugin which requires all serial numbers to be valid hexadecimal values may implement this method as follows:
```python ```python
def validate_serial_number(self, serial: str, part: Part): def validate_serial_number(self, serial: str, part: Part, stock_item: StockItem = None):
"""Validate the supplied serial number """Validate the supplied serial number
Arguments: Arguments:
serial: The proposed serial number (string) serial: The proposed serial number (string)
part: The Part instance for which this serial number is being validated 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: try:
@ -160,6 +222,15 @@ While InvenTree supports arbitrary text values in the serial number fields, behi
A custom plugin can implement the `convert_serial_to_int` method to determine how a particular serial number is converted to an integer representation. A custom plugin can implement the `convert_serial_to_int` method to determine how a particular serial number is converted to an integer representation.
::: plugin.base.integration.ValidationMixin.ValidationMixin.convert_serial_to_int
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
!!! info "Not Required" !!! info "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 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
@ -169,6 +240,15 @@ A core component of the InvenTree serial number system is the ability to *increm
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. 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.
::: plugin.base.integration.ValidationMixin.ValidationMixin.increment_serial_number
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
!!! info "Invalid Increment" !!! info "Invalid Increment"
If the provided number cannot be incremented (or an error occurs) the method should return `None` If the provided number cannot be incremented (or an error occurs) the method should return `None`

View File

@ -46,7 +46,7 @@ Additionally, the following information is stored for each part, in relation to
InvenTree supports pricing data in multiple currencies, allowing integration with suppliers and customers using different currency systems. InvenTree supports pricing data in multiple currencies, allowing integration with suppliers and customers using different currency systems.
Supported currencies must be configured as part of [the InvenTree setup process](../start/config.md#supported-currencies). Supported currencies can be configured in the [InvenTree settings](../settings/currency.md).
!!! info "Currency Support" !!! info "Currency Support"
InvenTree provides multi-currency pricing support via the [django-money](https://django-money.readthedocs.io/en/latest/) library. InvenTree provides multi-currency pricing support via the [django-money](https://django-money.readthedocs.io/en/latest/) library.

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import decimal import decimal
import hashlib import hashlib
import inspect
import logging import logging
import math import math
import os import os
@ -775,7 +776,22 @@ class Part(
for plugin in registry.with_mixin('validation'): for plugin in registry.with_mixin('validation'):
# Run the serial number through each custom validator # Run the serial number through each custom validator
# If the plugin returns 'True' we will skip any subsequent validation # If the plugin returns 'True' we will skip any subsequent validation
if plugin.validate_serial_number(serial, self):
result = False
if hasattr(plugin, 'validate_serial_number'):
signature = inspect.signature(plugin.validate_serial_number)
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 return True
except ValidationError as exc: except ValidationError as exc:
if raise_error: if raise_error:

View File

@ -63,7 +63,7 @@ class ValidationMixin:
None: or True (refer to class docstring) None: or True (refer to class docstring)
Raises: Raises:
ValidationError: if the instance cannot be deleted ValidationError: If the instance cannot be deleted
""" """
return None return None
@ -81,11 +81,11 @@ class ValidationMixin:
None: or True (refer to class docstring) None: or True (refer to class docstring)
Raises: Raises:
ValidationError: if the instance is invalid ValidationError: If the instance is invalid
""" """
return None return None
def validate_part_name(self, name: str, part: part.models.Part): def validate_part_name(self, name: str, part: part.models.Part) -> None:
"""Perform validation on a proposed Part name. """Perform validation on a proposed Part name.
Arguments: Arguments:
@ -96,11 +96,11 @@ class ValidationMixin:
None or True (refer to class docstring) None or True (refer to class docstring)
Raises: Raises:
ValidationError if the proposed name is objectionable ValidationError: If the proposed name is objectionable
""" """
return None return None
def validate_part_ipn(self, ipn: str, part: part.models.Part): def validate_part_ipn(self, ipn: str, part: part.models.Part) -> None:
"""Perform validation on a proposed Part IPN (internal part number). """Perform validation on a proposed Part IPN (internal part number).
Arguments: Arguments:
@ -111,11 +111,13 @@ class ValidationMixin:
None or True (refer to class docstring) None or True (refer to class docstring)
Raises: Raises:
ValidationError if the proposed IPN is objectionable ValidationError: If the proposed IPN is objectionable
""" """
return None return None
def validate_batch_code(self, batch_code: str, item: stock.models.StockItem): def validate_batch_code(
self, batch_code: str, item: stock.models.StockItem
) -> None:
"""Validate the supplied batch code. """Validate the supplied batch code.
Arguments: Arguments:
@ -126,11 +128,11 @@ class ValidationMixin:
None or True (refer to class docstring) None or True (refer to class docstring)
Raises: Raises:
ValidationError if the proposed batch code is objectionable ValidationError: If the proposed batch code is objectionable
""" """
return None return None
def generate_batch_code(self, **kwargs): def generate_batch_code(self, **kwargs) -> str:
"""Generate a new batch code. """Generate a new batch code.
This method is called when a new batch code is required. This method is called when a new batch code is required.
@ -143,22 +145,28 @@ class ValidationMixin:
""" """
return None return None
def validate_serial_number(self, serial: str, part: part.models.Part): def validate_serial_number(
self,
serial: str,
part: part.models.Part,
stock_item: stock.models.StockItem = None,
) -> None:
"""Validate the supplied serial number. """Validate the supplied serial number.
Arguments: Arguments:
serial: The proposed serial number (string) serial: The proposed serial number (string)
part: The Part instance for which this serial number is being validated 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: Returns:
None or True (refer to class docstring) None or True (refer to class docstring)
Raises: Raises:
ValidationError if the proposed serial is objectionable ValidationError: If the proposed serial is objectionable
""" """
return None return None
def convert_serial_to_int(self, serial: str): def convert_serial_to_int(self, serial: str) -> int:
"""Convert a serial number (string) into an integer representation. """Convert a serial number (string) into an integer representation.
This integer value is used for efficient sorting based on serial numbers. This integer value is used for efficient sorting based on serial numbers.
@ -179,7 +187,7 @@ class ValidationMixin:
""" """
return None return None
def increment_serial_number(self, serial: str): def increment_serial_number(self, serial: str) -> str:
"""Return the next sequential serial based on the provided value. """Return the next sequential serial based on the provided value.
A plugin which implements this method can either return: A plugin which implements this method can either return:
@ -189,10 +197,15 @@ class ValidationMixin:
Arguments: Arguments:
serial: Current serial value (string) serial: Current serial value (string)
Returns:
The next serial number in the sequence (string), or None
""" """
return None return None
def validate_part_parameter(self, parameter, data): def validate_part_parameter(
self, parameter: part.models.PartParameter, data: str
) -> None:
"""Validate a parameter value. """Validate a parameter value.
Arguments: Arguments:
@ -203,6 +216,6 @@ class ValidationMixin:
None or True (refer to class docstring) None or True (refer to class docstring)
Raises: Raises:
ValidationError if the proposed parameter value is objectionable ValidationError: If the proposed parameter value is objectionable
""" """
pass pass

View File

@ -121,7 +121,7 @@ class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin):
if d >= 100: if d >= 100:
self.raise_error('Value must be less than 100') self.raise_error('Value must be less than 100')
def validate_serial_number(self, serial: str, part): def validate_serial_number(self, serial: str, part, stock_item=None):
"""Validate serial number for a given StockItem. """Validate serial number for a given StockItem.
These examples are silly, but serve to demonstrate how the feature could be used These examples are silly, but serve to demonstrate how the feature could be used