2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-20 03:36:30 +00:00

[UI] Serial Number Navigation ()

* 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:
Oliver
2025-04-15 12:42:25 +10:00
committed by GitHub
parent 8d44a0d330
commit 448d24de21
13 changed files with 383 additions and 95 deletions
src

@@ -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'),
)