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/test_build.py b/InvenTree/build/test_build.py
index 32ad33dab3..bb7931d2f1 100644
--- a/InvenTree/build/test_build.py
+++ b/InvenTree/build/test_build.py
@@ -220,5 +220,5 @@ class BuildTest(TestCase):
# And a new stock item created for the build output
self.assertEqual(StockItem.objects.get(pk=7).quantity, 1)
- self.assertEqual(StockItem.objects.get(pk=7).serial, 1)
+ self.assertEqual(StockItem.objects.get(pk=7).serial, "1")
self.assertEqual(StockItem.objects.get(pk=7).build, self.build)
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