mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
d3909ef766
@ -404,21 +404,28 @@ def DownloadFile(data, filename, content_type='application/text', inline=False):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def extract_serial_numbers(serials, expected_quantity):
|
def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||||
""" Attempt to extract serial numbers from an input string.
|
""" Attempt to extract serial numbers from an input string.
|
||||||
- Serial numbers must be integer values
|
- Serial numbers must be integer values
|
||||||
- Serial numbers must be positive
|
- Serial numbers must be positive
|
||||||
- Serial numbers can be split by whitespace / newline / commma chars
|
- Serial numbers can be split by whitespace / newline / commma chars
|
||||||
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
||||||
|
- Serial numbers can be defined as ~ for getting the next available serial number
|
||||||
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
||||||
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
serials: input string with patterns
|
||||||
expected_quantity: The number of (unique) serial numbers we expect
|
expected_quantity: The number of (unique) serial numbers we expect
|
||||||
|
next_number(int): the next possible serial number
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serials = serials.strip()
|
serials = serials.strip()
|
||||||
|
|
||||||
|
# fill in the next serial number into the serial
|
||||||
|
if '~' in serials:
|
||||||
|
serials = serials.replace('~', str(next_number))
|
||||||
|
|
||||||
groups = re.split("[\s,]+", serials)
|
groups = re.split("[\s,]+", serials)
|
||||||
|
|
||||||
numbers = []
|
numbers = []
|
||||||
@ -493,11 +500,20 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Group should be a number
|
||||||
|
elif group:
|
||||||
|
# try conversion
|
||||||
|
try:
|
||||||
|
number = int(group)
|
||||||
|
except:
|
||||||
|
# seem like it is not a number
|
||||||
|
raise ValidationError(_(f"Invalid group {group}"))
|
||||||
|
|
||||||
|
number_add(number)
|
||||||
|
|
||||||
|
# No valid input group detected
|
||||||
else:
|
else:
|
||||||
if group in numbers:
|
raise ValidationError(_(f"Invalid/no group {group}"))
|
||||||
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
|
||||||
else:
|
|
||||||
numbers.append(group)
|
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
@ -237,25 +237,41 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
|
|
||||||
e = helpers.extract_serial_numbers
|
e = helpers.extract_serial_numbers
|
||||||
|
|
||||||
sn = e("1-5", 5)
|
sn = e("1-5", 5, 1)
|
||||||
self.assertEqual(len(sn), 5)
|
self.assertEqual(len(sn), 5, 1)
|
||||||
for i in range(1, 6):
|
for i in range(1, 6):
|
||||||
self.assertIn(i, sn)
|
self.assertIn(i, sn)
|
||||||
|
|
||||||
sn = e("1, 2, 3, 4, 5", 5)
|
sn = e("1, 2, 3, 4, 5", 5, 1)
|
||||||
self.assertEqual(len(sn), 5)
|
self.assertEqual(len(sn), 5)
|
||||||
|
|
||||||
sn = e("1-5, 10-15", 11)
|
sn = e("1-5, 10-15", 11, 1)
|
||||||
self.assertIn(3, sn)
|
self.assertIn(3, sn)
|
||||||
self.assertIn(13, sn)
|
self.assertIn(13, sn)
|
||||||
|
|
||||||
sn = e("1+", 10)
|
sn = e("1+", 10, 1)
|
||||||
self.assertEqual(len(sn), 10)
|
self.assertEqual(len(sn), 10)
|
||||||
self.assertEqual(sn, [_ for _ in range(1, 11)])
|
self.assertEqual(sn, [_ for _ in range(1, 11)])
|
||||||
|
|
||||||
sn = e("4, 1+2", 4)
|
sn = e("4, 1+2", 4, 1)
|
||||||
self.assertEqual(len(sn), 4)
|
self.assertEqual(len(sn), 4)
|
||||||
self.assertEqual(sn, ["4", 1, 2, 3])
|
self.assertEqual(sn, [4, 1, 2, 3])
|
||||||
|
|
||||||
|
sn = e("~", 1, 1)
|
||||||
|
self.assertEqual(len(sn), 1)
|
||||||
|
self.assertEqual(sn, [1])
|
||||||
|
|
||||||
|
sn = e("~", 1, 3)
|
||||||
|
self.assertEqual(len(sn), 1)
|
||||||
|
self.assertEqual(sn, [3])
|
||||||
|
|
||||||
|
sn = e("~+", 2, 5)
|
||||||
|
self.assertEqual(len(sn), 2)
|
||||||
|
self.assertEqual(sn, [5, 6])
|
||||||
|
|
||||||
|
sn = e("~+3", 4, 5)
|
||||||
|
self.assertEqual(len(sn), 4)
|
||||||
|
self.assertEqual(sn, [5, 6, 7, 8])
|
||||||
|
|
||||||
def test_failures(self):
|
def test_failures(self):
|
||||||
|
|
||||||
@ -263,26 +279,45 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
|
|
||||||
# Test duplicates
|
# Test duplicates
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("1,2,3,3,3", 5)
|
e("1,2,3,3,3", 5, 1)
|
||||||
|
|
||||||
# Test invalid length
|
# Test invalid length
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("1,2,3", 5)
|
e("1,2,3", 5, 1)
|
||||||
|
|
||||||
# Test empty string
|
# Test empty string
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e(", , ,", 0)
|
e(", , ,", 0, 1)
|
||||||
|
|
||||||
# Test incorrect sign in group
|
# Test incorrect sign in group
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("10-2", 8)
|
e("10-2", 8, 1)
|
||||||
|
|
||||||
# Test invalid group
|
# Test invalid group
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("1-5-10", 10)
|
e("1-5-10", 10, 1)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("10, a, 7-70j", 4)
|
e("10, a, 7-70j", 4, 1)
|
||||||
|
|
||||||
|
def test_combinations(self):
|
||||||
|
e = helpers.extract_serial_numbers
|
||||||
|
|
||||||
|
sn = e("1 3-5 9+2", 7, 1)
|
||||||
|
self.assertEqual(len(sn), 7)
|
||||||
|
self.assertEqual(sn, [1, 3, 4, 5, 9, 10, 11])
|
||||||
|
|
||||||
|
sn = e("1,3-5,9+2", 7, 1)
|
||||||
|
self.assertEqual(len(sn), 7)
|
||||||
|
self.assertEqual(sn, [1, 3, 4, 5, 9, 10, 11])
|
||||||
|
|
||||||
|
sn = e("~+2", 3, 14)
|
||||||
|
self.assertEqual(len(sn), 3)
|
||||||
|
self.assertEqual(sn, [14, 15, 16])
|
||||||
|
|
||||||
|
sn = e("~+", 2, 14)
|
||||||
|
self.assertEqual(len(sn), 2)
|
||||||
|
self.assertEqual(sn, [14, 15])
|
||||||
|
|
||||||
|
|
||||||
class TestVersionNumber(TestCase):
|
class TestVersionNumber(TestCase):
|
||||||
|
@ -109,7 +109,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
# Check that the serial numbers are valid
|
# Check that the serial numbers are valid
|
||||||
if serials:
|
if serials:
|
||||||
try:
|
try:
|
||||||
extracted = extract_serial_numbers(serials, quantity)
|
extracted = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
|
||||||
|
|
||||||
if extracted:
|
if extracted:
|
||||||
# Check for conflicting serial numbers
|
# Check for conflicting serial numbers
|
||||||
@ -143,7 +143,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
serials = data.get('serial_numbers', None)
|
serials = data.get('serial_numbers', None)
|
||||||
|
|
||||||
if serials:
|
if serials:
|
||||||
serial_numbers = extract_serial_numbers(serials, quantity)
|
serial_numbers = extract_serial_numbers(serials, quantity, build.part.getLatestSerialNumberInt())
|
||||||
else:
|
else:
|
||||||
serial_numbers = None
|
serial_numbers = None
|
||||||
|
|
||||||
|
@ -861,7 +861,7 @@ class SOSerialAllocationSerializer(serializers.Serializer):
|
|||||||
part = line_item.part
|
part = line_item.part
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data['serials'] = extract_serial_numbers(serial_numbers, quantity)
|
data['serials'] = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt())
|
||||||
except DjangoValidationError as e:
|
except DjangoValidationError as e:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'serial_numbers': e.messages,
|
'serial_numbers': e.messages,
|
||||||
|
@ -594,6 +594,26 @@ class Part(MPTTModel):
|
|||||||
# No serial numbers found
|
# No serial numbers found
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def getLatestSerialNumberInt(self):
|
||||||
|
"""
|
||||||
|
Return the "latest" serial number for this Part as a integer.
|
||||||
|
If it is not an integer the result is 0
|
||||||
|
"""
|
||||||
|
|
||||||
|
latest = self.getLatestSerialNumber()
|
||||||
|
|
||||||
|
# No serial number = > 0
|
||||||
|
if latest is None:
|
||||||
|
latest = 0
|
||||||
|
|
||||||
|
# Attempt to turn into an integer and return
|
||||||
|
try:
|
||||||
|
latest = int(latest)
|
||||||
|
return latest
|
||||||
|
except:
|
||||||
|
# not an integer so 0
|
||||||
|
return 0
|
||||||
|
|
||||||
def getSerialNumberString(self, quantity=1):
|
def getSerialNumberString(self, quantity=1):
|
||||||
"""
|
"""
|
||||||
Return a formatted string representing the next available serial numbers,
|
Return a formatted string representing the next available serial numbers,
|
||||||
|
@ -480,18 +480,6 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
notes = data.get('notes', '')
|
notes = data.get('notes', '')
|
||||||
|
|
||||||
serials = None
|
|
||||||
|
|
||||||
if serial_numbers:
|
|
||||||
# If serial numbers are specified, check that they match!
|
|
||||||
try:
|
|
||||||
serials = extract_serial_numbers(serial_numbers, data['quantity'])
|
|
||||||
except DjangoValidationError as e:
|
|
||||||
raise ValidationError({
|
|
||||||
'quantity': e.messages,
|
|
||||||
'serial_numbers': e.messages,
|
|
||||||
})
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|
||||||
# Create an initial stock item
|
# Create an initial stock item
|
||||||
@ -507,6 +495,19 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if item.part.default_expiry > 0:
|
if item.part.default_expiry > 0:
|
||||||
item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry)
|
item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry)
|
||||||
|
|
||||||
|
# fetch serial numbers
|
||||||
|
serials = None
|
||||||
|
|
||||||
|
if serial_numbers:
|
||||||
|
# If serial numbers are specified, check that they match!
|
||||||
|
try:
|
||||||
|
serials = extract_serial_numbers(serial_numbers, quantity, item.part.getLatestSerialNumberInt())
|
||||||
|
except DjangoValidationError as e:
|
||||||
|
raise ValidationError({
|
||||||
|
'quantity': e.messages,
|
||||||
|
'serial_numbers': e.messages,
|
||||||
|
})
|
||||||
|
|
||||||
# Finally, save the item (with user information)
|
# Finally, save the item (with user information)
|
||||||
item.save(user=user)
|
item.save(user=user)
|
||||||
|
|
||||||
|
@ -350,7 +350,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
|||||||
serial_numbers = data['serial_numbers']
|
serial_numbers = data['serial_numbers']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
serials = InvenTree.helpers.extract_serial_numbers(serial_numbers, quantity)
|
serials = InvenTree.helpers.extract_serial_numbers(serial_numbers, quantity, item.part.getLatestSerialNumberInt())
|
||||||
except DjangoValidationError as e:
|
except DjangoValidationError as e:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'serial_numbers': e.messages,
|
'serial_numbers': e.messages,
|
||||||
@ -379,6 +379,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
|||||||
serials = InvenTree.helpers.extract_serial_numbers(
|
serials = InvenTree.helpers.extract_serial_numbers(
|
||||||
data['serial_numbers'],
|
data['serial_numbers'],
|
||||||
data['quantity'],
|
data['quantity'],
|
||||||
|
item.part.getLatestSerialNumberInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
item.serializeStock(
|
item.serializeStock(
|
||||||
|
@ -1241,7 +1241,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
if len(sn) > 0:
|
if len(sn) > 0:
|
||||||
try:
|
try:
|
||||||
serials = extract_serial_numbers(sn, quantity)
|
serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt())
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
serials = None
|
serials = None
|
||||||
form.add_error('serial_numbers', e.messages)
|
form.add_error('serial_numbers', e.messages)
|
||||||
@ -1283,7 +1283,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
# Create a single stock item for each provided serial number
|
# Create a single stock item for each provided serial number
|
||||||
if len(sn) > 0:
|
if len(sn) > 0:
|
||||||
serials = extract_serial_numbers(sn, quantity)
|
serials = extract_serial_numbers(sn, quantity, part.getLatestSerialNumberInt())
|
||||||
|
|
||||||
for serial in serials:
|
for serial in serials:
|
||||||
item = StockItem(
|
item = StockItem(
|
||||||
|
Loading…
Reference in New Issue
Block a user