From f87f4387ed1713701fa0b90261154250de3aeb43 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@users.noreply.github.com> Date: Tue, 8 Apr 2025 07:31:40 +0200 Subject: [PATCH] Schema: (Redo) Remove hardcoded enum values from customizable fields (#9452) * Remove hardcoded currency enum from schema * Convert schema custom key enums to int to allow customized keys to validate * Convert stock status key enums to int to allow customizations to validate in schema * api version bump * Restore enumerated help text for currencies * Remove commented block of old code * Restore custom key enumerated values to schema documentation * Restore status key enumeration to schema documentation --- .../InvenTree/InvenTree/api_version.py | 6 ++- .../InvenTree/InvenTree/serializers.py | 10 ++++ src/backend/InvenTree/build/serializers.py | 12 ++--- .../InvenTree/generic/states/fields.py | 20 ++++++- src/backend/InvenTree/order/serializers.py | 15 ++---- src/backend/InvenTree/part/serializers.py | 20 ++++++- src/backend/InvenTree/stock/serializers.py | 52 +++++++++++++------ 7 files changed, 98 insertions(+), 37 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index f82170449b..887651f2b2 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 332 +INVENTREE_API_VERSION = 333 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v333 - 2025-04-03 : https://github.com/inventree/InvenTree/pull/9452 + - Currency string is no longer restricted to a hardcoded enum + - Customizable status keys are no longer hardcoded enum values + v332 - 2025-04-02 : https://github.com/inventree/InvenTree/pull/9393 - Adds 'search_notes' parameter to all searchable API endpoints diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index 4d6c84f082..9aa653c1af 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _ from djmoney.contrib.django_rest_framework.fields import MoneyField from djmoney.money import Money from djmoney.utils import MONEY_CLASSES, get_currency_field_name +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.fields import empty @@ -86,6 +87,7 @@ class InvenTreeMoneySerializer(MoneyField): return amount +@extend_schema_field(serializers.CharField()) class InvenTreeCurrencySerializer(serializers.ChoiceField): """Custom serializers for selecting currency option.""" @@ -111,6 +113,14 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField): if 'help_text' not in kwargs: kwargs['help_text'] = _('Select currency from available options') + if InvenTree.ready.isGeneratingSchema(): + kwargs['help_text'] = ( + kwargs['help_text'] + + '\n\n' + + '\n'.join(f'* `{value}` - {label}' for value, label in choices) + + "\n\nOther valid currencies may be found in the 'CURRENCY_CODES' global setting." + ) + super().__init__(*args, **kwargs) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index c017f0480c..84a5f1c517 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -40,7 +40,11 @@ from InvenTree.serializers import ( ) from stock.generators import generate_batch_code from stock.models import StockItem, StockLocation -from stock.serializers import LocationBriefSerializer, StockItemSerializerBrief +from stock.serializers import ( + LocationBriefSerializer, + StockItemSerializerBrief, + StockStatusCustomSerializer, +) from stock.status_codes import StockStatus from users.serializers import OwnerSerializer, UserSerializer @@ -581,11 +585,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer): help_text=_('Location for completed build outputs'), ) - status_custom_key = serializers.ChoiceField( - choices=StockStatus.items(custom=True), - default=StockStatus.OK.value, - label=_('Status'), - ) + status_custom_key = StockStatusCustomSerializer(default=StockStatus.OK.value) accept_incomplete_allocation = serializers.BooleanField( default=False, diff --git a/src/backend/InvenTree/generic/states/fields.py b/src/backend/InvenTree/generic/states/fields.py index 20d7d42f27..458a13dacb 100644 --- a/src/backend/InvenTree/generic/states/fields.py +++ b/src/backend/InvenTree/generic/states/fields.py @@ -8,9 +8,13 @@ from django.db import models from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.fields import ChoiceField +import InvenTree.ready + from .custom import get_logical_value @@ -73,6 +77,7 @@ class CustomChoiceField(serializers.ChoiceField): return field_info +@extend_schema_field(OpenApiTypes.INT) class ExtraCustomChoiceField(CustomChoiceField): """Custom Choice Field that returns value of status if empty. @@ -138,14 +143,27 @@ class InvenTreeCustomStatusModelField(models.PositiveIntegerField): if self.status_class: validators.append(CustomStatusCodeValidator(status_class=self.status_class)) + help_text = _('Additional status information for this item') + if InvenTree.ready.isGeneratingSchema(): + help_text = ( + help_text + + '\n\n' + + '\n'.join( + f'* `{value}` - {label}' + for value, label in self.status_class.items(custom=True) + ) + + "\n\nAdditional custom status keys may be retrieved from the corresponding 'status_retrieve' call." + ) + custom_key_field = ExtraInvenTreeCustomStatusModelField( default=None, verbose_name=_('Custom status key'), - help_text=_('Additional status information for this item'), + help_text=help_text, validators=validators, blank=True, null=True, ) + cls.add_to_class(f'{name}_custom_key', custom_key_field) self._custom_key_field = custom_key_field diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 36cea1e3ee..8034c735f6 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -795,11 +795,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer): allow_blank=True, ) - status = serializers.ChoiceField( - choices=StockStatus.items(custom=True), - default=StockStatus.OK.value, - label=_('Status'), - ) + status = stock.serializers.StockStatusCustomSerializer(default=StockStatus.OK.value) packaging = serializers.CharField( label=_('Packaging'), @@ -1981,13 +1977,8 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer): label=_('Return order line item'), ) - status = serializers.ChoiceField( - choices=stock.status_codes.StockStatus.items(custom=True), - default=None, - label=_('Status'), - help_text=_('Stock item status code'), - required=False, - allow_blank=True, + status = stock.serializers.StockStatusCustomSerializer( + default=None, required=False, allow_blank=True ) def validate_line_item(self, item): diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 3a69b4d73f..5c8b4870ca 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -16,6 +16,7 @@ from django.utils.translation import gettext_lazy as _ import structlog from djmoney.contrib.exchange.exceptions import MissingRate from djmoney.contrib.exchange.models import convert_money +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from sql_util.utils import SubqueryCount, SubquerySum from taggit.serializers import TagListSerializerField @@ -1300,6 +1301,21 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer): ) +@extend_schema_field( + serializers.CharField( + help_text=_('Select currency from available options') + + '\n\n' + + '\n'.join( + f'* `{value}` - {label}' + for value, label in common.currency.currency_code_mappings() + ) + + "\n\nOther valid currencies may be found in the 'CURRENCY_CODES' global setting." + ) +) +class PartPricingCurrencySerializer(serializers.ChoiceField): + """Serializer to allow annotating the schema to use String on currency fields.""" + + class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for Part pricing information.""" @@ -1384,7 +1400,7 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer): required=False, ) - override_min_currency = serializers.ChoiceField( + override_min_currency = PartPricingCurrencySerializer( label=_('Minimum price currency'), read_only=False, required=False, @@ -1399,7 +1415,7 @@ class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer): required=False, ) - override_max_currency = serializers.ChoiceField( + override_max_currency = PartPricingCurrencySerializer( label=_('Maximum price currency'), read_only=False, required=False, diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 6f8669f853..d7f0102e1b 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -10,6 +10,7 @@ from django.db.models.functions import Coalesce from django.utils.translation import gettext_lazy as _ import structlog +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.serializers import ValidationError from sql_util.utils import SubqueryCount, SubquerySum @@ -984,6 +985,38 @@ class ConvertStockItemSerializer(serializers.Serializer): stock_item.convert_to_variant(part, request.user) +@extend_schema_field( + serializers.IntegerField( + help_text='Status key, chosen from the list of StockStatus keys' + ) +) +class StockStatusCustomSerializer(serializers.ChoiceField): + """Serializer to allow annotating the schema to use int where custom values may be entered.""" + + def __init__(self, *args, **kwargs): + """Initialize the status selector.""" + if 'choices' not in kwargs: + kwargs['choices'] = stock.status_codes.StockStatus.items(custom=True) + + if 'label' not in kwargs: + kwargs['label'] = _('Status') + + if 'help_text' not in kwargs: + kwargs['help_text'] = _('Stock item status code') + + if InvenTree.ready.isGeneratingSchema(): + kwargs['help_text'] = ( + kwargs['help_text'] + + '\n\n' + + '\n'.join( + f'* `{value}` - {label}' for value, label in kwargs['choices'] + ) + + "\n\nAdditional custom status keys may be retrieved from the 'stock_status_retrieve' call." + ) + + super().__init__(*args, **kwargs) + + class ReturnStockItemSerializer(serializers.Serializer): """DRF serializer for returning a stock item from a customer.""" @@ -1001,14 +1034,7 @@ class ReturnStockItemSerializer(serializers.Serializer): help_text=_('Destination location for returned item'), ) - status = serializers.ChoiceField( - choices=stock.status_codes.StockStatus.items(custom=True), - default=None, - label=_('Status'), - help_text=_('Stock item status code'), - required=False, - allow_blank=True, - ) + status = StockStatusCustomSerializer(default=None, required=False, allow_blank=True) notes = serializers.CharField( label=_('Notes'), @@ -1058,10 +1084,8 @@ class StockChangeStatusSerializer(serializers.Serializer): return items - status = serializers.ChoiceField( - choices=stock.status_codes.StockStatus.items(custom=True), - default=stock.status_codes.StockStatus.OK.value, - label=_('Status'), + status = StockStatusCustomSerializer( + default=stock.status_codes.StockStatus.OK.value ) note = serializers.CharField( @@ -1620,11 +1644,9 @@ class StockAdjustmentItemSerializer(serializers.Serializer): help_text=_('Batch code for this stock item'), ) - status = serializers.ChoiceField( + status = StockStatusCustomSerializer( choices=stock_item_adjust_status_options(), default=None, - label=_('Status'), - help_text=_('Stock item status code'), required=False, allow_blank=True, )