diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index fd86306627..1418fddd41 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -404,21 +404,28 @@ def DownloadFile(data, filename, content_type='application/text', inline=False): return response -def extract_serial_numbers(serials, expected_quantity): +def extract_serial_numbers(serials, expected_quantity, next_number: int): """ Attempt to extract serial numbers from an input string. - Serial numbers must be integer values - Serial numbers must be positive - Serial numbers can be split by whitespace / newline / commma chars - Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20 + - Serial numbers can be defined as ~ for getting the next available serial number - Serial numbers can be supplied as + for getting all expecteded numbers starting from - Serial numbers can be supplied as + for getting numbers starting from Args: + serials: input string with patterns expected_quantity: The number of (unique) serial numbers we expect + next_number(int): the next possible serial number """ serials = serials.strip() + # fill in the next serial number into the serial + if '~' in serials: + serials = serials.replace('~', str(next_number)) + groups = re.split("[\s,]+", serials) numbers = [] @@ -493,11 +500,20 @@ def extract_serial_numbers(serials, expected_quantity): errors.append(_("Invalid group: {g}").format(g=group)) continue + # Group should be a number + elif group: + # try conversion + try: + number = int(group) + except: + # seem like it is not a number + raise ValidationError(_(f"Invalid group {group}")) + + number_add(number) + + # No valid input group detected else: - if group in numbers: - errors.append(_("Duplicate serial: {g}".format(g=group))) - else: - numbers.append(group) + raise ValidationError(_(f"Invalid/no group {group}")) if len(errors) > 0: raise ValidationError(errors) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 06dcad9797..7fd62908a0 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -237,25 +237,41 @@ class TestSerialNumberExtraction(TestCase): e = helpers.extract_serial_numbers - sn = e("1-5", 5) - self.assertEqual(len(sn), 5) + sn = e("1-5", 5, 1) + self.assertEqual(len(sn), 5, 1) for i in range(1, 6): self.assertIn(i, sn) - sn = e("1, 2, 3, 4, 5", 5) + sn = e("1, 2, 3, 4, 5", 5, 1) self.assertEqual(len(sn), 5) - sn = e("1-5, 10-15", 11) + sn = e("1-5, 10-15", 11, 1) self.assertIn(3, sn) self.assertIn(13, sn) - sn = e("1+", 10) + sn = e("1+", 10, 1) self.assertEqual(len(sn), 10) self.assertEqual(sn, [_ for _ in range(1, 11)]) - sn = e("4, 1+2", 4) + sn = e("4, 1+2", 4, 1) self.assertEqual(len(sn), 4) - self.assertEqual(sn, ["4", 1, 2, 3]) + self.assertEqual(sn, [4, 1, 2, 3]) + + sn = e("~", 1, 1) + self.assertEqual(len(sn), 1) + self.assertEqual(sn, [1]) + + sn = e("~", 1, 3) + self.assertEqual(len(sn), 1) + self.assertEqual(sn, [3]) + + sn = e("~+", 2, 5) + self.assertEqual(len(sn), 2) + self.assertEqual(sn, [5, 6]) + + sn = e("~+3", 4, 5) + self.assertEqual(len(sn), 4) + self.assertEqual(sn, [5, 6, 7, 8]) def test_failures(self): @@ -263,26 +279,45 @@ class TestSerialNumberExtraction(TestCase): # Test duplicates with self.assertRaises(ValidationError): - e("1,2,3,3,3", 5) + e("1,2,3,3,3", 5, 1) # Test invalid length with self.assertRaises(ValidationError): - e("1,2,3", 5) + e("1,2,3", 5, 1) # Test empty string with self.assertRaises(ValidationError): - e(", , ,", 0) + e(", , ,", 0, 1) # Test incorrect sign in group with self.assertRaises(ValidationError): - e("10-2", 8) + e("10-2", 8, 1) # Test invalid group with self.assertRaises(ValidationError): - e("1-5-10", 10) + e("1-5-10", 10, 1) with self.assertRaises(ValidationError): - e("10, a, 7-70j", 4) + e("10, a, 7-70j", 4, 1) + + def test_combinations(self): + e = helpers.extract_serial_numbers + + sn = e("1 3-5 9+2", 7, 1) + self.assertEqual(len(sn), 7) + self.assertEqual(sn, [1, 3, 4, 5, 9, 10, 11]) + + sn = e("1,3-5,9+2", 7, 1) + self.assertEqual(len(sn), 7) + self.assertEqual(sn, [1, 3, 4, 5, 9, 10, 11]) + + sn = e("~+2", 3, 14) + self.assertEqual(len(sn), 3) + self.assertEqual(sn, [14, 15, 16]) + + sn = e("~+", 2, 14) + self.assertEqual(len(sn), 2) + self.assertEqual(sn, [14, 15]) class TestVersionNumber(TestCase): diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index fd730b6a7e..1d28cb8d50 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -109,7 +109,7 @@ class BuildOutputCreate(AjaxUpdateView): # Check that the serial numbers are valid if serials: try: - extracted = extract_serial_numbers(serials, quantity) + extracted = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt()) if extracted: # Check for conflicting serial numbers @@ -143,7 +143,7 @@ class BuildOutputCreate(AjaxUpdateView): serials = data.get('serial_numbers', None) if serials: - serial_numbers = extract_serial_numbers(serials, quantity) + serial_numbers = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt()) else: serial_numbers = None diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 32e50943b1..6097a707c7 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -861,7 +861,7 @@ class SOSerialAllocationSerializer(serializers.Serializer): part = line_item.part try: - data['serials'] = extract_serial_numbers(serial_numbers, quantity) + data['serials'] = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt()) except DjangoValidationError as e: raise ValidationError({ 'serial_numbers': e.messages, diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b1aee26094..ec76846a08 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -594,6 +594,26 @@ class Part(MPTTModel): # No serial numbers found return None + def getLatestSerialNumberInt(self): + """ + Return the "latest" serial number for this Part as a integer. + If it is not an integer the result is 0 + """ + + latest = self.getLatestSerialNumber() + + # No serial number = > 0 + if latest is None: + latest = 0 + + # Attempt to turn into an integer and return + try: + latest = int(latest) + return latest + except: + # not an integer so 0 + return 0 + def getSerialNumberString(self, quantity=1): """ Return a formatted string representing the next available serial numbers, diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index a016f9057f..cfd0b45d89 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -480,18 +480,6 @@ class StockList(generics.ListCreateAPIView): notes = data.get('notes', '') - serials = None - - if serial_numbers: - # If serial numbers are specified, check that they match! - try: - serials = extract_serial_numbers(serial_numbers, data['quantity']) - except DjangoValidationError as e: - raise ValidationError({ - 'quantity': e.messages, - 'serial_numbers': e.messages, - }) - with transaction.atomic(): # Create an initial stock item @@ -507,6 +495,19 @@ class StockList(generics.ListCreateAPIView): if item.part.default_expiry > 0: item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry) + # fetch serial numbers + serials = None + + if serial_numbers: + # If serial numbers are specified, check that they match! + try: + serials = extract_serial_numbers(serial_numbers, quantity, item.part.getLatestSerialNumberInt()) + except DjangoValidationError as e: + raise ValidationError({ + 'quantity': e.messages, + 'serial_numbers': e.messages, + }) + # Finally, save the item (with user information) item.save(user=user) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 681ead9caf..1c4c8844ff 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -350,7 +350,7 @@ class SerializeStockItemSerializer(serializers.Serializer): serial_numbers = data['serial_numbers'] try: - serials = InvenTree.helpers.extract_serial_numbers(serial_numbers, quantity) + serials = InvenTree.helpers.extract_serial_numbers(serial_numbers, quantity, item.part.getLatestSerialNumberInt()) except DjangoValidationError as e: raise ValidationError({ 'serial_numbers': e.messages, @@ -379,6 +379,7 @@ class SerializeStockItemSerializer(serializers.Serializer): serials = InvenTree.helpers.extract_serial_numbers( data['serial_numbers'], data['quantity'], + item.part.getLatestSerialNumberInt() ) item.serializeStock( diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 27801f0ed6..6c89db0f2f 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1241,7 +1241,7 @@ class StockItemCreate(AjaxCreateView): if len(sn) > 0: try: - serials = extract_serial_numbers(sn, quantity) + serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt()) except ValidationError as e: serials = None form.add_error('serial_numbers', e.messages) @@ -1283,7 +1283,7 @@ class StockItemCreate(AjaxCreateView): # Create a single stock item for each provided serial number if len(sn) > 0: - serials = extract_serial_numbers(sn, quantity) + serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt()) for serial in serials: item = StockItem(