mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #948 from SchrodingersGat/bcc-feature-non-int-serial
Bcc feature non int serial
This commit is contained in:
commit
cae6fb6731
@ -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:
|
|
||||||
errors.append(_("Duplicate serial: {n}".format(n=n)))
|
|
||||||
else:
|
else:
|
||||||
numbers.append(n)
|
numbers.append(group)
|
||||||
except ValueError:
|
|
||||||
errors.append(_("Invalid group: {g}".format(g=group)))
|
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
@ -220,5 +220,5 @@ class BuildTest(TestCase):
|
|||||||
|
|
||||||
# And a new stock item created for the build output
|
# 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).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)
|
self.assertEqual(StockItem.objects.get(pk=7).build, self.build)
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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:
|
# There are no matchin StockItem objects (skip further tests)
|
||||||
return stock.first().serial
|
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:
|
if quantity >= 2:
|
||||||
sn = "{n}-{m}".format(
|
text = '{n} - {m}'.format(n=latest + 1, m=latest + 1 + quantity)
|
||||||
n=sn,
|
|
||||||
m=int(sn + quantity - 1)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
sn = str(sn)
|
|
||||||
|
|
||||||
return sn
|
return _('Next available serial numbers are') + ' ' + text
|
||||||
|
else:
|
||||||
|
text = str(latest)
|
||||||
|
|
||||||
|
return _('Next available serial number is') + ' ' + text
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Non-integer values, no option but to return latest
|
||||||
|
|
||||||
|
return _('Most recent serial number is') + ' ' + str(latest)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
|
@ -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>
|
||||||
|
18
InvenTree/stock/migrations/0050_auto_20200821_1403.py
Normal file
18
InvenTree/stock/migrations/0050_auto_20200821_1403.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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")})
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
1
InvenTree/stock/templates/stock/stock_move.html
Normal file
1
InvenTree/stock/templates/stock/stock_move.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
@ -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!)
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user