diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index f6fdbc4d46..338e664252 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -9,7 +9,7 @@ as JSON objects and passing them to modal forms (using jQuery / bootstrap).
 from __future__ import unicode_literals
 
 from django.template.loader import render_to_string
-from django.http import JsonResponse
+from django.http import JsonResponse, HttpResponseRedirect
 
 from django.views import View
 from django.views.generic import UpdateView, CreateView
@@ -132,6 +132,9 @@ class AjaxMixin(object):
             JSON response object
         """
 
+        if not request.is_ajax():
+            return HttpResponseRedirect('/')
+
         if context is None:
             try:
                 context = self.get_context_data()
diff --git a/InvenTree/build/templates/build/allocation_item.html b/InvenTree/build/templates/build/allocation_item.html
index 920012e953..ebe05ce4d4 100644
--- a/InvenTree/build/templates/build/allocation_item.html
+++ b/InvenTree/build/templates/build/allocation_item.html
@@ -6,10 +6,7 @@
 {% block collapse_panel_setup %}class='panel part-allocation' id='allocation-panel-{{ item.sub_part.id }}'{% endblock %}
 
 {% block collapse_title %}
-    
         {{ item.sub_part.full_name }}
         
{{ item.sub_part.description }} 
diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html
index dc2160a006..72d328df9b 100644
--- a/InvenTree/build/templates/build/auto_allocate.html
+++ b/InvenTree/build/templates/build/auto_allocate.html
@@ -21,10 +21,7 @@ Automatically allocate stock to this build?
 {% for item in allocations %}
 
     
-        
-             
+        {% include "hover_image.html" with image=item.stock_item.part.image %}
      
     
         {{ item.stock_item.part.full_name }}  
                     
                         Part 
-                        {{ build.part.full_name }} 
+                        {{ build.part.full_name }}  
                     
                         Quantity 
diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html
index 9f41bee54e..0a8f094614 100644
--- a/InvenTree/build/templates/build/complete.html
+++ b/InvenTree/build/templates/build/complete.html
@@ -18,10 +18,7 @@ The following items will be removed from stock:
 {% for item in taking %}
  
     
-        
-             
+        {% include "hover_image.html" with image=item.stock_item.part.image %}
      
     
         {{ item.stock_item.part.full_name }}
-    
-         
+    {% include "hover_image.html" with image=build.part.image %}
     {{ build.quantity }} x {{ build.part.full_name }}
 
  
         
-            
-                 
+            {% include "hover_image.html" with image=item.part.image %}
             {{ item.part.full_name }} 
          
         {{ item.part.total_stock }} 
diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html
index 98ac08a8d3..8061e32e7a 100644
--- a/InvenTree/part/templates/part/variants.html
+++ b/InvenTree/part/templates/part/variants.html
@@ -33,12 +33,7 @@
     {% for variant in part.variants.all %}
          
             
-                
+                {% include "hover_image.html" with image=variant.image %}
                 {{ variant.full_name }} 
              
             {{ variant.description }} 
diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css
index 0769380fe1..da7d38d250 100644
--- a/InvenTree/static/css/inventree.css
+++ b/InvenTree/static/css/inventree.css
@@ -192,7 +192,7 @@
 }
 
 .modal-dialog {
-    width: 45%;
+    width: 60%;
 }
 
 .modal-secondary .modal-dialog {
@@ -225,6 +225,7 @@
 /* Force a control-label div to be 100% width */
 .modal .control-label {
     width: 100%;
+    margin-top: 5px;
 }
 
 .modal .control-label .btn {
@@ -281,6 +282,13 @@
     margin-right: 2px;
 }
 
+.btn-remove {
+    padding: 3px;
+    padding-left: 5px;
+    padding-right: 5px;
+    color: #A11;
+}
+
 .button-toolbar {
     padding-left: 0px;
 }
diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js
index 800b23bbb6..65b0c55da5 100644
--- a/InvenTree/static/script/inventree/stock.js
+++ b/InvenTree/static/script/inventree/stock.js
@@ -14,377 +14,34 @@ function getStockLocations(filters={}, options={}) {
     return inventreeGet('/api/stock/location/', filters, options)
 }
 
-
-/* Present user with a dialog to update multiple stock items
- * Possible actions:
- * - Stocktake
- * - Take stock
- * - Add stock
+/* Functions for interacting with stock management forms
  */
-function updateStock(items, options={}) {
 
-    if (!options.action) {
-        alert('No action supplied to stock update');
-        return false;
-    }
+function removeStockRow(e) {
+    // Remove a selected row from a stock modal form
 
-    var modal = options.modal || '#modal-form';
+    e = e || window.event;
+    var src = e.target || e.srcElement;
 
-    if (items.length == 0) {
-        alert('No items selected');
-        return;
-    }
+    var row = $(src).attr('row');
 
-    var html = '';
-
-    html += "';
-
-    html += "Note field must be filled 
";
-
-    html += `
-        
-            
-                 
-            
Confirm stock count 
-        
";
-
-            for (i = 0; i < response.length; i++) {
-                var loc = response[i];
-
-                html += makeOption(loc.pk, loc.pathstring + ' - ' + loc.description + ' ');
-            }
-
-            html += " Note field must be filled 
";
-
-            html += "\d+)/', include(location_endpoints)),
 
-    url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'),
-
-    url(r'move/?', StockMove.as_view(), name='api-stock-move'),
+    # These JSON endpoints have been replaced (for now) with server-side form rendering - 02/06/2019
+    # url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'),
+    # url(r'move/?', StockMove.as_view(), name='api-stock-move'),
 
     url(r'track/?', StockTrackingList.as_view(), name='api-stock-track'),
 
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index a05c8caef0..7187cada4e 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -42,26 +42,45 @@ class CreateStockItemForm(HelperForm):
         ]
 
 
-class MoveStockItemForm(forms.ModelForm):
-    """ Form for moving a StockItem to a new location """
+class AdjustStockForm(forms.ModelForm):
+    """ Form for performing simple stock adjustments.
 
+    - Add stock
+    - Remove stock
+    - Count stock
+    - Move stock
+
+    This form is used for managing stock adjuments for single or multiple stock items.
+    """
+
+    def get_location_choices(self):
+        locs = StockLocation.objects.all()
+
+        choices = [(None, '---------')]
+
+        for loc in locs:
+            choices.append((loc.pk, loc.pathstring + ' - ' + loc.description))
+
+        return choices
+
+    destination = forms.ChoiceField(label='Destination', required=True, help_text='Destination stock location')
     note = forms.CharField(label='Notes', required=True, help_text='Add note (required)')
+    # transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts')
+    confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items')
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        
+        self.fields['destination'].choices = self.get_location_choices()
 
     class Meta:
         model = StockItem
 
         fields = [
-            'location',
-        ]
-
-
-class StocktakeForm(forms.ModelForm):
-
-    class Meta:
-        model = StockItem
-
-        fields = [
-            'quantity',
+            'destination',
+            'note',
+            # 'transaction',
+            'confirm',
         ]
 
 
diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html
index cb54ac6f5e..40d78eb7e1 100644
--- a/InvenTree/stock/templates/stock/item.html
+++ b/InvenTree/stock/templates/stock/item.html
@@ -22,11 +22,13 @@
         
\ No newline at end of file
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index 37e54750de..4666eb05dc 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -19,8 +19,6 @@ stock_location_detail_urls = [
 stock_item_detail_urls = [
     url(r'^edit/?', views.StockItemEdit.as_view(), name='stock-item-edit'),
     url(r'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
-    url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'),
-    url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'),
     url(r'^qr_code/?', views.StockItemQRCode.as_view(), name='stock-item-qr'),
 
     url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
@@ -36,6 +34,8 @@ stock_urls = [
 
     url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'),
 
+    url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
+
     # Individual stock items
     url(r'^item/(?P\d+)/', include(stock_item_detail_urls)),
 
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 42ca16d3ba..3ede00697e 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -5,21 +5,26 @@ Django views for interacting with Stock app
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
+from django.views.generic.edit import FormMixin
 from django.views.generic import DetailView, ListView
 from django.forms.models import model_to_dict
 from django.forms import HiddenInput
 
+from django.utils.translation import ugettext as _
+
+from InvenTree.views import AjaxView
 from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
 from InvenTree.views import QRCodeView
 
+from InvenTree.helpers import str2bool
+
 from part.models import Part
 from .models import StockItem, StockLocation, StockItemTracking
 
 from .forms import EditStockLocationForm
 from .forms import CreateStockItemForm
 from .forms import EditStockItemForm
-from .forms import MoveStockItemForm
-from .forms import StocktakeForm
+from .forms import AdjustStockForm
 
 
 class StockIndex(ListView):
@@ -120,7 +125,276 @@ class StockItemQRCode(QRCodeView):
             return item.format_barcode()
         except StockItem.DoesNotExist:
             return None
+
+
+class StockAdjust(AjaxView, FormMixin):
+    """ View for enacting simple stock adjustments:
+    
+    - Take items from stock
+    - Add items to stock
+    - Count items
+    - Move stock
+    
+    """
+
+    ajax_template_name = 'stock/stock_adjust.html'
+    ajax_form_title = 'Adjust Stock'
+    form_class = AdjustStockForm
+    stock_items = []
+
+    def get_GET_items(self):
+        """ Return list of stock items initally requested using GET """
+
+        # Start with all 'in stock' items
+        items = StockItem.objects.filter(customer=None, belongs_to=None)
+
+        # Client provides a list of individual stock items
+        if 'stock[]' in self.request.GET:
+            items = items.filter(id__in=self.request.GET.getlist('stock[]'))
+
+        # Client provides a PART reference
+        elif 'part' in self.request.GET:
+            items = items.filter(part=self.request.GET.get('part'))
+
+        # Client provides a LOCATION reference
+        elif 'location' in self.request.GET:
+            items = items.filter(location=self.request.GET.get('location'))
+
+        # Client provides a single StockItem lookup
+        elif 'item' in self.request.GET:
+            items = [StockItem.objects.get(id=self.request.GET.get('item'))]
+
+        # Unsupported query
+        else:
+            items = None
+
+        for item in items:
+
+            # Initialize quantity to zero for addition/removal
+            if self.stock_action in ['take', 'add']:
+                item.new_quantity = 0
+            # Initialize quantity at full amount for counting or moving
+            else:
+                item.new_quantity = item.quantity
+
+        return items
+
+    def get_POST_items(self):
+        """ Return list of stock items sent back by client on a POST request """
+
+        items = []
+
+        for item in self.request.POST:
+            if item.startswith('stock-id-'):
+                
+                pk = item.replace('stock-id-', '')
+                q = self.request.POST[item]
+
+                try:
+                    stock_item = StockItem.objects.get(pk=pk)
+                except StockItem.DoesNotExist:
+                    continue
+
+                stock_item.new_quantity = q
+
+                items.append(stock_item)
+
+        return items
+
+    def get_context_data(self):
+
+        context = super().get_context_data()
+
+        context['stock_items'] = self.stock_items
+
+        context['stock_action'] = self.stock_action
+
+        context['stock_action_title'] = self.stock_action.capitalize()
+
+        return context
+
+    def get_form(self):
+
+        form = super().get_form()
+
+        if not self.stock_action == 'move':
+            form.fields.pop('destination')
+
+        return form
+
+    def get(self, request, *args, **kwargs):
+
+        self.request = request
+
+        # Action
+        self.stock_action = request.GET.get('action', '').lower()
+
+        # Pick a default action...
+        if self.stock_action not in ['move', 'count', 'take', 'add']:
+            self.stock_action = 'count'
+
+        # Choose the form title based on the action
+        titles = {
+            'move': 'Move Stock',
+            'count': 'Count Stock',
+            'take': 'Remove Stock',
+            'add': 'Add Stock'
+        }
+
+        self.ajax_form_title = titles[self.stock_action]
+        
+        # Save list of items!
+        self.stock_items = self.get_GET_items()
+
+        return self.renderJsonResponse(request, self.get_form())
+
+    def post(self, request, *args, **kwargs):
+
+        self.request = request
+
+        self.stock_action = request.POST.get('stock_action').lower()
+
+        # Update list of stock items
+        self.stock_items = self.get_POST_items()
+
+        form = self.get_form()
+
+        valid = form.is_valid()
+        
+        for item in self.stock_items:
+            try:
+                item.new_quantity = int(item.new_quantity)
+            except ValueError:
+                item.error = _('Must enter integer value')
+                valid = False
+                continue
+
+            if item.new_quantity < 0:
+                item.error = _('Quantity must be positive')
+                valid = False
+                continue
+
+            if self.stock_action in ['move', 'take']:
+
+                if item.new_quantity > item.quantity:
+                    item.error = _('Quantity must not exceed {x}'.format(x=item.quantity))
+                    valid = False
+                    continue
+
+        confirmed = str2bool(request.POST.get('confirm'))
+
+        if not confirmed:
+            valid = False
+            form.errors['confirm'] = [_('Confirm stock adjustment')]
+
+        data = {
+            'form_valid': valid,
+        }
+
+        if valid:
+
+            data['success'] = self.do_action()
+
+        return self.renderJsonResponse(request, form, data=data)
+
+    def do_action(self):
+        """ Perform stock adjustment action """
+
+        if self.stock_action == 'move':
+            destination = None
+
+            try:
+                destination = StockLocation.objects.get(id=self.request.POST.get('destination'))
+            except StockLocation.DoesNotExist:
+                pass
+            except ValueError:
+                pass
+
+            return self.do_move(destination)
+
+        elif self.stock_action == 'add':
+            return self.do_add()
+
+        elif self.stock_action == 'take':
+            return self.do_take()
+
+        elif self.stock_action == 'count':
+            return self.do_count()
+
+        else:
+            return 'No action performed'
+
+    def do_add(self):
+        
+        count = 0
+        note = self.request.POST['note']
+
+        for item in self.stock_items:
+            if item.new_quantity <= 0:
+                continue
+
+            item.add_stock(item.new_quantity, self.request.user, notes=note)
+
+            count += 1
+
+        return _("Added stock to {n} items".format(n=count))
+
+    def do_take(self):
+
+        count = 0
+        note = self.request.POST['note']
+
+        for item in self.stock_items:
+            if item.new_quantity <= 0:
+                continue
+
+            item.take_stock(item.new_quantity, self.request.user, notes=note)
+
+            count += 1
+
+        return _("Removed stock from {n} items".format(n=count))
+
+    def do_count(self):
+        
+        count = 0
+        note = self.request.POST['note']
+
+        for item in self.stock_items:
             
+            item.stocktake(item.new_quantity, self.request.user, notes=note)
+
+            count += 1
+
+        return _("Counted stock for {n} items".format(n=count))
+
+    def do_move(self, destination):
+        """ Perform actual stock movement """
+
+        count = 0
+
+        note = self.request.POST['note']
+
+        for item in self.stock_items:
+            # Avoid moving zero quantity
+            if item.new_quantity <= 0:
+                continue
+            
+            # Do not move to the same location
+            if destination == item.location:
+                continue
+
+            item.move(destination, note, self.request.user, quantity=int(item.new_quantity))
+
+            count += 1
+
+        if count == 0:
+            return _('No items were moved')
+        
+        else:
+            return _('Moved {n} items to {dest}'.format(
+                n=count,
+                dest=destination.pathstring))
+
 
 class StockItemEdit(AjaxUpdateView):
     """
@@ -306,76 +580,6 @@ class StockItemDelete(AjaxDeleteView):
     ajax_form_title = 'Delete Stock Item'
 
 
-class StockItemMove(AjaxUpdateView):
-    """
-    View to move a StockItem from one location to another
-    Performs some data validation to prevent illogical stock moves
-    """
-
-    model = StockItem
-    ajax_template_name = 'modal_form.html'
-    context_object_name = 'item'
-    ajax_form_title = 'Move Stock Item'
-    form_class = MoveStockItemForm
-
-    def post(self, request, *args, **kwargs):
-        form = self.form_class(request.POST, instance=self.get_object())
-
-        if form.is_valid():
-
-            obj = self.get_object()
-
-            try:
-                loc_id = form['location'].value()
-
-                if loc_id:
-                    loc = StockLocation.objects.get(pk=form['location'].value())
-                    if str(loc.pk) == str(obj.pk):
-                        form.errors['location'] = ['Item is already in this location']
-                    else:
-                        obj.move(loc, form['note'].value(), request.user)
-                else:
-                    form.errors['location'] = ['Cannot move to an empty location']
-
-            except StockLocation.DoesNotExist:
-                form.errors['location'] = ['Location does not exist']
-
-        data = {
-            'form_valid': form.is_valid() and len(form.errors) == 0,
-        }
-
-        return self.renderJsonResponse(request, form, data)
-
-
-class StockItemStocktake(AjaxUpdateView):
-    """
-    View to perform stocktake on a single StockItem
-    Updates the quantity, which will also create a new StockItemTracking item
-    """
-
-    model = StockItem
-    template_name = 'modal_form.html'
-    context_object_name = 'item'
-    ajax_form_title = 'Item stocktake'
-    form_class = StocktakeForm
-
-    def post(self, request, *args, **kwargs):
-
-        form = self.form_class(request.POST, instance=self.get_object())
-
-        if form.is_valid():
-
-            obj = self.get_object()
-
-            obj.stocktake(form.data['quantity'], request.user)
-
-        data = {
-            'form_valid': form.is_valid()
-        }
-
-        return self.renderJsonResponse(request, form, data)
-
-
 class StockTrackingIndex(ListView):
     """
     StockTrackingIndex provides a page to display StockItemTracking objects
diff --git a/InvenTree/templates/hover_image.html b/InvenTree/templates/hover_image.html
new file mode 100644
index 0000000000..0853c4fc12
--- /dev/null
+++ b/InvenTree/templates/hover_image.html
@@ -0,0 +1,12 @@
+{% load static %}
+
+
\ No newline at end of file
diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html
index fae7b04214..acdd371d08 100644
--- a/InvenTree/templates/stock_table.html
+++ b/InvenTree/templates/stock_table.html
@@ -8,8 +8,8 @@