diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index c5a8ad4b67..7bd4fd819d 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -427,8 +427,9 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): serials = serials.strip() # fill in the next serial number into the serial - if '~' in serials: - serials = serials.replace('~', str(next_number)) + while '~' in serials: + serials = serials.replace('~', str(next_number), 1) + next_number += 1 # Split input string by whitespace or comma (,) characters groups = re.split("[\s,]+", serials) @@ -438,6 +439,12 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): # Helper function to check for duplicated numbers def add_sn(sn): + # Attempt integer conversion first, so numerical strings are never stored + try: + sn = int(sn) + except ValueError: + pass + if sn in numbers: errors.append(_('Duplicate serial: {sn}').format(sn=sn)) else: @@ -451,15 +458,25 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): if len(serials) == 0: raise ValidationError([_("Empty serial number string")]) - for group in groups: + # If the user has supplied the correct number of serials, don't process them for groups + # just add them so any duplicates (or future validations) are checked + if len(groups) == expected_quantity: + for group in groups: + add_sn(group) + if len(errors) > 0: + raise ValidationError(errors) + + return numbers + + for group in groups: group = group.strip() # Hyphen indicates a range of numbers if '-' in group: items = group.split('-') - if len(items) == 2: + if len(items) == 2 and all([i.isnumeric() for i in items]): a = items[0].strip() b = items[1].strip() @@ -471,13 +488,14 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): for n in range(a, b + 1): add_sn(n) else: - errors.append(_("Invalid group: {g}").format(g=group)) + errors.append(_("Invalid group range: {g}").format(g=group)) except ValueError: errors.append(_("Invalid group: {g}").format(g=group)) continue else: - errors.append(_("Invalid group: {g}").format(g=group)) + # More than 2 hyphens or non-numeric group so add without interpolating + add_sn(group) # plus signals either # 1: 'start+': expected number of serials, starting at start @@ -495,23 +513,17 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): # case 1 else: - end = start + expected_quantity + end = start + (expected_quantity - len(numbers)) for n in range(start, end): add_sn(n) # no case else: - errors.append(_("Invalid group: {g}").format(g=group)) + errors.append(_("Invalid group sequence: {g}").format(g=group)) # At this point, we assume that the "group" is just a single serial value elif group: - - try: - # First attempt to add as an integer value - add_sn(int(group)) - except (ValueError): - # As a backup, add as a string value - add_sn(group) + add_sn(group) # No valid input group detected else: diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 669628bdea..13f9198d92 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -252,6 +252,31 @@ class TestSerialNumberExtraction(TestCase): sn = e("1, 2, 3, 4, 5", 5, 1) self.assertEqual(len(sn), 5) + # Test partially specifying serials + sn = e("1, 2, 4+", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, 4, 5, 6]) + + # Test groups are not interpolated if enough serials are supplied + sn = e("1, 2, 3, AF5-69H, 5", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, 3, "AF5-69H", 5]) + + # Test groups are not interpolated with more than one hyphen in a word + sn = e("1, 2, TG-4SR-92, 4+", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, "TG-4SR-92", 4, 5]) + + # Test groups are not interpolated with alpha characters + sn = e("1, A-2, 3+", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, "A-2", 3, 4, 5]) + + # Test multiple placeholders + sn = e("1 2 ~ ~ ~", 5, 3) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, 3, 4, 5]) + sn = e("1-5, 10-15", 11, 1) self.assertIn(3, sn) self.assertIn(13, sn) @@ -307,6 +332,10 @@ class TestSerialNumberExtraction(TestCase): with self.assertRaises(ValidationError): e("10, a, 7-70j", 4, 1) + # Test groups are not interpolated with word characters + with self.assertRaises(ValidationError): + e("1, 2, 3, E-5", 5, 1) + def test_combinations(self): e = helpers.extract_serial_numbers