From e837e5d7d72d69c98c086790aa24b0906635e94f Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 21 Aug 2024 15:45:47 +1000 Subject: [PATCH] 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 --- docs/docs/extend/plugins/validation.md | 84 ++++++++++++++++++- docs/docs/part/pricing.md | 2 +- src/backend/InvenTree/part/models.py | 18 +++- .../base/integration/ValidationMixin.py | 43 ++++++---- .../samples/integration/validation_sample.py | 2 +- 5 files changed, 129 insertions(+), 20 deletions(-) diff --git a/docs/docs/extend/plugins/validation.md b/docs/docs/extend/plugins/validation.md index 0002cf0232..5191f79668 100644 --- a/docs/docs/extend/plugins/validation.md +++ b/docs/docs/extend/plugins/validation.md @@ -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. +::: 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 -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 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](../../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. +::: 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. +::: 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 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. +::: 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 A plugin which requires all serial numbers to be valid hexadecimal values may implement this method as follows: ```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 Arguments: serial: The proposed serial number (string) 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: @@ -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. +::: 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" 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. +::: 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" If the provided number cannot be incremented (or an error occurs) the method should return `None` diff --git a/docs/docs/part/pricing.md b/docs/docs/part/pricing.md index 7ba1352de4..c911a832cf 100644 --- a/docs/docs/part/pricing.md +++ b/docs/docs/part/pricing.md @@ -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. -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" InvenTree provides multi-currency pricing support via the [django-money](https://django-money.readthedocs.io/en/latest/) library. diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index baeb9d8ef4..ccda586f23 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -4,6 +4,7 @@ from __future__ import annotations import decimal import hashlib +import inspect import logging import math import os @@ -775,7 +776,22 @@ class Part( for plugin in registry.with_mixin('validation'): # Run the serial number through each custom validator # 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 except ValidationError as exc: if raise_error: diff --git a/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py b/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py index d00c6753f6..3f60b44753 100644 --- a/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py +++ b/src/backend/InvenTree/plugin/base/integration/ValidationMixin.py @@ -63,7 +63,7 @@ class ValidationMixin: None: or True (refer to class docstring) Raises: - ValidationError: if the instance cannot be deleted + ValidationError: If the instance cannot be deleted """ return None @@ -81,11 +81,11 @@ class ValidationMixin: None: or True (refer to class docstring) Raises: - ValidationError: if the instance is invalid + ValidationError: If the instance is invalid """ 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. Arguments: @@ -96,11 +96,11 @@ class ValidationMixin: None or True (refer to class docstring) Raises: - ValidationError if the proposed name is objectionable + ValidationError: If the proposed name is objectionable """ 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). Arguments: @@ -111,11 +111,13 @@ class ValidationMixin: None or True (refer to class docstring) Raises: - ValidationError if the proposed IPN is objectionable + ValidationError: If the proposed IPN is objectionable """ 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. Arguments: @@ -126,11 +128,11 @@ class ValidationMixin: None or True (refer to class docstring) Raises: - ValidationError if the proposed batch code is objectionable + ValidationError: If the proposed batch code is objectionable """ return None - def generate_batch_code(self, **kwargs): + def generate_batch_code(self, **kwargs) -> str: """Generate a new batch code. This method is called when a new batch code is required. @@ -143,22 +145,28 @@ class ValidationMixin: """ 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. Arguments: serial: The proposed serial number (string) 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: None or True (refer to class docstring) Raises: - ValidationError if the proposed serial is objectionable + ValidationError: If the proposed serial is objectionable """ 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. This integer value is used for efficient sorting based on serial numbers. @@ -179,7 +187,7 @@ class ValidationMixin: """ 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. A plugin which implements this method can either return: @@ -189,10 +197,15 @@ class ValidationMixin: Arguments: serial: Current serial value (string) + + Returns: + The next serial number in the sequence (string), or 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. Arguments: @@ -203,6 +216,6 @@ class ValidationMixin: None or True (refer to class docstring) Raises: - ValidationError if the proposed parameter value is objectionable + ValidationError: If the proposed parameter value is objectionable """ pass diff --git a/src/backend/InvenTree/plugin/samples/integration/validation_sample.py b/src/backend/InvenTree/plugin/samples/integration/validation_sample.py index 09362b0135..3541d12240 100644 --- a/src/backend/InvenTree/plugin/samples/integration/validation_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/validation_sample.py @@ -121,7 +121,7 @@ class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin): if d >= 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. These examples are silly, but serve to demonstrate how the feature could be used