mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-20 03:36:30 +00:00
[UI] Serial Number Navigation (#9505)
* Add checkClose function to forms - Allow custom check for whether form should be closed * Add form to jump to serial number * Tweak stock detail display * Remove dead field (might fix later, but it's hard with the current API) * Add some icons * Enhance extract_int functionality * Add API endpoint for "next" and "previous" serials for a given stock item * Add serial number navigation on stock item page * Add playwright tests * Bump API version * Fix for serial number clipping * Another tweak
This commit is contained in:
src
backend
InvenTree
frontend
src
components
forms
enums
forms
functions
hooks
pages
tests
pages
@@ -1,13 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 336
|
||||
INVENTREE_API_VERSION = 337
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v337 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9505
|
||||
- Adds API endpoint with extra serial number information for a given StockItem object
|
||||
|
||||
v336 -> 2025-04-10 : https://github.com/inventree/InvenTree/pull/9492
|
||||
- Fixed query and response serialization for units_all and version_text
|
||||
- Fixed LicenseView and VersionInformation serialization
|
||||
|
@@ -32,18 +32,67 @@ from .settings import MEDIA_URL, STATIC_URL
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
INT_CLIP_MAX = 0x7FFFFFFF
|
||||
|
||||
def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
|
||||
"""Extract an integer out of reference."""
|
||||
|
||||
def extract_int(
|
||||
reference, clip=INT_CLIP_MAX, try_hex=False, allow_negative=False
|
||||
) -> int:
|
||||
"""Extract an integer out of provided string.
|
||||
|
||||
Arguments:
|
||||
reference: Input string to extract integer from
|
||||
clip: Maximum value to return (default = 0x7FFFFFFF)
|
||||
try_hex: Attempt to parse as hex if integer conversion fails (default = False)
|
||||
allow_negative: Allow negative values (default = False)
|
||||
"""
|
||||
# Default value if we cannot convert to an integer
|
||||
ref_int = 0
|
||||
|
||||
def do_clip(value: int, clip: int, allow_negative: bool) -> int:
|
||||
"""Perform clipping on the provided value.
|
||||
|
||||
Arguments:
|
||||
value: Value to clip
|
||||
clip: Maximum value to clip to
|
||||
allow_negative: Allow negative values (default = False)
|
||||
"""
|
||||
if clip is None:
|
||||
return value
|
||||
|
||||
clip = min(clip, INT_CLIP_MAX)
|
||||
|
||||
if value > clip:
|
||||
return clip
|
||||
elif value < -clip:
|
||||
return -clip
|
||||
|
||||
if not allow_negative:
|
||||
value = abs(value)
|
||||
|
||||
return value
|
||||
|
||||
reference = str(reference).strip()
|
||||
|
||||
# Ignore empty string
|
||||
if len(reference) == 0:
|
||||
return 0
|
||||
|
||||
# Try naive integer conversion first
|
||||
try:
|
||||
ref_int = int(reference)
|
||||
return do_clip(ref_int, clip, allow_negative)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Hex?
|
||||
if try_hex or reference.startswith('0x'):
|
||||
try:
|
||||
ref_int = int(reference, base=16)
|
||||
return do_clip(ref_int, clip, allow_negative)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Look at the start of the string - can it be "integerized"?
|
||||
result = re.match(r'^(\d+)', reference)
|
||||
|
||||
@@ -66,11 +115,7 @@ def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
|
||||
|
||||
# Ensure that the returned values are within the range that can be stored in an IntegerField
|
||||
# Note: This will result in large values being "clipped"
|
||||
if clip is not None:
|
||||
if ref_int > clip:
|
||||
ref_int = clip
|
||||
elif ref_int < -clip:
|
||||
ref_int = -clip
|
||||
ref_int = do_clip(ref_int, clip, allow_negative)
|
||||
|
||||
if not allow_negative and ref_int < 0:
|
||||
ref_int = abs(ref_int)
|
||||
|
@@ -1227,6 +1227,17 @@ class StockDetail(StockApiMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API detail endpoint for a single StockItem instance."""
|
||||
|
||||
|
||||
class StockItemSerialNumbers(RetrieveAPI):
|
||||
"""View extra serial number information for a given stock item.
|
||||
|
||||
Provides information on the "previous" and "next" stock items,
|
||||
based on the serial number of the given stock item.
|
||||
"""
|
||||
|
||||
queryset = StockItem.objects.all()
|
||||
serializer_class = StockSerializers.StockItemSerialNumbersSerializer
|
||||
|
||||
|
||||
class StockItemTestResultMixin:
|
||||
"""Mixin class for the StockItemTestResult API endpoints."""
|
||||
|
||||
@@ -1615,6 +1626,11 @@ stock_api_urls = [
|
||||
StockItemUninstall.as_view(),
|
||||
name='api-stock-item-uninstall',
|
||||
),
|
||||
path(
|
||||
'serial-numbers/',
|
||||
StockItemSerialNumbers.as_view(),
|
||||
name='api-stock-item-serial-numbers',
|
||||
),
|
||||
path('', StockDetail.as_view(), name='api-stock-detail'),
|
||||
]),
|
||||
),
|
||||
|
@@ -692,6 +692,16 @@ class StockItem(
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_next_stock_item(self):
|
||||
"""Return the 'next' stock item (based on serial number)."""
|
||||
return self.get_next_serialized_item()
|
||||
|
||||
@property
|
||||
def get_previous_stock_item(self):
|
||||
"""Return the 'previous' stock item (based on serial number)."""
|
||||
return self.get_next_serialized_item(reverse=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save this StockItem to the database.
|
||||
|
||||
|
@@ -32,7 +32,11 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
||||
from importer.registry import register_importer
|
||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||
from InvenTree.ready import isGeneratingSchema
|
||||
from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField
|
||||
from InvenTree.serializers import (
|
||||
InvenTreeCurrencySerializer,
|
||||
InvenTreeDecimalField,
|
||||
InvenTreeModelSerializer,
|
||||
)
|
||||
from users.serializers import UserSerializer
|
||||
|
||||
from .models import (
|
||||
@@ -1808,3 +1812,23 @@ class StockTransferSerializer(StockAdjustmentSerializer):
|
||||
stock_item.move(
|
||||
location, notes, request.user, quantity=quantity, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class StockItemSerialNumbersSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for extra serial number information about a stock item."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = StockItem
|
||||
fields = ['next', 'previous']
|
||||
|
||||
next = StockItemSerializer(
|
||||
read_only=True, source='get_next_stock_item', label=_('Next Serial Number')
|
||||
)
|
||||
|
||||
previous = StockItemSerializer(
|
||||
read_only=True,
|
||||
source='get_previous_stock_item',
|
||||
label=_('Previous Serial Number'),
|
||||
)
|
||||
|
Reference in New Issue
Block a user