diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js
index 44f563aa51..ddb20ad1f1 100644
--- a/InvenTree/static/script/inventree/stock.js
+++ b/InvenTree/static/script/inventree/stock.js
@@ -79,7 +79,7 @@ function updateStock(items, options={}) {
html += "max='" + vMax + "' ";
}
- html += "type='number' id='q-" + item.pk + "'/>";
+ html += "type='number' id='q-update-" + item.pk + "'/>";
html += '';
}
@@ -128,7 +128,7 @@ function updateStock(items, options={}) {
for (idx = 0; idx < items.length; idx++) {
var item = items[idx];
- var q = $(modal).find("#q-" + item.pk).val();
+ var q = $(modal).find("#q-update-" + item.pk).val();
stocktake.push({
pk: item.pk,
@@ -229,7 +229,7 @@ function moveStockItems(items, options) {
inventreePut("/api/stock/move/",
{
location: location,
- 'parts[]': parts,
+ 'stock': parts,
'notes': notes,
},
{
@@ -246,7 +246,6 @@ function moveStockItems(items, options) {
getStockLocations({},
{
success: function(response) {
-
// Extact part row info
var parts = [];
@@ -267,21 +266,42 @@ function moveStockItems(items, options) {
html += "
Note field must be filled
";
- html += "
The following stock items will be moved:
\n";
+ html += "
The following stock items will be moved:
";
+
+ html += `
+ ";
openModal({
modal: modal,
@@ -307,6 +327,15 @@ function moveStockItems(items, options) {
return false;
}
+ // Update the quantity for each item
+ for (var ii = 0; ii < parts.length; ii++) {
+ var pk = parts[ii].pk;
+
+ var q = $(modal).find('#q-move-' + pk).val();
+
+ parts[ii].quantity = q;
+ }
+
doMove(locId, parts, notes);
});
},
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 070e7657a6..0bd048087e 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -151,46 +151,50 @@ class StockMove(APIView):
data = request.data
- if u'location' not in data:
+ if 'location' not in data:
raise ValidationError({'location': 'Destination must be specified'})
- loc_id = data.get(u'location')
+ try:
+ loc_id = int(data.get('location'))
+ except ValueError:
+ raise ValidationError({'location': 'Integer ID required'})
try:
location = StockLocation.objects.get(pk=loc_id)
except StockLocation.DoesNotExist:
raise ValidationError({'location': 'Location does not exist'})
- if u'parts[]' not in data:
- raise ValidationError({'parts[]': 'Parts list must be specified'})
+ if 'stock' not in data:
+ raise ValidationError({'stock': 'Stock list must be specified'})
+
+ stock_list = data.get('stock')
- part_list = data.get(u'parts[]')
+ if type(stock_list) is not list:
+ raise ValidationError({'stock': 'Stock must be supplied as a list'})
- parts = []
+ if 'notes' not in data:
+ raise ValidationError({'notes': 'Notes field must be supplied'})
- errors = []
-
- if u'notes' not in data:
- errors.append({'notes': 'Notes field must be supplied'})
-
- for pid in part_list:
+ for item in stock_list:
try:
- part = StockItem.objects.get(pk=pid)
- parts.append(part)
+ stock_id = int(item['pk'])
+ quantity = int(item['quantity'])
+ except ValueError:
+ # Ignore this one
+ continue
+
+ # Ignore a zero quantity movement
+ if quantity <= 0:
+ continue
+
+ try:
+ stock = StockItem.objects.get(pk=stock_id)
except StockItem.DoesNotExist:
- errors.append({'part': 'Part {id} does not exist'.format(id=pid)})
+ continue
- if len(errors) > 0:
- raise ValidationError(errors)
+ stock.move(location, data.get('notes'), request.user, quantity=quantity)
- n = 0
-
- for part in parts:
- if part.move(location, data.get('notes'), request.user):
- n += 1
-
- return Response({'success': 'Moved {n} parts to {loc}'.format(
- n=n,
+ return Response({'success': 'Moved parts to {loc}'.format(
loc=str(location)
)})
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index b4216c46c1..f24516bfcb 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -281,7 +281,64 @@ class StockItem(models.Model):
track.save()
@transaction.atomic
- def move(self, location, notes, user):
+ def splitStock(self, quantity, user):
+ """ Split this stock item into two items, in the same location.
+ Stock tracking notes for this StockItem will be duplicated,
+ and added to the new StockItem.
+
+ Args:
+ quantity: Number of stock items to remove from this entity, and pass to the next
+
+ Notes:
+ The provided quantity will be subtracted from this item and given to the new one.
+ The new item will have a different StockItem ID, while this will remain the same.
+ """
+
+ # Doesn't make sense for a zero quantity
+ if quantity <= 0:
+ return
+
+ # Also doesn't make sense to split the full amount
+ if quantity >= self.quantity:
+ return
+
+ # Create a new StockItem object, duplicating relevant fields
+ new_stock = StockItem.objects.create(
+ part=self.part,
+ quantity=quantity,
+ supplier_part=self.supplier_part,
+ location=self.location,
+ batch=self.batch,
+ delete_on_deplete=self.delete_on_deplete
+ )
+
+ new_stock.save()
+
+ # Add a new tracking item for the new stock item
+ new_stock.addTransactionNote(
+ "Split from existing stock",
+ user,
+ "Split {n} from existing stock item".format(n=quantity))
+
+ # Remove the specified quantity from THIS stock item
+ self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity))
+
+ @transaction.atomic
+ def move(self, location, notes, user, **kwargs):
+ """ Move part to a new location.
+
+ Args:
+ location: Destination location (cannot be null)
+ notes: User notes
+ user: Who is performing the move
+ kwargs:
+ quantity: If provided, override the quantity (default = total stock quantity)
+ """
+
+ quantity = int(kwargs.get('quantity', self.quantity))
+
+ if quantity <= 0:
+ return False
if location is None:
# TODO - Raise appropriate error (cannot move to blank location)
@@ -290,6 +347,13 @@ class StockItem(models.Model):
# TODO - Raise appropriate error (cannot move to same location)
return False
+ # Test for a partial movement
+ if quantity < self.quantity:
+ # We need to split the stock!
+
+ # Leave behind certain quantity
+ self.splitStock(self.quantity - quantity, user)
+
msg = "Moved to {loc}".format(loc=str(location))
if self.location: