Merge remote-tracking branch 'inventree/master' into multi-db-unit-test

# Conflicts:
#	InvenTree/build/test_build.py
This commit is contained in:
Oliver Walters 2020-09-02 20:02:22 +10:00
commit bb9e8fa97b
10 changed files with 100 additions and 64 deletions

View File

@ -371,14 +371,10 @@ def ExtractSerialNumbers(serials, expected_quantity):
continue continue
else: else:
try: if group in numbers:
n = int(group) errors.append(_("Duplicate serial: {g}".format(g=group)))
if n in numbers: else:
errors.append(_("Duplicate serial: {n}".format(n=n))) numbers.append(group)
else:
numbers.append(n)
except ValueError:
errors.append(_("Invalid group: {g}".format(g=group)))
if len(errors) > 0: if len(errors) > 0:
raise ValidationError(errors) raise ValidationError(errors)

View File

@ -197,12 +197,8 @@ class BuildComplete(AjaxUpdateView):
if not build.part.trackable: if not build.part.trackable:
form.fields.pop('serial_numbers') form.fields.pop('serial_numbers')
else: 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() form.rebuild_layout()

View File

@ -319,52 +319,75 @@ class Part(MPTTModel):
return stock.exists() 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", Note: Serial numbers must be unique across an entire Part "tree",
so we filter by the entire tree. so we filter by the entire tree.
""" """
parts = Part.objects.filter(tree_id=self.tree_id) parts = Part.objects.filter(tree_id=self.tree_id)
stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None).order_by('-serial') stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None)
if stock.count() > 0:
return stock.first().serial
# 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 # No serial numbers found
return None return None
def getNextSerialNumber(self): def getSerialNumberString(self, quantity=1):
"""
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):
""" """
Return a formatted string representing the next available serial numbers, Return a formatted string representing the next available serial numbers,
given a certain quantity of items. 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: else:
sn = str(sn) # Non-integer values, no option but to return latest
return sn return _('Most recent serial number is') + ' ' + str(latest)
@property @property
def full_name(self): def full_name(self):

View File

@ -36,8 +36,14 @@
{% if part.trackable %} {% if part.trackable %}
<tr> <tr>
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td><b>{% trans "Next Serial Number" %}</b></td> <td><b>{% trans "Latest Serial Number" %}</b></td>
<td>{{ part.getNextSerialNumber }}</td> <td>
{% if part.getLatestSerialNumber %}
{{ part.getLatestSerialNumber }}
{% else %}
{% trans "No serial numbers recorded" %}
{% endif %}
</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>

View File

@ -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'),
),
]

View File

@ -355,9 +355,9 @@ class StockItem(MPTTModel):
verbose_name=_("Customer"), verbose_name=_("Customer"),
) )
serial = models.PositiveIntegerField( serial = models.CharField(
verbose_name=_('Serial Number'), verbose_name=_('Serial Number'),
blank=True, null=True, max_length=100, blank=True, null=True,
help_text=_('Serial number for this item') help_text=_('Serial number for this item')
) )
@ -687,9 +687,6 @@ class StockItem(MPTTModel):
if not type(serials) in [list, tuple]: if not type(serials) in [list, tuple]:
raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")}) 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): if not quantity == len(serials):
raise ValidationError({"quantity": _("Quantity does not match serial numbers")}) raise ValidationError({"quantity": _("Quantity does not match serial numbers")})

View File

@ -113,7 +113,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
allocated = serializers.FloatField(source='allocation_count', required=False) 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) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False)

View File

@ -0,0 +1 @@
{% extends "modal_form.html" %}

View File

@ -295,10 +295,7 @@ class StockTest(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
item.serializeStock(-1, [], self.user) item.serializeStock(-1, [], self.user)
# Try invalid serial numbers # Not enough serial numbers for all stock items.
with self.assertRaises(ValidationError):
item.serializeStock(3, [1, 2, 'k'], self.user)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
item.serializeStock(3, "hello", self.user) item.serializeStock(3, "hello", self.user)
@ -375,14 +372,14 @@ class VariantTest(StockTest):
self.assertFalse(chair.checkIfSerialNumberExists(30)) self.assertFalse(chair.checkIfSerialNumberExists(30))
self.assertEqual(chair.getNextSerialNumber(), 23) self.assertEqual(chair.getLatestSerialNumber(), '22')
# Same operations on a sub-item # Same operations on a sub-item
variant = Part.objects.get(pk=10003) variant = Part.objects.get(pk=10003)
self.assertEqual(variant.getNextSerialNumber(), 23) self.assertEqual(variant.getLatestSerialNumber(), '22')
# Create a new serial number # Create a new serial number
n = variant.getHighestSerialNumber() n = variant.getLatestSerialNumber()
item = StockItem( item = StockItem(
part=variant, part=variant,
@ -394,8 +391,14 @@ class VariantTest(StockTest):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
item.save() item.save()
# This should pass # Verify items with a non-numeric serial don't offer a next serial.
item.serial = n + 1 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() item.save()
# Attempt to create the same serial number but for a variant (should fail!) # Attempt to create the same serial number but for a variant (should fail!)

View File

@ -1234,8 +1234,9 @@ class StockItemCreate(AjaxCreateView):
part = self.get_part(form=form) part = self.get_part(form=form)
if part is not None: 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() form.rebuild_layout()
@ -1353,11 +1354,6 @@ class StockItemCreate(AjaxCreateView):
part = Part.objects.get(id=part_id) part = Part.objects.get(id=part_id)
quantity = Decimal(form['quantity'].value()) 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): except (Part.DoesNotExist, ValueError, InvalidOperation):
part = None part = None
quantity = 1 quantity = 1