diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 28cebbcd3d..4ec84c7912 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -371,14 +371,10 @@ def ExtractSerialNumbers(serials, expected_quantity): continue else: - try: - n = int(group) - if n in numbers: - errors.append(_("Duplicate serial: {n}".format(n=n))) - else: - numbers.append(n) - except ValueError: - errors.append(_("Invalid group: {g}".format(g=group))) + if group in numbers: + errors.append(_("Duplicate serial: {g}".format(g=group))) + else: + numbers.append(group) if len(errors) > 0: raise ValidationError(errors) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 2124207c7d..88dc66085f 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -197,12 +197,8 @@ class BuildComplete(AjaxUpdateView): if not build.part.trackable: form.fields.pop('serial_numbers') else: - if build.quantity == 1: - text = _('Next available serial number is') - else: - text = _('Next available serial numbers are') - form.field_placeholder['serial_numbers'] = text + " " + build.part.getSerialNumberString(build.quantity) + form.field_placeholder['serial_numbers'] = build.part.getSerialNumberString(build.quantity) form.rebuild_layout() diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 26dd04ac52..fea2f56f12 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -319,52 +319,75 @@ class Part(MPTTModel): return stock.exists() - def getHighestSerialNumber(self): + def getLatestSerialNumber(self): """ - Return the highest serial number for this Part. + Return the "latest" serial number for this Part. + + If *all* the serial numbers are integers, then this will return the highest one. + Otherwise, it will simply return the serial number most recently added. Note: Serial numbers must be unique across an entire Part "tree", so we filter by the entire tree. """ parts = Part.objects.filter(tree_id=self.tree_id) - stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None).order_by('-serial') - - if stock.count() > 0: - return stock.first().serial + stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None) + # There are no matchin StockItem objects (skip further tests) + if not stock.exists(): + return None + + # Attempt to coerce the returned serial numbers to integers + # If *any* are not integers, fail! + try: + ordered = sorted(stock.all(), reverse=True, key=lambda n: int(n.serial)) + + if len(ordered) > 0: + return ordered[0].serial + + # One or more of the serial numbers was non-numeric + # In this case, the "best" we can do is return the most recent + except ValueError: + return stock.last().serial + # No serial numbers found return None - def getNextSerialNumber(self): - """ - Return the next-available serial number for this Part. - """ - - n = self.getHighestSerialNumber() - - if n is None: - return 1 - else: - return n + 1 - - def getSerialNumberString(self, quantity): + def getSerialNumberString(self, quantity=1): """ Return a formatted string representing the next available serial numbers, given a certain quantity of items. """ - sn = self.getNextSerialNumber() + latest = self.getLatestSerialNumber() + + quantity = int(quantity) + + # No serial numbers can be found, assume 1 as the first serial + if latest is None: + latest = 0 + + # Attempt to turn into an integer + try: + latest = int(latest) + except: + pass + + if type(latest) is int: + + if quantity >= 2: + text = '{n} - {m}'.format(n=latest + 1, m=latest + 1 + quantity) + + return _('Next available serial numbers are') + ' ' + text + else: + text = str(latest) + + return _('Next available serial number is') + ' ' + text - if quantity >= 2: - sn = "{n}-{m}".format( - n=sn, - m=int(sn + quantity - 1) - ) else: - sn = str(sn) + # Non-integer values, no option but to return latest - return sn + return _('Most recent serial number is') + ' ' + str(latest) @property def full_name(self): diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 18970fe373..eee68c6d6d 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -36,8 +36,14 @@ {% if part.trackable %} - {% trans "Next Serial Number" %} - {{ part.getNextSerialNumber }} + {% trans "Latest Serial Number" %} + + {% if part.getLatestSerialNumber %} + {{ part.getLatestSerialNumber }} + {% else %} + {% trans "No serial numbers recorded" %} + {% endif %} + {% endif %} diff --git a/InvenTree/stock/migrations/0050_auto_20200821_1403.py b/InvenTree/stock/migrations/0050_auto_20200821_1403.py new file mode 100644 index 0000000000..fa02c0d0f7 --- /dev/null +++ b/InvenTree/stock/migrations/0050_auto_20200821_1403.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-08-21 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0049_auto_20200820_0454'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='serial', + field=models.CharField(blank=True, help_text='Serial number for this item', max_length=100, null=True, verbose_name='Serial Number'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 49c185220f..c10295ee6a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -355,9 +355,9 @@ class StockItem(MPTTModel): verbose_name=_("Customer"), ) - serial = models.PositiveIntegerField( + serial = models.CharField( verbose_name=_('Serial Number'), - blank=True, null=True, + max_length=100, blank=True, null=True, help_text=_('Serial number for this item') ) @@ -687,9 +687,6 @@ class StockItem(MPTTModel): if not type(serials) in [list, tuple]: raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")}) - if any([type(i) is not int for i in serials]): - raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")}) - if not quantity == len(serials): raise ValidationError({"quantity": _("Quantity does not match serial numbers")}) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index cccc138523..f50b8572c9 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -113,7 +113,7 @@ class StockItemSerializer(InvenTreeModelSerializer): allocated = serializers.FloatField(source='allocation_count', required=False) - serial = serializers.IntegerField(required=False) + serial = serializers.CharField(required=False) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False) diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html new file mode 100644 index 0000000000..c7de8c74b2 --- /dev/null +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -0,0 +1 @@ +{% extends "modal_form.html" %} \ No newline at end of file diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 513368c422..1001ed12ea 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -295,10 +295,7 @@ class StockTest(TestCase): with self.assertRaises(ValidationError): item.serializeStock(-1, [], self.user) - # Try invalid serial numbers - with self.assertRaises(ValidationError): - item.serializeStock(3, [1, 2, 'k'], self.user) - + # Not enough serial numbers for all stock items. with self.assertRaises(ValidationError): item.serializeStock(3, "hello", self.user) @@ -375,14 +372,14 @@ class VariantTest(StockTest): self.assertFalse(chair.checkIfSerialNumberExists(30)) - self.assertEqual(chair.getNextSerialNumber(), 23) + self.assertEqual(chair.getLatestSerialNumber(), '22') # Same operations on a sub-item variant = Part.objects.get(pk=10003) - self.assertEqual(variant.getNextSerialNumber(), 23) + self.assertEqual(variant.getLatestSerialNumber(), '22') # Create a new serial number - n = variant.getHighestSerialNumber() + n = variant.getLatestSerialNumber() item = StockItem( part=variant, @@ -394,8 +391,14 @@ class VariantTest(StockTest): with self.assertRaises(ValidationError): item.save() - # This should pass - item.serial = n + 1 + # Verify items with a non-numeric serial don't offer a next serial. + item.serial = "string" + item.save() + + self.assertEqual(variant.getLatestSerialNumber(), "string") + + # This should pass, although not strictly an int field now. + item.serial = int(n) + 1 item.save() # Attempt to create the same serial number but for a variant (should fail!) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index acbc9c0863..2833a47af9 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1234,8 +1234,9 @@ class StockItemCreate(AjaxCreateView): part = self.get_part(form=form) if part is not None: - sn = part.getNextSerialNumber() - form.field_placeholder['serial_numbers'] = _('Next available serial number is') + ' ' + str(sn) + + # Add placeholder text for the serial number field + form.field_placeholder['serial_numbers'] = part.getSerialNumberString() form.rebuild_layout() @@ -1353,11 +1354,6 @@ class StockItemCreate(AjaxCreateView): part = Part.objects.get(id=part_id) quantity = Decimal(form['quantity'].value()) - sn = part.getNextSerialNumber() - form.field_placeholder['serial_numbers'] = _("Next available serial number is") + " " + str(sn) - - form.rebuild_layout() - except (Part.DoesNotExist, ValueError, InvalidOperation): part = None quantity = 1