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 + "'/></td>"; + html += "type='number' id='q-update-" + item.pk + "'/></td>"; html += '</tr>'; } @@ -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 += "<p class='warning-msg' id='note-warning'><i>Note field must be filled</i></p>"; - html += "<hr>The following stock items will be moved:<br><ul class='list-group'>\n"; + html += "<hr>The following stock items will be moved:<hr>"; + + html += ` + <table class='table table-striped table-condensed'> + <tr> + <th>Part</th> + <th>Location</th> + <th>Available</th> + <th>Moving</th> + </tr> + `; for (i = 0; i < items.length; i++) { - parts.push(items[i].pk); + + parts.push({ + pk: items[i].pk, + quantity: items[i].quantity, + }); - html += "<li class='list-group-item'>" + items[i].quantity + " × " + items[i].part.name; + var item = items[i]; - if (items[i].location) { - html += " (" + items[i].location.name + ")"; - } + html += "<tr>"; - html += "</li>\n"; + html += "<td>" + item.part.name + "</td>"; + html += "<td>" + item.location.pathstring + "</td>"; + html += "<td>" + item.quantity + "</td>"; + + html += "<td>"; + html += "<input class='form-control' min='0' max='" + item.quantity + "'"; + html += " value='" + item.quantity + "'"; + html += "type='number' id='q-move-" + item.pk + "'/></td>"; + + html += "</tr>"; } - html += "</ul>\n"; + html += "</table>"; 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: