From d9f73ae05bf8b44ab0b5d7f3752feb8f332fdbcb Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 7 Mar 2022 12:27:00 +1100 Subject: [PATCH 1/6] Clip integer versions of serial numbers to specified range --- InvenTree/InvenTree/models.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 0fe3136871..1296ba8978 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -133,7 +133,7 @@ class ReferenceIndexingMixin(models.Model): reference_int = models.BigIntegerField(default=0) -def extract_int(reference): +def extract_int(reference, clip=0x7fffffff): # Default value if we cannot convert to an integer ref_int = 0 @@ -146,6 +146,15 @@ def extract_int(reference): ref_int = int(ref) except: ref_int = 0 + + # 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 + return ref_int From 2f3f57148a451530eeb536a872f06a6123b45275 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 7 Mar 2022 13:47:38 +1100 Subject: [PATCH 2/6] Refactor previous code for looking up "previous" and "next" serial numbers - Old way was extremely inefficient, especially when large spaces existed between serial numbers - New method reduces O(n) to O(1) --- InvenTree/stock/models.py | 49 +++++++++++++++++++++++++++++++++++++++ InvenTree/stock/views.py | 41 ++++++-------------------------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 70af651686..2abcecdc5f 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -273,6 +273,55 @@ class StockItem(MPTTModel): self.serial_int = serial_int + def get_next_serial_number(self, include_variants=True, reverse=False): + """ + Get the "next" serial number for the part this stock item references. + + e.g. if this stock item has a serial number 100, we may return the stock item with serial number 101 + + Note that this only works for "serialized" stock items with integer values + + Args: + include_variants: True if we wish to include stock for variant parts + reverse: True if we want to return the "previous" (lower) serial number + + Returns: + A StockItem object matching the requirements, or None + + """ + + if not self.serialized: + return None + + # Find only serialized stock items + items = StockItem.objects.exclude(serial=None).exclude(serial='') + + if include_variants: + # Match against any part within the variant tree + items = items.filter(part__tree_id=self.part.tree_id) + else: + # Match only against the specific part + items = items.filter(part=self.part) + + serial = self.serial_int + + if reverse: + # Select only stock items with lower serial numbers, in decreasing order + items = items.filter(serial_int__lt=serial) + items = items.order_by('-serial_int') + else: + # Select only stock items with higher serial numbers, in increasing order + items = items.filter(serial_int__gt=serial) + items = items.order_by('serial_int') + + if items.count() > 0: + item = items.first() + + if item.serialized: + return item + + return None + def save(self, *args, **kwargs): """ Save this StockItem to the database. Performs a number of checks: diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 95cb498739..71dc592080 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -101,43 +101,16 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView): model = StockItem def get_context_data(self, **kwargs): - """ add previous and next item """ + """ + Add information on the "next" and "previous" StockItem objects, + based on the serial numbers. + """ + data = super().get_context_data(**kwargs) if self.object.serialized: - - serial_elem = {} - - try: - current = int(self.object.serial) - - for item in self.object.part.stock_items.all(): - - if item.serialized: - try: - sn = int(item.serial) - serial_elem[sn] = item - except ValueError: - # We only support integer serial number progression - pass - - serials = serial_elem.keys() - - # previous - for nbr in range(current - 1, min(serials), -1): - if nbr in serials: - data['previous'] = serial_elem.get(nbr, None) - break - - # next - for nbr in range(current + 1, max(serials) + 1): - if nbr in serials: - data['next'] = serial_elem.get(nbr, None) - break - - except ValueError: - # We only support integer serial number progression - pass + data['previous'] = self.object.get_next_serial_number(reverse=True) + data['next'] = self.object.get_next_serial_number() data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') data['item_owner'] = self.object.get_item_owner() From 0adf89e793f35a0520c4f02752a149a18a631ed0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 7 Mar 2022 14:07:01 +1100 Subject: [PATCH 3/6] Add unit tests for serial number functionality --- InvenTree/InvenTree/models.py | 4 +- InvenTree/stock/models.py | 5 ++- InvenTree/stock/tests.py | 72 +++++++++++++++++++++++++++++++++++ InvenTree/stock/views.py | 4 +- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 1296ba8978..99232519dc 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -152,8 +152,8 @@ def extract_int(reference, clip=0x7fffffff): if clip is not None: if ref_int > clip: ref_int = clip - elif ref_int < clip: - ref_int = clip + elif ref_int < -clip: + ref_int = -clip return ref_int diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 2abcecdc5f..69d2bbaf26 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -269,11 +269,14 @@ class StockItem(MPTTModel): serial_int = 0 if serial is not None: + + serial = str(serial).strip() + serial_int = extract_int(str(serial)) self.serial_int = serial_int - def get_next_serial_number(self, include_variants=True, reverse=False): + def get_next_serialized_item(self, include_variants=True, reverse=False): """ Get the "next" serial number for the part this stock item references. diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 98a8f5e288..ba0e6554d6 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -380,6 +380,78 @@ class StockTest(TestCase): item.save() self.assertTrue(item.serialized) + def test_big_serials(self): + """ + Unit tests for "large" serial numbers which exceed integer encoding + """ + + p = Part.objects.create( + name='trackable part', + description='trackable part', + trackable=True, + ) + + item = StockItem.objects.create( + part=p, + quantity=1, + ) + + for sn in [12345, '12345', ' 12345 ']: + item.serial = sn + item.save() + + self.assertEqual(item.serial_int, 12345) + + item.serial = "-123" + item.save() + + # Negative number should map to zero + self.assertEqual(item.serial_int, 0) + + # Non-numeric values should encode to zero + for sn in ['apple', 'banana', 'carrot']: + item.serial = sn + item.save() + + self.assertEqual(item.serial_int, 0) + + # Next, test for incremenet / decrement functionality + item.serial = 100 + item.save() + + item_next = StockItem.objects.create( + part=p, + serial=150, + quantity=1 + ) + + self.assertEqual(item.get_next_serialized_item(), item_next) + + item_prev = StockItem.objects.create( + part=p, + serial=' 57', + quantity=1, + ) + + self.assertEqual(item.get_next_serialized_item(reverse=True), item_prev) + + # Create a number of serialized stock items around the current item + for i in range(75, 125): + try: + StockItem.objects.create( + part=p, + serial=i, + quantity=1, + ) + except: + pass + + item_next = item.get_next_serialized_item() + item_prev = item.get_next_serialized_item(reverse=True) + + self.assertEqual(item_next.serial_int, 101) + self.assertEqual(item_prev.serial_int, 99) + def test_serialize_stock_invalid(self): """ Test manual serialization of parts. diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 71dc592080..079f9c2dc9 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -109,8 +109,8 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView): data = super().get_context_data(**kwargs) if self.object.serialized: - data['previous'] = self.object.get_next_serial_number(reverse=True) - data['next'] = self.object.get_next_serial_number() + data['previous'] = self.object.get_next_serialized_item(reverse=True) + data['next'] = self.object.get_next_serialized_item() data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') data['item_owner'] = self.object.get_item_owner() From 09ff4862ecb673aee567609ac28d4e10e1138b67 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 7 Mar 2022 14:43:55 +1100 Subject: [PATCH 4/6] Unit test fixes --- InvenTree/order/test_api.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index b6fee8a218..8d5d91f0fb 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -112,17 +112,16 @@ class PurchaseOrderTest(OrderTest): self.assignRole('purchase_order.add') url = reverse('api-po-list') - huge_numer = 9223372036854775808 + huge_number = 9223372036854775808 - # too big self.post( url, { 'supplier': 1, - 'reference': huge_numer, + 'reference': huge_number, 'description': 'PO not created via the API', }, - expected_code=400 + expected_code=201, ) def test_po_attachments(self): From ac512cb8bbc11770ec82c3379bbc46f2daac3e2f Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 7 Mar 2022 14:52:31 +1100 Subject: [PATCH 5/6] Add test for very large serial number --- InvenTree/stock/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index ba0e6554d6..0ee17b244a 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -405,6 +405,12 @@ class StockTest(TestCase): item.serial = "-123" item.save() + # Test a very very large value + item.serial = '99999999999999999999999999999999999999999999999999999' + item.save() + + self.assertEqual(item.serial_int, 0x7fffffff) + # Negative number should map to zero self.assertEqual(item.serial_int, 0) From 6fcc9ec8f06b4339bb6ed41f455a0002be10d2b3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 7 Mar 2022 15:01:15 +1100 Subject: [PATCH 6/6] Unit test fixes --- InvenTree/stock/models.py | 2 +- InvenTree/stock/tests.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 69d2bbaf26..27d6cf5fc3 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -272,7 +272,7 @@ class StockItem(MPTTModel): serial = str(serial).strip() - serial_int = extract_int(str(serial)) + serial_int = extract_int(serial) self.serial_int = serial_int diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 0ee17b244a..d1e68fc8e5 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -405,15 +405,15 @@ class StockTest(TestCase): item.serial = "-123" item.save() + # Negative number should map to zero + self.assertEqual(item.serial_int, 0) + # Test a very very large value item.serial = '99999999999999999999999999999999999999999999999999999' item.save() self.assertEqual(item.serial_int, 0x7fffffff) - # Negative number should map to zero - self.assertEqual(item.serial_int, 0) - # Non-numeric values should encode to zero for sn in ['apple', 'banana', 'carrot']: item.serial = sn