diff --git a/InvenTree/static/script/inventree/api.js b/InvenTree/static/script/inventree/api.js
index 7cd3c3d66c..a2f0e14814 100644
--- a/InvenTree/static/script/inventree/api.js
+++ b/InvenTree/static/script/inventree/api.js
@@ -46,6 +46,12 @@ function inventreeUpdate(url, data={}, options={}) {
data["_is_final"] = true;
}
+ var method = 'put';
+
+ if ('method' in options) {
+ method = options.method;
+ }
+
// Middleware token required for data update
//var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
var csrftoken = getCookie('csrftoken');
@@ -55,7 +61,7 @@ function inventreeUpdate(url, data={}, options={}) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
- type: 'put',
+ type: method,
data: data,
dataType: 'json',
success: function(response, status) {
diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js
new file mode 100644
index 0000000000..77341fe363
--- /dev/null
+++ b/InvenTree/static/script/inventree/stock.js
@@ -0,0 +1,72 @@
+
+function moveStock(rows, options) {
+
+ var modal = '#modal-form';
+
+ if ('modal' in options) {
+ modal = options.modal;
+ }
+
+ if (rows.length == 0) {
+ alert('No stock items selected');
+ return;
+ }
+
+ function doMove(location, parts) {
+ inventreeUpdate("/api/stock/move/",
+ {
+ location: location,
+ parts: parts
+ },
+ {
+ success: function(response) {
+ closeModal(modal);
+ if (options.success) {
+ options.success();
+ }
+ },
+ error: function(error) {
+ alert('error!:\n' + error);
+ },
+ method: 'post'
+ });
+ }
+
+ getStockLocations({},
+ {
+ success: function(response) {
+ openModal(modal);
+ modalSetTitle(modal, "Move " + rows.length + " stock items");
+ modalSetButtonText(modal, "Move");
+
+ // Extact part row info
+ var parts = [];
+
+ for (i = 0; i < rows.length; i++) {
+ parts.push(rows[i].pk);
+ }
+
+ var form = "
+
+ modalSetContent(modal, form);
+ attachSelect(modal);
+
+ $(modal).on('click', '#modal-form-submit', function() {
+ var locId = $(modal).find("#stock-location").val();
+
+ doMove(locId, parts);
+ });
+ },
+ error: function(error) {
+ alert('Error getting stock locations:\n' + error.error);
+ }
+ });
+}
\ No newline at end of file
diff --git a/InvenTree/static/script/modal_form.js b/InvenTree/static/script/modal_form.js
index 931f1e8d6d..952e722f75 100644
--- a/InvenTree/static/script/modal_form.js
+++ b/InvenTree/static/script/modal_form.js
@@ -61,6 +61,9 @@ function modalSetButtonText(modal, text) {
$(modal).find("#modal-form-submit").html(text);
}
+function closeModal(modal='#modal-form') {
+ $(modal).modal('hide');
+}
function openModal(modal, title='', content='') {
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index cc222ba96a..714259b5d1 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -13,6 +13,12 @@ from .serializers import LocationSerializer
from InvenTree.views import TreeSerializer
from InvenTree.serializers import DraftRUDView
+from rest_framework.serializers import ValidationError
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import authentication, permissions
+from django.contrib.auth.models import User
+
class StockCategoryTree(TreeSerializer):
title = 'Stock'
model = StockLocation
@@ -45,6 +51,54 @@ class StockFilter(FilterSet):
fields = ['quantity', 'part', 'location']
+class StockMove(APIView):
+
+ permission_classes = [
+ permissions.IsAuthenticatedOrReadOnly,
+ ]
+
+ def post(self, request, *args, **kwargs):
+
+ data = request.data
+
+ if not u'location' in data:
+ raise ValidationError({'location': 'Destination must be specified'})
+
+ loc_id = data.get(u'location')
+
+ try:
+ location = StockLocation.objects.get(pk=loc_id)
+ except StockLocation.DoesNotExist:
+ raise ValidationError({'location': 'Location does not exist'})
+
+ if not u'parts[]' in data:
+ raise ValidationError({'parts[]': 'Parts list must be specified'})
+
+ part_list = data.getlist(u'parts[]')
+
+ parts = []
+
+ errors = []
+
+ for pid in part_list:
+ try:
+ part = StockItem.objects.get(pk=pid)
+ parts.append(part)
+ except StockItem.DoesNotExist:
+ errors.append({'part': 'Part {id} does not exist'.format(id=part_id)})
+
+ if len(errors) > 0:
+ raise ValidationError(errors)
+
+ for part in parts:
+ part.move(location, request.user)
+
+ return Response({'success': 'Moved {n} parts to {loc}'.format(
+ n=len(parts),
+ loc=location.name
+ )})
+
+
class StockLocationList(generics.ListCreateAPIView):
queryset = StockLocation.objects.all()
@@ -167,6 +221,8 @@ stock_api_urls = [
url(r'location/(?P\d+)/', include(location_endpoints)),
+ url(r'move/?', StockMove.as_view(), name='api-stock-move'),
+
url(r'^tree/?', StockCategoryTree.as_view(), name='api-stock-tree'),
url(r'^.*$', StockList.as_view(), name='api-stock-list'),
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index f15abf869c..33c9f4dddf 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -192,6 +192,23 @@ class StockItem(models.Model):
track.save()
+ @transaction.atomic
+ def move(self, location, user):
+
+ if location == self.location:
+ return
+
+ note = "Moved to {loc}".format(loc=location.name)
+
+ self.location = location
+ self.save()
+
+ self.add_transaction_note('Transfer',
+ user,
+ notes=note,
+ system=True)
+
+
@transaction.atomic
def stocktake(self, count, user):
""" Perform item stocktake.
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html
index 87e83005c5..757014bee7 100644
--- a/InvenTree/stock/templates/stock/location.html
+++ b/InvenTree/stock/templates/stock/location.html
@@ -61,6 +61,7 @@
{% block js_load %}
{{ block.super }}
+
{% endblock %}
{% block js_ready %}
@@ -125,9 +126,12 @@
var items = selectedStock();
- alert('Moving ' + items.length + ' items');
-
- return false;
+ moveStock(items,
+ {
+ success: function() {
+ $("#stock-table").bootstrapTable('refresh');
+ }
+ });
});
$("#multi-item-delete").click(function() {