mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Add ablity to serialize an existing quantity of stock
- Do not have to serialize all the stock - Add tests - Add function to copy entire stock transaction history
This commit is contained in:
		| @@ -43,6 +43,7 @@ | ||||
|     name: 'Widget' | ||||
|     description: 'A watchamacallit' | ||||
|     category: 7 | ||||
|     trackable: true | ||||
|  | ||||
| - model: part.part | ||||
|   pk: 50 | ||||
|   | ||||
| @@ -843,7 +843,8 @@ class Part(models.Model): | ||||
|         # Copy the BOM data | ||||
|         if kwargs.get('bom', False): | ||||
|             for item in other.bom_items.all(): | ||||
|                 # Point the item to THIS part | ||||
|                 # Point the item to THIS part. | ||||
|                 # Set the pk to None so a new entry is created. | ||||
|                 item.part = self | ||||
|                 item.pk = None | ||||
|                 item.save() | ||||
|   | ||||
| @@ -138,6 +138,12 @@ class StockItem(models.Model): | ||||
|         if not part.trackable: | ||||
|             return False | ||||
|  | ||||
|         # Return False if an invalid serial number is supplied | ||||
|         try: | ||||
|             serial_number = int(serial_number) | ||||
|         except ValueError: | ||||
|             return False | ||||
|  | ||||
|         items = StockItem.objects.filter(serial=serial_number) | ||||
|  | ||||
|         # Is this part a variant? If so, check S/N across all sibling variants | ||||
| @@ -367,12 +373,91 @@ class StockItem(models.Model): | ||||
|         track.save() | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def serializeStock(self, serials, user): | ||||
|     def serializeStock(self, quantity, serials, user, location=None): | ||||
|         """ Split this stock item into unique serial numbers. | ||||
|  | ||||
|         - Quantity can be less than or equal to the quantity of the stock item | ||||
|         - Number of serial numbers must match the quantity | ||||
|         - Provided serial numbers must not already be in use | ||||
|  | ||||
|         Args: | ||||
|             quantity: Number of items to serialize (integer) | ||||
|             serials: List of serial numbers (list<int>) | ||||
|             user: User object associated with action | ||||
|             location: If specified, serialized items will be placed in the given location | ||||
|         """ | ||||
|  | ||||
|         # TODO | ||||
|         pass | ||||
|         # Cannot serialize stock that is already serialized! | ||||
|         if self.serialized: | ||||
|             return | ||||
|  | ||||
|         # Do not serialize non-trackable parts | ||||
|         if not self.part.trackable: | ||||
|             raise ValueError({"part": _("Cannot serialize a non-trackable part")}) | ||||
|  | ||||
|         # Quantity must be a valid integer value | ||||
|         try: | ||||
|             quantity = int(quantity) | ||||
|         except ValueError: | ||||
|             raise ValueError({"quantity": _("Quantity must be integer")}) | ||||
|  | ||||
|         if quantity <= 0: | ||||
|             raise ValueError({"quantity": _("Quantity must be greater than zero")}) | ||||
|  | ||||
|         if quantity > self.quantity: | ||||
|             raise ValidationError({"quantity": _("Quantity must not exceed stock quantity")}) | ||||
|  | ||||
|         if not type(serials) in [list, tuple]: | ||||
|             raise ValueError({"serials": _("Serial numbers must be a list of integers")}) | ||||
|  | ||||
|         if any([type(i) is not int for i in serials]): | ||||
|             raise ValueError({"serials": _("Serial numbers must be a list of integers")}) | ||||
|  | ||||
|         if not quantity == len(serials): | ||||
|             raise ValueError({"quantity": _("Quantity does not match serial numbers")}) | ||||
|  | ||||
|         # Test if each of the serial numbers are valid | ||||
|         existing = [] | ||||
|  | ||||
|         for serial in serials: | ||||
|             if not StockItem.check_serial_number(self.part, serial): | ||||
|                 existing.append(serial) | ||||
|  | ||||
|         if len(existing) > 0: | ||||
|             raise ValidationError({"serials": _("Serial numbers already exist: ") + str(existing)}) | ||||
|  | ||||
|         # Create a new stock item for each unique serial number | ||||
|         for serial in serials: | ||||
|              | ||||
|             # Create a copy of this StockItem | ||||
|             new_item = StockItem.objects.get(pk=self.pk) | ||||
|             new_item.quantity = 1 | ||||
|             new_item.serial = serial | ||||
|             new_item.pk = None | ||||
|  | ||||
|             if location: | ||||
|                 new_item.location = location | ||||
|  | ||||
|             new_item.save() | ||||
|  | ||||
|             # Copy entire transaction history | ||||
|             new_item.copyHistoryFrom(self) | ||||
|  | ||||
|             # Create a new stock tracking item | ||||
|             self.addTransactionNote(_('Add serial number'), user) | ||||
|  | ||||
|         # Remove the equivalent number of items | ||||
|         self.take_stock(quantity, user, notes=_('Serialized {n} items'.format(n=quantity))) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def copyHistoryFrom(self, other): | ||||
|         """ Copy stock history from another part """ | ||||
|  | ||||
|         for item in other.tracking_info.all(): | ||||
|              | ||||
|             item.item = self | ||||
|             item.pk = None | ||||
|             item.save() | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def splitStock(self, quantity, user): | ||||
| @@ -414,6 +499,9 @@ class StockItem(models.Model): | ||||
|  | ||||
|         new_stock.save() | ||||
|  | ||||
|         # Copy the transaction history | ||||
|         new_stock.copyHistoryFrom(self) | ||||
|  | ||||
|         # Add a new tracking item for the new stock item | ||||
|         new_stock.addTransactionNote( | ||||
|             "Split from existing stock", | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| from django.test import TestCase | ||||
| from django.db.models import Sum | ||||
| from django.contrib.auth import get_user_model | ||||
| from django.core.exceptions import ValidationError | ||||
|  | ||||
| from .models import StockLocation, StockItem, StockItemTracking | ||||
| from part.models import Part | ||||
| @@ -28,6 +30,14 @@ class StockTest(TestCase): | ||||
|         self.drawer2 = StockLocation.objects.get(name='Drawer_2') | ||||
|         self.drawer3 = StockLocation.objects.get(name='Drawer_3') | ||||
|  | ||||
|         # Create a user | ||||
|         User = get_user_model() | ||||
|         User.objects.create_user('username', 'user@email.com', 'password') | ||||
|  | ||||
|         self.client.login(username='username', password='password') | ||||
|  | ||||
|         self.user = User.objects.get(username='username') | ||||
|  | ||||
|     def test_loc_count(self): | ||||
|         self.assertEqual(StockLocation.objects.count(), 7) | ||||
|  | ||||
| @@ -244,3 +254,71 @@ class StockTest(TestCase): | ||||
|  | ||||
|         with self.assertRaises(StockItem.DoesNotExist): | ||||
|             w2 = StockItem.objects.get(pk=101) | ||||
|  | ||||
|     def test_serialize_stock_invalid(self): | ||||
|         """ | ||||
|         Test manual serialization of parts. | ||||
|         Each of these tests should fail | ||||
|         """ | ||||
|  | ||||
|         # Test serialization of non-serializable part | ||||
|         item = StockItem.objects.get(pk=1234) | ||||
|  | ||||
|         with self.assertRaises(ValueError): | ||||
|             item.serializeStock(5, [1, 2, 3, 4, 5], self.user) | ||||
|  | ||||
|         # Pick a StockItem which can actually be serialized | ||||
|         item = StockItem.objects.get(pk=100) | ||||
|  | ||||
|         # Try an invalid quantity | ||||
|         with self.assertRaises(ValueError): | ||||
|             item.serializeStock("k", [], self.user) | ||||
|  | ||||
|         with self.assertRaises(ValueError): | ||||
|             item.serializeStock(-1, [], self.user) | ||||
|  | ||||
|         # Try invalid serial numbers | ||||
|         with self.assertRaises(ValueError): | ||||
|             item.serializeStock(3, [1, 2, 'k'], self.user) | ||||
|  | ||||
|         with self.assertRaises(ValueError): | ||||
|             item.serializeStock(3, "hello", self.user) | ||||
|  | ||||
|     def test_seiralize_stock_valid(self): | ||||
|         """ Perform valid stock serializations """ | ||||
|  | ||||
|         # There are 10 of these in stock | ||||
|         # Item will deplete when deleted | ||||
|         item = StockItem.objects.get(pk=100) | ||||
|         item.delete_on_deplete = True | ||||
|         item.save() | ||||
|  | ||||
|         n = StockItem.objects.filter(part=25).count() | ||||
|  | ||||
|         self.assertEqual(item.quantity, 10) | ||||
|  | ||||
|         item.serializeStock(3, [1, 2, 3], self.user) | ||||
|  | ||||
|         self.assertEqual(item.quantity, 7) | ||||
|  | ||||
|         # Try to serialize again (with same serial numbers) | ||||
|         with self.assertRaises(ValidationError): | ||||
|             item.serializeStock(3, [1, 2, 3], self.user) | ||||
|  | ||||
|         # Try to serialize too many items | ||||
|         with self.assertRaises(ValidationError): | ||||
|             item.serializeStock(13, [1, 2, 3], self.user) | ||||
|  | ||||
|         # Serialize some more stock | ||||
|         item.serializeStock(5, [6, 7, 8, 9, 10], self.user) | ||||
|  | ||||
|         self.assertEqual(item.quantity, 2) | ||||
|  | ||||
|         # There should be 8 more items now | ||||
|         self.assertEqual(StockItem.objects.filter(part=25).count(), n + 8) | ||||
|  | ||||
|         # Serialize the remainder of the stock | ||||
|         item.serializeStock(2, [99, 100], self.user) | ||||
|  | ||||
|         # Two more items but the original has been deleted | ||||
|         self.assertEqual(StockItem.objects.filter(part=25).count(), n + 9) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user