mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 04:56:45 +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:
parent
bc03ae53bd
commit
43967e302b
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user