mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +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' |     name: 'Widget' | ||||||
|     description: 'A watchamacallit' |     description: 'A watchamacallit' | ||||||
|     category: 7 |     category: 7 | ||||||
|  |     trackable: true | ||||||
|  |  | ||||||
| - model: part.part | - model: part.part | ||||||
|   pk: 50 |   pk: 50 | ||||||
|   | |||||||
| @@ -843,7 +843,8 @@ class Part(models.Model): | |||||||
|         # Copy the BOM data |         # Copy the BOM data | ||||||
|         if kwargs.get('bom', False): |         if kwargs.get('bom', False): | ||||||
|             for item in other.bom_items.all(): |             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.part = self | ||||||
|                 item.pk = None |                 item.pk = None | ||||||
|                 item.save() |                 item.save() | ||||||
|   | |||||||
| @@ -138,6 +138,12 @@ class StockItem(models.Model): | |||||||
|         if not part.trackable: |         if not part.trackable: | ||||||
|             return False |             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) |         items = StockItem.objects.filter(serial=serial_number) | ||||||
|  |  | ||||||
|         # Is this part a variant? If so, check S/N across all sibling variants |         # Is this part a variant? If so, check S/N across all sibling variants | ||||||
| @@ -367,12 +373,91 @@ class StockItem(models.Model): | |||||||
|         track.save() |         track.save() | ||||||
|  |  | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def serializeStock(self, serials, user): |     def serializeStock(self, quantity, serials, user, location=None): | ||||||
|         """ Split this stock item into unique serial numbers. |         """ 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 |         # Cannot serialize stock that is already serialized! | ||||||
|         pass |         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 |     @transaction.atomic | ||||||
|     def splitStock(self, quantity, user): |     def splitStock(self, quantity, user): | ||||||
| @@ -414,6 +499,9 @@ class StockItem(models.Model): | |||||||
|  |  | ||||||
|         new_stock.save() |         new_stock.save() | ||||||
|  |  | ||||||
|  |         # Copy the transaction history | ||||||
|  |         new_stock.copyHistoryFrom(self) | ||||||
|  |  | ||||||
|         # Add a new tracking item for the new stock item |         # Add a new tracking item for the new stock item | ||||||
|         new_stock.addTransactionNote( |         new_stock.addTransactionNote( | ||||||
|             "Split from existing stock", |             "Split from existing stock", | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.db.models import Sum | 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 .models import StockLocation, StockItem, StockItemTracking | ||||||
| from part.models import Part | from part.models import Part | ||||||
| @@ -28,6 +30,14 @@ class StockTest(TestCase): | |||||||
|         self.drawer2 = StockLocation.objects.get(name='Drawer_2') |         self.drawer2 = StockLocation.objects.get(name='Drawer_2') | ||||||
|         self.drawer3 = StockLocation.objects.get(name='Drawer_3') |         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): |     def test_loc_count(self): | ||||||
|         self.assertEqual(StockLocation.objects.count(), 7) |         self.assertEqual(StockLocation.objects.count(), 7) | ||||||
|  |  | ||||||
| @@ -244,3 +254,71 @@ class StockTest(TestCase): | |||||||
|  |  | ||||||
|         with self.assertRaises(StockItem.DoesNotExist): |         with self.assertRaises(StockItem.DoesNotExist): | ||||||
|             w2 = StockItem.objects.get(pk=101) |             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