mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Simplify creation of serialized stock via the API
- Fixes atomicity issues - Don't create-then-delete stock items
This commit is contained in:
parent
9153b62ea0
commit
d0aa09337a
@ -465,12 +465,6 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
user = request.user
|
user = request.user
|
||||||
data = request.data
|
data = request.data
|
||||||
|
|
||||||
serializer = self.get_serializer(data=data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
# Check if a set of serial numbers was provided
|
|
||||||
serial_numbers = data.get('serial_numbers', '')
|
|
||||||
|
|
||||||
quantity = data.get('quantity', None)
|
quantity = data.get('quantity', None)
|
||||||
|
|
||||||
if quantity is None:
|
if quantity is None:
|
||||||
@ -478,77 +472,84 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
'quantity': _('Quantity is required'),
|
'quantity': _('Quantity is required'),
|
||||||
})
|
})
|
||||||
|
|
||||||
notes = data.get('notes', '')
|
try:
|
||||||
|
part = Part.objects.get(pk=data.get('part', None))
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
raise ValidationError({
|
||||||
|
'part': _('Valid part ID must be supplied'),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set default location (if not provided)
|
||||||
|
if 'location' not in data:
|
||||||
|
location = part.get_default_location()
|
||||||
|
|
||||||
|
if location:
|
||||||
|
data['location'] = location.pk
|
||||||
|
|
||||||
|
# An expiry date was *not* specified - try to infer it!
|
||||||
|
if 'expiry_date' not in data:
|
||||||
|
|
||||||
|
if part.default_expiry > 0:
|
||||||
|
data['expiry_date'] = datetime.now().date() + timedelta(days=part.default_expiry)
|
||||||
|
|
||||||
|
# Attempt to extract serial numbers from submitted data
|
||||||
|
serials = None
|
||||||
|
|
||||||
|
# Check if a set of serial numbers was provided
|
||||||
|
serial_numbers = data.get('serial_numbers', '')
|
||||||
|
|
||||||
|
# Assign serial numbers for a trackable part
|
||||||
|
if serial_numbers and part.trackable:
|
||||||
|
|
||||||
|
# If serial numbers are specified, check that they match!
|
||||||
|
try:
|
||||||
|
serials = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt())
|
||||||
|
except DjangoValidationError as e:
|
||||||
|
raise ValidationError({
|
||||||
|
'quantity': e.messages,
|
||||||
|
'serial_numbers': e.messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
if serials is not None:
|
||||||
|
"""
|
||||||
|
If the stock item is going to be serialized, set the quantity to 1
|
||||||
|
"""
|
||||||
|
data['quantity'] = 1
|
||||||
|
|
||||||
|
# De-serialize the provided data
|
||||||
|
serializer = self.get_serializer(data=data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|
||||||
# Create an initial stock item
|
# Create an initial StockItem object
|
||||||
item = serializer.save()
|
item = serializer.save()
|
||||||
|
|
||||||
# A location was *not* specified - try to infer it
|
if serials:
|
||||||
if 'location' not in data:
|
# Assign the first serial number to the "master" item
|
||||||
item.location = item.part.get_default_location()
|
item.serial = serials[0]
|
||||||
|
|
||||||
# An expiry date was *not* specified - try to infer it!
|
# Save the item (with user information)
|
||||||
if 'expiry_date' not in data:
|
|
||||||
|
|
||||||
if item.part.default_expiry > 0:
|
|
||||||
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)
|
|
||||||
item.save(user=user)
|
item.save(user=user)
|
||||||
|
|
||||||
if serials:
|
if serials:
|
||||||
"""
|
for serial in serials[1:]:
|
||||||
Serialize the stock, if required
|
|
||||||
|
|
||||||
- Note that the "original" stock item needs to be created first, so it can be serialized
|
# Create a duplicate stock item with the next serial number
|
||||||
- It is then immediately deleted
|
item.pk = None
|
||||||
"""
|
item.serial = serial
|
||||||
|
|
||||||
try:
|
item.save()
|
||||||
item.serializeStock(
|
|
||||||
quantity,
|
|
||||||
serials,
|
|
||||||
user,
|
|
||||||
notes=notes,
|
|
||||||
location=item.location,
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
response_data = {
|
||||||
|
'quantity': quantity,
|
||||||
|
'serial_numbers': serials,
|
||||||
|
}
|
||||||
|
|
||||||
# Delete the original item
|
else:
|
||||||
item.delete()
|
response_data = serializer.data
|
||||||
|
|
||||||
response_data = {
|
return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data))
|
||||||
'quantity': quantity,
|
|
||||||
'serial_numbers': serials,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
|
|
||||||
|
|
||||||
except DjangoValidationError as e:
|
|
||||||
raise ValidationError({
|
|
||||||
'quantity': e.messages,
|
|
||||||
'serial_numbers': e.messages,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Return a response
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -380,6 +380,67 @@ class StockItemTest(StockAPITestCase):
|
|||||||
expected_code=201
|
expected_code=201
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_creation_with_serials(self):
|
||||||
|
"""
|
||||||
|
Test that serialized stock items can be created via the API,
|
||||||
|
"""
|
||||||
|
|
||||||
|
trackable_part = part.models.Part.objects.create(
|
||||||
|
name='My part',
|
||||||
|
description='A trackable part',
|
||||||
|
trackable=True,
|
||||||
|
default_location=StockLocation.objects.get(pk=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(trackable_part.stock_entries().count(), 0)
|
||||||
|
self.assertEqual(trackable_part.get_stock_count(), 0)
|
||||||
|
|
||||||
|
# This should fail, incorrect serial number count
|
||||||
|
response = self.post(
|
||||||
|
self.list_url,
|
||||||
|
data={
|
||||||
|
'part': trackable_part.pk,
|
||||||
|
'quantity': 10,
|
||||||
|
'serial_numbers': '1-20',
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
self.list_url,
|
||||||
|
data={
|
||||||
|
'part': trackable_part.pk,
|
||||||
|
'quantity': 10,
|
||||||
|
'serial_numbers': '1-10',
|
||||||
|
},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
|
||||||
|
self.assertEqual(data['quantity'], 10)
|
||||||
|
sn = data['serial_numbers']
|
||||||
|
|
||||||
|
# Check that each serial number was created
|
||||||
|
for i in range(1, 11):
|
||||||
|
self.assertTrue(i in sn)
|
||||||
|
|
||||||
|
# Check the unique stock item has been created
|
||||||
|
|
||||||
|
item = StockItem.objects.get(
|
||||||
|
part=trackable_part,
|
||||||
|
serial=str(i),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Item location should have been set automatically
|
||||||
|
self.assertIsNotNone(item.location)
|
||||||
|
|
||||||
|
self.assertEqual(str(i), item.serial)
|
||||||
|
|
||||||
|
# There now should be 10 unique stock entries for this part
|
||||||
|
self.assertEqual(trackable_part.stock_entries().count(), 10)
|
||||||
|
self.assertEqual(trackable_part.get_stock_count(), 10)
|
||||||
|
|
||||||
def test_default_expiry(self):
|
def test_default_expiry(self):
|
||||||
"""
|
"""
|
||||||
Test that the "default_expiry" functionality works via the API.
|
Test that the "default_expiry" functionality works via the API.
|
||||||
|
Loading…
Reference in New Issue
Block a user