diff --git a/InvenTree/InvenTree/static/script/inventree/delay.js b/InvenTree/InvenTree/static/script/inventree/delay.js
deleted file mode 100644
index 9070d0c5b9..0000000000
--- a/InvenTree/InvenTree/static/script/inventree/delay.js
+++ /dev/null
@@ -1,12 +0,0 @@
-var msDelay = 0;
-
-var delay = (function(){
-    return function(callback, ms){
-        clearTimeout(msDelay);
-        msDelay = setTimeout(callback, ms);
-    };
-})();
-
-function cancelTimer(){
-    clearTimeout(msDelay);
-}
\ No newline at end of file
diff --git a/InvenTree/InvenTree/static/script/inventree/sidenav.js b/InvenTree/InvenTree/static/script/inventree/sidenav.js
deleted file mode 100644
index eca19076f2..0000000000
--- a/InvenTree/InvenTree/static/script/inventree/sidenav.js
+++ /dev/null
@@ -1,249 +0,0 @@
-function loadTree(url, tree, options={}) {
-    /* Load the side-nav tree view
-
-    Args:
-        url: URL to request tree data
-        tree: html ref to treeview
-        options:
-            data: data object to pass to the AJAX request
-            selected: ID of currently selected item
-            name: name of the tree
-    */
-
-    var data = {};
-
-    if (options.data) {
-        data = options.data;
-    }
-
-    var key = "inventree-sidenav-items-";
-
-    if (options.name) {
-        key += options.name;
-    }
-
-    $.ajax({
-        url: url,
-        type: 'get',
-        dataType: 'json',
-        data: data,
-        success: function (response) {
-            if (response.tree) {
-                $(tree).treeview({
-                    data: response.tree,
-                    enableLinks: true,
-                    showTags: true,
-                });
-
-                if (localStorage.getItem(key)) {
-                    var saved_exp = localStorage.getItem(key).split(",");
-
-                    // Automatically expand the desired notes
-                    for (var q = 0; q < saved_exp.length; q++) {
-                        $(tree).treeview('expandNode', parseInt(saved_exp[q]));
-                    }
-                }
-
-                // Setup a callback whenever a node is toggled
-                $(tree).on('nodeExpanded nodeCollapsed', function(event, data) {
-                    
-                    // Record the entire list of expanded items
-                    var expanded = $(tree).treeview('getExpanded');
-
-                    var exp = [];
-
-                    for (var i = 0; i < expanded.length; i++) {
-                        exp.push(expanded[i].nodeId);
-                    }
-
-                    // Save the expanded nodes
-                    localStorage.setItem(key, exp);
-                });
-            }
-        },
-        error: function (xhr, ajaxOptions, thrownError) {
-            //TODO
-        }
-    });
-}
-
-
-/**
- * Initialize navigation tree display
- */
-function initNavTree(options) {
-
-    var resize = true;
-
-    if ('resize' in options) {
-        resize = options.resize;
-    }
-
-    var label = options.label || 'nav';
-
-    var stateLabel = `${label}-tree-state`;
-    var widthLabel = `${label}-tree-width`;
-
-    var treeId = options.treeId || '#sidenav-left';
-    var toggleId = options.toggleId;
-
-    // Initially hide the tree
-    $(treeId).animate({
-        width: '0px',
-    }, 0, function() {
-
-        if (resize) {
-            $(treeId).resizable({
-                minWidth: '0px',
-                maxWidth: '500px',
-                handles: 'e, se',
-                grid: [5, 5],
-                stop: function(event, ui) {
-                    var width = Math.round(ui.element.width());
-
-                    if (width < 75) {
-                        $(treeId).animate({
-                            width: '0px'
-                        }, 50);
-
-                        localStorage.setItem(stateLabel, 'closed');
-                    } else {
-                        localStorage.setItem(stateLabel, 'open');
-                        localStorage.setItem(widthLabel, `${width}px`);
-                    }
-                }
-            });
-        }
-
-        var state = localStorage.getItem(stateLabel);
-        var width = localStorage.getItem(widthLabel) || '300px';
-
-        if (state && state == 'open') {
-
-            $(treeId).animate({
-                width: width,
-            }, 50);
-        }
-    });
-
-    // Register callback for 'toggle' button
-    if (toggleId) {
-        
-        $(toggleId).click(function() {
-
-            var state = localStorage.getItem(stateLabel) || 'closed';
-            var width = localStorage.getItem(widthLabel) || '300px';
-
-            if (state == 'open') {
-                $(treeId).animate({
-                    width: '0px'
-                }, 50);
-
-                localStorage.setItem(stateLabel, 'closed');
-            } else {
-                $(treeId).animate({
-                    width: width,
-                }, 50);
-
-                localStorage.setItem(stateLabel, 'open');
-            }
-        });
-    }
-}
-
-
-/**
- * Handle left-hand icon menubar display
- */
-function enableNavbar(options) {
-
-    var resize = true;
-
-    if ('resize' in options) {
-        resize = options.resize;
-    }
-
-    var label = options.label || 'nav';
-
-    label = `navbar-${label}`;
-
-    var stateLabel = `${label}-state`;
-    var widthLabel = `${label}-width`;
-
-    var navId = options.navId || '#sidenav-right';
-
-    var toggleId = options.toggleId;
-
-    // Extract the saved width for this element
-    $(navId).animate({
-        width: '45px',
-        'min-width': '45px',
-        display: 'block',
-    }, 50, function() {
-
-        // Make the navbar resizable
-        if (resize) {
-            $(navId).resizable({
-                minWidth: options.minWidth || '100px',
-                maxWidth: options.maxWidth || '500px',
-                handles: 'e, se',
-                grid: [5, 5],
-                stop: function(event, ui) {
-                    // Record the new width
-                    var width = Math.round(ui.element.width());
-
-                    // Reasonably narrow? Just close it!
-                    if (width <= 75) {
-                        $(navId).animate({
-                            width: '45px'
-                        }, 50);
-
-                        localStorage.setItem(stateLabel, 'closed');
-                    } else {
-                        localStorage.setItem(widthLabel, `${width}px`);
-                        localStorage.setItem(stateLabel, 'open');
-                    }
-                }
-            });
-        }
-
-        var state = localStorage.getItem(stateLabel);
-
-        var width = localStorage.getItem(widthLabel) || '250px';
-        
-        if (state && state == 'open') {
-
-            $(navId).animate({
-                width: width
-            }, 100);
-        }
-
-    });
-
-    // Register callback for 'toggle' button
-    if (toggleId) {
-
-        $(toggleId).click(function() {
-
-            var state = localStorage.getItem(stateLabel) || 'closed';
-            var width = localStorage.getItem(widthLabel) || '250px';
-
-            if (state == 'open') {
-                $(navId).animate({
-                    width: '45px',
-                    minWidth: '45px',
-                }, 50);
-
-                localStorage.setItem(stateLabel, 'closed');
-
-            } else {
-
-                $(navId).animate({
-                    'width': width
-                }, 50);
-
-                localStorage.setItem(stateLabel, 'open');
-            }
-        });
-    }
-}
\ No newline at end of file
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 5bbae8565e..48539713f2 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -12,11 +12,14 @@ import common.models
 INVENTREE_SW_VERSION = "0.6.0 dev"
 
 # InvenTree API version
-INVENTREE_API_VERSION = 15
+INVENTREE_API_VERSION = 16
 
 """
 Increment this API version number whenever there is a significant change to the API that any clients need to know about
 
+v16 -> 2021-10-17
+    - Adds API endpoint for completing build order outputs
+
 v15 -> 2021-10-06
     - Adds detail endpoint for SalesOrderAllocation model
     - Allows use of the API forms interface for adjusting SalesOrderAllocation objects
diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py
index cf4d44a03e..819ffed1b6 100644
--- a/InvenTree/build/api.py
+++ b/InvenTree/build/api.py
@@ -5,12 +5,9 @@ JSON API for the Build app
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
-from django.utils.translation import ugettext_lazy as _
-
 from django.conf.urls import url, include
 
 from rest_framework import filters, generics
-from rest_framework.serializers import ValidationError
 
 from django_filters.rest_framework import DjangoFilterBackend
 from django_filters import rest_framework as rest_filters
@@ -21,7 +18,7 @@ from InvenTree.filters import InvenTreeOrderingFilter
 from InvenTree.status_codes import BuildStatus
 
 from .models import Build, BuildItem, BuildOrderAttachment
-from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
+from .serializers import BuildAttachmentSerializer, BuildCompleteSerializer, BuildSerializer, BuildItemSerializer
 from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer
 
 
@@ -201,30 +198,43 @@ class BuildUnallocate(generics.CreateAPIView):
     queryset = Build.objects.none()
 
     serializer_class = BuildUnallocationSerializer
-
-    def get_build(self):
-        """
-        Returns the BuildOrder associated with this API endpoint
-        """
-
-        pk = self.kwargs.get('pk', None)
-
-        try:
-            build = Build.objects.get(pk=pk)
-        except (ValueError, Build.DoesNotExist):
-            raise ValidationError(_("Matching build order does not exist"))
-
-        return build
-
+    
     def get_serializer_context(self):
 
         ctx = super().get_serializer_context()
-        ctx['build'] = self.get_build()
+
+        try:
+            ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
+        except:
+            pass
+
         ctx['request'] = self.request
 
         return ctx
 
 
+class BuildComplete(generics.CreateAPIView):
+    """
+    API endpoint for completing build outputs
+    """
+
+    queryset = Build.objects.none()
+
+    serializer_class = BuildCompleteSerializer
+
+    def get_serializer_context(self):
+        ctx = super().get_serializer_context()
+
+        ctx['request'] = self.request
+
+        try:
+            ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
+        except:
+            pass
+        
+        return ctx
+
+
 class BuildAllocate(generics.CreateAPIView):
     """
     API endpoint to allocate stock items to a build order
@@ -241,20 +251,6 @@ class BuildAllocate(generics.CreateAPIView):
 
     serializer_class = BuildAllocationSerializer
 
-    def get_build(self):
-        """
-        Returns the BuildOrder associated with this API endpoint
-        """
-
-        pk = self.kwargs.get('pk', None)
-
-        try:
-            build = Build.objects.get(pk=pk)
-        except (Build.DoesNotExist, ValueError):
-            raise ValidationError(_("Matching build order does not exist"))
-
-        return build
-
     def get_serializer_context(self):
         """
         Provide the Build object to the serializer context
@@ -262,7 +258,11 @@ class BuildAllocate(generics.CreateAPIView):
 
         context = super().get_serializer_context()
 
-        context['build'] = self.get_build()
+        try:
+            context['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
+        except:
+            pass
+
         context['request'] = self.request
 
         return context
@@ -390,6 +390,7 @@ build_api_urls = [
     # Build Detail
     url(r'^(?P<pk>\d+)/', include([
         url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
+        url(r'^complete/', BuildComplete.as_view(), name='api-build-complete'),
         url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
         url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
     ])),
diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index bc7bdd50f5..19bf3566dc 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -10,63 +10,9 @@ from django.utils.translation import ugettext_lazy as _
 from django import forms
 
 from InvenTree.forms import HelperForm
-from InvenTree.fields import RoundingDecimalFormField
-from InvenTree.fields import DatePickerFormField
-
-from InvenTree.status_codes import StockStatus
 
 from .models import Build
 
-from stock.models import StockLocation, StockItem
-
-
-class EditBuildForm(HelperForm):
-    """ Form for editing a Build object.
-    """
-
-    field_prefix = {
-        'reference': 'BO',
-        'link': 'fa-link',
-        'batch': 'fa-layer-group',
-        'serial-numbers': 'fa-hashtag',
-        'location': 'fa-map-marker-alt',
-        'target_date': 'fa-calendar-alt',
-    }
-
-    field_placeholder = {
-        'reference': _('Build Order reference'),
-        'target_date': _('Order target date'),
-    }
-
-    target_date = DatePickerFormField(
-        label=_('Target Date'),
-        help_text=_('Target date for build completion. Build will be overdue after this date.')
-    )
-
-    quantity = RoundingDecimalFormField(
-        max_digits=10, decimal_places=5,
-        label=_('Quantity'),
-        help_text=_('Number of items to build')
-    )
-
-    class Meta:
-        model = Build
-        fields = [
-            'reference',
-            'title',
-            'part',
-            'quantity',
-            'batch',
-            'target_date',
-            'take_from',
-            'destination',
-            'parent',
-            'sales_order',
-            'link',
-            'issued_by',
-            'responsible',
-        ]
-
 
 class BuildOutputCreateForm(HelperForm):
     """
@@ -155,59 +101,6 @@ class CompleteBuildForm(HelperForm):
         ]
 
 
-class CompleteBuildOutputForm(HelperForm):
-    """
-    Form for completing a single build output
-    """
-
-    field_prefix = {
-        'serial_numbers': 'fa-hashtag',
-    }
-
-    field_placeholder = {
-    }
-
-    location = forms.ModelChoiceField(
-        queryset=StockLocation.objects.all(),
-        label=_('Location'),
-        help_text=_('Location of completed parts'),
-    )
-
-    stock_status = forms.ChoiceField(
-        label=_('Status'),
-        help_text=_('Build output stock status'),
-        initial=StockStatus.OK,
-        choices=StockStatus.items(),
-    )
-
-    confirm_incomplete = forms.BooleanField(
-        required=False,
-        label=_('Confirm incomplete'),
-        help_text=_("Confirm completion with incomplete stock allocation")
-    )
-
-    confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm build completion'))
-
-    output = forms.ModelChoiceField(
-        queryset=StockItem.objects.all(),  # Queryset is narrowed in the view
-        widget=forms.HiddenInput(),
-    )
-
-    class Meta:
-        model = Build
-        fields = [
-            'location',
-            'output',
-            'stock_status',
-            'confirm',
-            'confirm_incomplete',
-        ]
-
-    def __init__(self, *args, **kwargs):
-
-        super().__init__(*args, **kwargs)
-
-
 class CancelBuildForm(HelperForm):
     """ Form for cancelling a build """
 
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index c477794e8c..403b3a9430 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -724,7 +724,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
         items.all().delete()
 
     @transaction.atomic
-    def completeBuildOutput(self, output, user, **kwargs):
+    def complete_build_output(self, output, user, **kwargs):
         """
         Complete a particular build output
 
@@ -741,10 +741,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
         allocated_items = output.items_to_install.all()
 
         for build_item in allocated_items:
-
-            # TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
-            # TODO: Use the background worker process to handle this task!
-
             # Complete the allocation of stock for that item
             build_item.complete_allocation(user)
 
@@ -770,6 +766,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
 
         # Increase the completed quantity for this build
         self.completed += output.quantity
+
         self.save()
 
     def requiredQuantity(self, part, output):
diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py
index 547f565905..8f76f3e603 100644
--- a/InvenTree/build/serializers.py
+++ b/InvenTree/build/serializers.py
@@ -18,9 +18,10 @@ from rest_framework.serializers import ValidationError
 from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
 from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
 
+from InvenTree.status_codes import StockStatus
 import InvenTree.helpers
 
-from stock.models import StockItem
+from stock.models import StockItem, StockLocation
 from stock.serializers import StockItemSerializerBrief, LocationSerializer
 
 from part.models import BomItem
@@ -120,6 +121,124 @@ class BuildSerializer(InvenTreeModelSerializer):
         ]
 
 
+class BuildOutputSerializer(serializers.Serializer):
+    """
+    Serializer for a "BuildOutput"
+
+    Note that a "BuildOutput" is really just a StockItem which is "in production"!
+    """
+
+    output = serializers.PrimaryKeyRelatedField(
+        queryset=StockItem.objects.all(),
+        many=False,
+        allow_null=False,
+        required=True,
+        label=_('Build Output'),
+    )
+
+    def validate_output(self, output):
+
+        build = self.context['build']
+
+        # The stock item must point to the build
+        if output.build != build:
+            raise ValidationError(_("Build output does not match the parent build"))
+
+        # The part must match!
+        if output.part != build.part:
+            raise ValidationError(_("Output part does not match BuildOrder part"))
+
+        # The build output must be "in production"
+        if not output.is_building:
+            raise ValidationError(_("This build output has already been completed"))
+
+        # The build output must have all tracked parts allocated
+        if not build.isFullyAllocated(output):
+            raise ValidationError(_("This build output is not fully allocated"))
+
+        return output
+
+    class Meta:
+        fields = [
+            'output',
+        ]
+
+
+class BuildCompleteSerializer(serializers.Serializer):
+    """
+    DRF serializer for completing one or more build outputs
+    """
+
+    class Meta:
+        fields = [
+            'outputs',
+            'location',
+            'status',
+            'notes',
+        ]
+
+    outputs = BuildOutputSerializer(
+        many=True,
+        required=True,
+    )
+
+    location = serializers.PrimaryKeyRelatedField(
+        queryset=StockLocation.objects.all(),
+        required=True,
+        many=False,
+        label=_("Location"),
+        help_text=_("Location for completed build outputs"),
+    )
+
+    status = serializers.ChoiceField(
+        choices=list(StockStatus.items()),
+        default=StockStatus.OK,
+        label=_("Status"),
+    )
+
+    notes = serializers.CharField(
+        label=_("Notes"),
+        required=False,
+        allow_blank=True,
+    )
+
+    def validate(self, data):
+
+        super().validate(data)
+
+        outputs = data.get('outputs', [])
+
+        if len(outputs) == 0:
+            raise ValidationError(_("A list of build outputs must be provided"))
+
+        return data
+
+    def save(self):
+        """
+        "save" the serializer to complete the build outputs
+        """
+
+        build = self.context['build']
+        request = self.context['request']
+
+        data = self.validated_data
+
+        outputs = data.get('outputs', [])
+
+        # Mark the specified build outputs as "complete"
+        with transaction.atomic():
+            for item in outputs:
+
+                output = item['output']
+
+                build.complete_build_output(
+                    output,
+                    request.user,
+                    status=data['status'],
+                    notes=data.get('notes', '')
+                )
+
+
 class BuildUnallocationSerializer(serializers.Serializer):
     """
     DRF serializer for unallocating stock from a BuildOrder
diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html
deleted file mode 100644
index 3ce4a52aeb..0000000000
--- a/InvenTree/build/templates/build/allocation_card.html
+++ /dev/null
@@ -1,51 +0,0 @@
-{% load i18n %}
-{% load inventree_extras %}
-
-{% define item.pk as pk %}
-
-<div class="panel panel-default" id='allocation-panel-{{ pk }}'>
-    <div class="panel-heading" role="tab" id="heading-{{ pk }}">
-      <div class="panel-title">
-          <div class='row'>
-              {% if tracked_items %}
-              <a class='collapsed' aria-expanded='false' role="button" data-toggle="collapse" data-parent="#build-output-accordion" href="#collapse-{{ pk }}" aria-controls="collapse-{{ pk }}">
-              {% endif %}
-                <div class='col-sm-4'>
-                    {% if tracked_items %}
-                    <span class='fas fa-caret-right'></span>
-                    {% endif %}
-                    {{ item.part.full_name }}
-                </div>
-                <div class='col-sm-2'>
-                    {% if item.serial %}
-                    {% trans "Serial Number" %}: {{ item.serial }}
-                    {% else %}
-                    {% trans "Quantity" %}: {% decimal item.quantity %}
-                    {% endif %}
-                </div>
-            {% if tracked_items %}
-            </a>
-            {% endif %}
-            <div class='col-sm-3'>
-                <div>
-                    <div id='output-progress-{{ pk }}'>
-                        {% if tracked_items %}
-                        <span class='fas fa-spin fa-spinner'></span>
-                        {% endif %}
-                    </div>
-                </div>
-            </div>
-            <div class='col-sm-3'>
-                <div class='btn-group float-right' id='output-actions-{{ pk }}'>
-                    <span class='fas fa-spin fa-spinner'></span>
-                </div>
-            </div>
-        </div>
-    </div>
-    </div>
-    <div id="collapse-{{ pk }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-{{ pk }}">
-      <div class="panel-body">
-          <table class='table table-striped table-condensed' id='allocation-table-{{ pk }}'></table>
-      </div>
-    </div>
-  </div>
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index e3119e6fdb..1731e00403 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -91,16 +91,11 @@ src="{% static 'img/blank_image.png' %}"
             <span class='fas fa-print'></span> <span class='caret'></span>
         </button>
         <ul class='dropdown-menu' role='menu'>
-            <li><a href='#' id='print-build-report'><span class='fas fa-file-pdf'></span> {% trans "Print Build Order" %}</a></li>
+            <li><a href='#' id='print-build-report'><span class='fas fa-file-pdf'></span> {% trans "Print build order report" %}</a></li>
         </ul>
     </div>
     <!-- Build actions -->
     {% if roles.build.change %}
-    {% if build.active %}
-    <button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
-        <span class='fas fa-paper-plane'></span>
-    </button>
-    {% endif %}
     <div class='btn-group'>
         <button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
             <span class='fas fa-tools'></span> <span class='caret'></span>
@@ -115,6 +110,11 @@ src="{% static 'img/blank_image.png' %}"
             {% endif %}
         </ul>
     </div>
+    {% if build.active %}
+    <button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
+        <span class='fas fa-check-circle'></span>
+    </button>
+    {% endif %}
     {% endif %}
 </div>
 {% endblock %}
@@ -153,8 +153,8 @@ src="{% static 'img/blank_image.png' %}"
     </tr>
     {% endif %}
     <tr>
-        <td><span class='fas fa-spinner'></span></td>
-        <td>{% trans "Progress" %}</td>
+        <td><span class='fas fa-check-circle'></span></td>
+        <td>{% trans "Completed" %}</td>
         <td> {{ build.completed }} / {{ build.quantity }}</td>
     </tr>
     {% if build.parent %}
diff --git a/InvenTree/build/templates/build/complete_output.html b/InvenTree/build/templates/build/complete_output.html
deleted file mode 100644
index d03885774f..0000000000
--- a/InvenTree/build/templates/build/complete_output.html
+++ /dev/null
@@ -1,53 +0,0 @@
-{% extends "modal_form.html" %}
-{% load inventree_extras %}
-{% load i18n %}
-
-{% block pre_form_content %}
-
-{% if not build.has_tracked_bom_items %}
-{% elif fully_allocated %}
-<div class='alert alert-block alert-success'>
-    {% trans "Stock allocation is complete for this output" %}
-</div>
-{% else %}
-<div class='alert alert-block alert-danger'>
-    <h4>{% trans "Stock allocation is incomplete" %}</h4>
-
-    <div class='panel-group'>
-        <div class='panel panel-default'>
-            <div class='panel panel-heading'>
-                <a data-toggle='collapse' href='#collapse-unallocated'>
-                    {{ unallocated_parts|length }} {% trans "tracked parts have not been fully allocated" %}
-                </a>
-            </div>
-            <div class='panel-collapse collapse' id='collapse-unallocated'>
-                <div class='panel-body'>
-                    <ul class='list-group'>
-                        {% for part in unallocated_parts %}
-                        <li class='list-group-item'>
-                            {% include "hover_image.html" with image=part.image %} {{ part }}
-                        </li>
-                        {% endfor %}
-                    </ul>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-{% endif %}
-
-<div class='panel panel-info'>
-    <div class='panel-heading'>
-        {% trans "The following items will be created" %}
-    </div>
-    <div class='panel-content' style='padding-bottom:16px'>
-        {% include "hover_image.html" with image=build.part.image %}
-        {% if output.serialized %}
-        {{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
-        {% else %}
-        {% decimal output.quantity %} x {{ output.part.full_name }}
-        {% endif %}
-    </div>
-</div>
-
-{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index cfba2046e3..fcd60c2edd 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -63,10 +63,17 @@
                     <td>{% build_status_label build.status %}</td>
                 </tr>
                 <tr>
-                    <td><span class='fas fa-spinner'></span></td>
-                    <td>{% trans "Progress" %}</td>
+                    <td><span class='fas fa-check-circle'></span></td>
+                    <td>{% trans "Completed" %}</td>
                     <td>{{ build.completed }} / {{ build.quantity }}</td>
                 </tr>
+                {% if True or build.active and build.has_untracked_bom_items %}
+                <tr>
+                    <td><span class='fas fa-list'></span></td>
+                    <td>{% trans "Allocated Parts" %}</td>
+                    <td id='output-progress-untracked'><span class='fas fa-spinner fa-spin'></span></td>
+                </tr>
+                {% endif %}
                 {% if build.batch %}
                 <tr>
                     <td><span class='fas fa-layer-group'></span></td>
@@ -213,35 +220,35 @@
 </div>
 
 <div class='panel panel-default panel-inventree panel-hidden' id='panel-outputs'>
-    {% if not build.is_complete %}
     <div class='panel-heading'>
         <h4>{% trans "Incomplete Build Outputs" %}</h4>
     </div>
     <div class='panel-content'>
-        <div class='btn-group' role='group'>
-            {% if build.active %}
-            <button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
-                <span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
-            </button>
-            {% endif %}
+        <div id='build-output-toolbar'>
+            <div class='button-toolbar container-fluid'>
+                {% if build.active %}
+                <div class='btn-group'>
+                    <button class='btn btn-success' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
+                        <span class='fas fa-plus-circle'></span>
+                    </button>
+                    <!-- Build output actions -->
+                    <div class='btn-group'>
+                        <button id='output-options' class='btn btn-primary dropdown-toiggle' type='button' data-toggle='dropdown' title='{% trans "Output Actions" %}'>
+                            <span class='fas fa-tools'></span> <span class='caret'></span>
+                        </button>
+                        <ul class='dropdown-menu'>
+                            <li><a href='#' id='multi-output-complete' title='{% trans "Complete selected items" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}</a></li>
+                        </ul>
+                    </div>
+                    {% endif %}
+                </div>
+            </div>
         </div>
-
-        {% if build.incomplete_outputs %}
-        <div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
-            {% for item in build.incomplete_outputs %}
-            {% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %}
-            {% endfor %}
-        </div>
-        {% else %}
-        <div class='alert alert-block alert-info'>
-            <strong>{% trans "Create a new build output" %}</strong><br>
-            {% trans "No incomplete build outputs remain." %}<br>
-            {% trans "Create a new build output using the button above" %}
-        </div>
-        {% endif %}
+        <table class='table table-striped table-condensed' id='build-output-table' data-toolbar='#build-output-toolbar'></table>
     </div>
-    {% endif %}
+</div>
 
+<div class='panel panel-default panel-inventree panel-hidden' id='panel-completed'>
     <div class='panel-heading'>
         <h4>
             {% trans "Completed Build Outputs" %}
@@ -313,26 +320,75 @@ loadStockTable($("#build-stock-table"), {
     url: "{% url 'api-stock-list' %}",    
 });
 
-var buildInfo = {
-    pk: {{ build.pk }},
-    quantity: {{ build.quantity }},
-    completed: {{ build.completed }},
-    part: {{ build.part.pk }},
-    {% if build.take_from %}
-    source_location: {{ build.take_from.pk }},
-    {% endif %}
-};
 
-{% for item in build.incomplete_outputs %}
-// Get the build output as a javascript object
-inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
+// Get the list of BOM items required for this build
+inventreeGet(
+    '{% url "api-bom-list" %}',
+    {
+        part: {{ build.part.pk }},
+        sub_part_detail: true,
+    },
     {
         success: function(response) {
-            loadBuildOutputAllocationTable(buildInfo, response);
+
+            var build_info = {
+                pk: {{ build.pk }},
+                part: {{ build.part.pk }},
+                quantity: {{ build.quantity }},
+                bom_items: response,
+                {% if build.take_from %}
+                source_location: {{ build.take_from.pk }},
+                {% endif %}
+                {% if build.has_tracked_bom_items %}
+                tracked_parts: true,
+                {% else %}
+                tracked_parts: false,
+                {% endif %}
+            };
+
+            {% if build.active %}
+            loadBuildOutputTable(build_info);
+            linkButtonsToSelection(
+                '#build-output-table',
+                [
+                    '#output-options',
+                    '#multi-output-complete',
+                ]
+            );
+
+            $('#multi-output-complete').click(function() {
+                var outputs = $('#build-output-table').bootstrapTable('getSelections');
+
+                completeBuildOutputs(
+                    build_info.pk,
+                    outputs,
+                    {
+                        success: function() {
+                            // Reload the "in progress" table
+                            $('#build-output-table').bootstrapTable('refresh');
+
+                            // Reload the "completed" table
+                            $('#build-stock-table').bootstrapTable('refresh');
+                        }
+                    }
+                );
+            });
+
+            {% endif %}
+        
+            {% if build.active and build.has_untracked_bom_items %}
+            // Load allocation table for un-tracked parts
+            loadBuildOutputAllocationTable(
+                build_info,
+                null,
+                {
+                    search: true,
+                }
+            );
+            {% endif %}
         }
     }
 );
-{% endfor %}
 
 loadBuildTable($('#sub-build-table'), {
     url: '{% url "api-build-list" %}',
@@ -342,6 +398,7 @@ loadBuildTable($('#sub-build-table'), {
     }
 });
 
+
 enableDragAndDrop(
     '#attachment-dropzone',
     '{% url "api-build-attachment-list" %}',
@@ -416,11 +473,6 @@ $('#edit-notes').click(function() {
     });
 });
 
-{% if build.has_untracked_bom_items %}
-// Load allocation table for un-tracked parts
-loadBuildOutputAllocationTable(buildInfo, null);
-{% endif %}
-
 function reloadTable() {
     $('#allocation-table-untracked').bootstrapTable('refresh');
 }
diff --git a/InvenTree/build/templates/build/edit_build_item.html b/InvenTree/build/templates/build/edit_build_item.html
deleted file mode 100644
index 99cad71ba2..0000000000
--- a/InvenTree/build/templates/build/edit_build_item.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{% extends "modal_form.html" %}
-{% load i18n %}
-
-{% block pre_form_content %}
-<div class='alert alert-block alert-info'>
-    <p>
-        {% trans "Alter the quantity of stock allocated to the build output" %}
-    </p>
-</div>
-{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/navbar.html b/InvenTree/build/templates/build/navbar.html
index e4c4fe4e50..9b159503dc 100644
--- a/InvenTree/build/templates/build/navbar.html
+++ b/InvenTree/build/templates/build/navbar.html
@@ -19,16 +19,25 @@
     {% if build.active %}
     <li class='list-group-item' title='{% trans "Allocate Stock" %}'>
         <a href='#' id='select-allocate' class='nav-toggle'>
-            <span class='fas fa-tools sidebar-icon'></span>
+            <span class='fas fa-tasks sidebar-icon'></span>
             {% trans "Allocate Stock" %}
         </a>
     </li>
     {% endif %}
 
-    <li class='list-group-item' title='{% trans "Build Outputs" %}'>
+    {% if not build.is_complete %}
+    <li class='list-group-item' title='{% trans "Pending Outputs" %}'>
         <a href='#' id='select-outputs' class='nav-toggle'>
-            <span class='fas fa-box sidebar-icon'></span>
-            {% trans "Build Outputs" %}
+            <span class='fas fa-tools sidebar-icon'></span>
+            {% trans "Pending Outputs" %}
+        </a>
+    </li>
+    {% endif %}
+
+    <li class='list-group-item' title='{% trans "Completed Outputs" %}'>
+        <a href='#' id='select-completed' class='nav-toggle'>
+            <span class='fas fa-boxes sidebar-icon'></span>
+            {% trans "Completed Outputs" %}
         </a>
     </li>
 
diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py
index 017f0126c5..e2b6448f2f 100644
--- a/InvenTree/build/test_api.py
+++ b/InvenTree/build/test_api.py
@@ -7,6 +7,7 @@ from django.urls import reverse
 
 from part.models import Part
 from build.models import Build, BuildItem
+from stock.models import StockItem
 
 from InvenTree.status_codes import BuildStatus
 from InvenTree.api_tester import InvenTreeAPITestCase
@@ -37,6 +38,148 @@ class BuildAPITest(InvenTreeAPITestCase):
         super().setUp()
 
 
+class BuildCompleteTest(BuildAPITest):
+    """
+    Unit testing for the build complete API endpoint
+    """
+
+    def setUp(self):
+
+        super().setUp()
+
+        self.build = Build.objects.get(pk=1)
+
+        self.url = reverse('api-build-complete', kwargs={'pk': self.build.pk})
+
+    def test_invalid(self):
+        """
+        Test with invalid data
+        """
+
+        # Test with an invalid build ID
+        self.post(
+            reverse('api-build-complete', kwargs={'pk': 99999}),
+            {},
+            expected_code=400
+        )
+
+        data = self.post(self.url, {}, expected_code=400).data
+
+        self.assertIn("This field is required", str(data['outputs']))
+        self.assertIn("This field is required", str(data['location']))
+
+        # Test with an invalid location
+        data = self.post(
+            self.url,
+            {
+                "outputs": [],
+                "location": 999999,
+            },
+            expected_code=400
+        ).data
+
+        self.assertIn(
+            "Invalid pk",
+            str(data["location"])
+        )
+
+        data = self.post(
+            self.url,
+            {
+                "outputs": [],
+                "location": 1,
+            },
+            expected_code=400
+        ).data
+
+        self.assertIn("A list of build outputs must be provided", str(data))
+
+        stock_item = StockItem.objects.create(
+            part=self.build.part,
+            quantity=100,
+        )
+
+        post_data = {
+            "outputs": [
+                {
+                    "output": stock_item.pk,
+                },
+            ],
+            "location": 1,
+        }
+
+        # Post with a stock item that does not match the build
+        data = self.post(
+            self.url,
+            post_data,
+            expected_code=400
+        ).data
+
+        self.assertIn(
+            "Build output does not match the parent build",
+            str(data["outputs"][0])
+        )
+
+        # Now, ensure that the stock item *does* match the build
+        stock_item.build = self.build
+        stock_item.save()
+
+        data = self.post(
+            self.url,
+            post_data,
+            expected_code=400,
+        ).data
+
+        self.assertIn(
+            "This build output has already been completed",
+            str(data["outputs"][0]["output"])
+        )
+
+    def test_complete(self):
+        """
+        Test build order completion
+        """
+
+        # We start without any outputs assigned against the build
+        self.assertEqual(self.build.incomplete_outputs.count(), 0)
+
+        # Create some more build outputs
+        for ii in range(10):
+            self.build.create_build_output(10)
+
+        # Check that we are in a known state
+        self.assertEqual(self.build.incomplete_outputs.count(), 10)
+        self.assertEqual(self.build.incomplete_count, 100)
+        self.assertEqual(self.build.completed, 0)
+
+        # We shall complete 4 of these outputs
+        outputs = self.build.incomplete_outputs[0:4]
+
+        self.post(
+            self.url,
+            {
+                "outputs": [{"output": output.pk} for output in outputs],
+                "location": 1,
+                "status": 50,  # Item requires attention
+            },
+            expected_code=201
+        )
+
+        # There now should be 6 incomplete build outputs remaining
+        self.assertEqual(self.build.incomplete_outputs.count(), 6)
+
+        # And there should be 4 completed outputs
+        outputs = self.build.complete_outputs
+        self.assertEqual(outputs.count(), 4)
+
+        for output in outputs:
+            self.assertFalse(output.is_building)
+            self.assertEqual(output.build, self.build)
+
+        self.build.refresh_from_db()
+        self.assertEqual(self.build.completed, 40)
+
+
 class BuildAllocationTest(BuildAPITest):
     """
     Unit tests for allocation of stock items against a build order.
diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py
index df6253362e..f8c381f224 100644
--- a/InvenTree/build/test_build.py
+++ b/InvenTree/build/test_build.py
@@ -339,11 +339,11 @@ class BuildTest(TestCase):
         self.assertTrue(self.build.isFullyAllocated(self.output_1))
         self.assertTrue(self.build.isFullyAllocated(self.output_2))
 
-        self.build.completeBuildOutput(self.output_1, None)
+        self.build.complete_build_output(self.output_1, None)
 
         self.assertFalse(self.build.can_complete)
 
-        self.build.completeBuildOutput(self.output_2, None)
+        self.build.complete_build_output(self.output_2, None)
 
         self.assertTrue(self.build.can_complete)
 
diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py
index 7b2568b1c7..7afd078ce9 100644
--- a/InvenTree/build/tests.py
+++ b/InvenTree/build/tests.py
@@ -15,7 +15,7 @@ from datetime import datetime, timedelta
 from .models import Build
 from stock.models import StockItem
 
-from InvenTree.status_codes import BuildStatus, StockStatus
+from InvenTree.status_codes import BuildStatus
 
 
 class BuildTestSimple(TestCase):
@@ -252,53 +252,6 @@ class TestBuildViews(TestCase):
 
         self.assertIn(build.title, content)
 
-    def test_build_output_complete(self):
-        """
-        Test the build output completion form
-        """
-
-        # Firstly, check that the build cannot be completed!
-        self.assertFalse(self.build.can_complete)
-
-        url = reverse('build-output-complete', args=(1,))
-
-        # Test without confirmation
-        response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-        self.assertEqual(response.status_code, 200)
-
-        data = json.loads(response.content)
-        self.assertFalse(data['form_valid'])
-
-        # Test with confirmation, valid location
-        response = self.client.post(
-            url,
-            {
-                'confirm': 1,
-                'confirm_incomplete': 1,
-                'location': 1,
-                'output': self.output.pk,
-                'stock_status': StockStatus.DAMAGED
-            },
-            HTTP_X_REQUESTED_WITH='XMLHttpRequest'
-        )
-
-        self.assertEqual(response.status_code, 200)
-
-        data = json.loads(response.content)
-
-        self.assertTrue(data['form_valid'])
-
-        # Now the build should be able to be completed
-        self.build.refresh_from_db()
-        self.assertTrue(self.build.can_complete)
-
-        # Test with confirmation, invalid location
-        response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-        self.assertEqual(response.status_code, 200)
-
-        data = json.loads(response.content)
-        self.assertFalse(data['form_valid'])
-
     def test_build_cancel(self):
         """ Test the build cancellation form """
 
diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py
index d80b16056c..8ea339ae26 100644
--- a/InvenTree/build/urls.py
+++ b/InvenTree/build/urls.py
@@ -11,7 +11,6 @@ build_detail_urls = [
     url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
     url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'),
     url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'),
-    url(r'^complete-output/', views.BuildOutputComplete.as_view(), name='build-output-complete'),
     url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'),
 
     url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py
index 8c63c1296c..fd730b6a7e 100644
--- a/InvenTree/build/views.py
+++ b/InvenTree/build/views.py
@@ -12,16 +12,17 @@ from django.forms import HiddenInput
 
 from .models import Build
 from . import forms
-from stock.models import StockLocation, StockItem
+from stock.models import StockItem
 
 from InvenTree.views import AjaxUpdateView, AjaxDeleteView
 from InvenTree.views import InvenTreeRoleMixin
 from InvenTree.helpers import str2bool, extract_serial_numbers
-from InvenTree.status_codes import BuildStatus, StockStatus
+from InvenTree.status_codes import BuildStatus
 
 
 class BuildIndex(InvenTreeRoleMixin, ListView):
-    """ View for displaying list of Builds
+    """
+    View for displaying list of Builds
     """
     model = Build
     template_name = 'build/index.html'
@@ -278,178 +279,10 @@ class BuildComplete(AjaxUpdateView):
         }
 
 
-class BuildOutputComplete(AjaxUpdateView):
-    """
-    View to mark a particular build output as Complete.
-
-    - Notifies the user of which parts will be removed from stock.
-    - Assignes (tracked) allocated items from stock to the build output
-    - Deletes pending BuildItem objects
-    """
-
-    model = Build
-    form_class = forms.CompleteBuildOutputForm
-    context_object_name = "build"
-    ajax_form_title = _("Complete Build Output")
-    ajax_template_name = "build/complete_output.html"
-
-    def get_form(self):
-
-        build = self.get_object()
-
-        form = super().get_form()
-
-        # Extract the build output object
-        output = None
-        output_id = form['output'].value()
-
-        try:
-            output = StockItem.objects.get(pk=output_id)
-        except (ValueError, StockItem.DoesNotExist):
-            pass
-
-        if output:
-            if build.isFullyAllocated(output):
-                form.fields['confirm_incomplete'].widget = HiddenInput()
-
-        return form
-
-    def validate(self, build, form, **kwargs):
-        """
-        Custom validation steps for the BuildOutputComplete" form
-        """
-
-        data = form.cleaned_data
-
-        output = data.get('output', None)
-
-        stock_status = data.get('stock_status', StockStatus.OK)
-
-        # Any "invalid" stock status defaults to OK
-        try:
-            stock_status = int(stock_status)
-        except (ValueError):
-            stock_status = StockStatus.OK
-
-        if int(stock_status) not in StockStatus.keys():
-            form.add_error('stock_status', _('Invalid stock status value selected'))
-
-        if output:
-
-            quantity = data.get('quantity', None)
-
-            if quantity and quantity > output.quantity:
-                form.add_error('quantity', _('Quantity to complete cannot exceed build output quantity'))
-
-            if not build.isFullyAllocated(output):
-                confirm = str2bool(data.get('confirm_incomplete', False))
-
-                if not confirm:
-                    form.add_error('confirm_incomplete', _('Confirm completion of incomplete build'))
-
-        else:
-            form.add_error(None, _('Build output must be specified'))
-
-    def get_initial(self):
-        """ Get initial form data for the CompleteBuild form
-
-        - If the part being built has a default location, pre-select that location
-        """
-
-        initials = super().get_initial()
-        build = self.get_object()
-
-        if build.part.default_location is not None:
-            try:
-                location = StockLocation.objects.get(pk=build.part.default_location.id)
-                initials['location'] = location
-            except StockLocation.DoesNotExist:
-                pass
-
-        output = self.get_param('output', None)
-
-        if output:
-            try:
-                output = StockItem.objects.get(pk=output)
-            except (ValueError, StockItem.DoesNotExist):
-                output = None
-
-        # Output has not been supplied? Try to "guess"
-        if not output:
-
-            incomplete = build.get_build_outputs(complete=False)
-
-            if incomplete.count() == 1:
-                output = incomplete[0]
-
-        if output is not None:
-            initials['output'] = output
-
-        initials['location'] = build.destination
-
-        return initials
-
-    def get_context_data(self, **kwargs):
-        """
-        Get context data for passing to the rendered form
-
-        - Build information is required
-        """
-
-        build = self.get_object()
-
-        context = {}
-
-        # Build object
-        context['build'] = build
-
-        form = self.get_form()
-
-        output = form['output'].value()
-
-        if output:
-            try:
-                output = StockItem.objects.get(pk=output)
-                context['output'] = output
-                context['fully_allocated'] = build.isFullyAllocated(output)
-                context['allocated_parts'] = build.allocatedParts(output)
-                context['unallocated_parts'] = build.unallocatedParts(output)
-            except (ValueError, StockItem.DoesNotExist):
-                pass
-
-        return context
-
-    def save(self, build, form, **kwargs):
-
-        data = form.cleaned_data
-
-        location = data.get('location', None)
-        output = data.get('output', None)
-        stock_status = data.get('stock_status', StockStatus.OK)
-
-        # Any "invalid" stock status defaults to OK
-        try:
-            stock_status = int(stock_status)
-        except (ValueError):
-            stock_status = StockStatus.OK
-
-        # Complete the build output
-        build.completeBuildOutput(
-            output,
-            self.request.user,
-            location=location,
-            status=stock_status,
-        )
-
-    def get_data(self):
-        """ Provide feedback data back to the form """
-        return {
-            'success': _('Build output completed')
-        }
-
-
 class BuildDetail(InvenTreeRoleMixin, DetailView):
-    """ Detail view of a single Build object. """
+    """
+    Detail view of a single Build object.
+    """
 
     model = Build
     template_name = 'build/detail.html'
@@ -477,7 +310,9 @@ class BuildDetail(InvenTreeRoleMixin, DetailView):
 
 
 class BuildDelete(AjaxDeleteView):
-    """ View to delete a build """
+    """
+    View to delete a build
+    """
 
     model = Build
     ajax_template_name = 'build/delete_build.html'
diff --git a/InvenTree/common/templates/common/delete_currency.html b/InvenTree/common/templates/common/delete_currency.html
deleted file mode 100644
index 9dfa320668..0000000000
--- a/InvenTree/common/templates/common/delete_currency.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% extends "modal_delete_form.html" %}
-
-{% block pre_form_content %}
-
-Are you sure you wish to delete this currency?
-
-{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
index df0ec1a5de..f4ebff4dfb 100644
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -5,7 +5,6 @@ JSON API for the Order app
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
-from django.utils.translation import ugettext_lazy as _
 from django.conf.urls import url, include
 from django.db.models import Q, F
 
@@ -13,7 +12,6 @@ from django_filters import rest_framework as rest_filters
 from rest_framework import generics
 from rest_framework import filters, status
 from rest_framework.response import Response
-from rest_framework.serializers import ValidationError
 
 
 from InvenTree.filters import InvenTreeOrderingFilter
@@ -236,25 +234,15 @@ class POReceive(generics.CreateAPIView):
         context = super().get_serializer_context()
 
         # Pass the purchase order through to the serializer for validation
-        context['order'] = self.get_order()
+        try:
+            context['order'] = PurchaseOrder.objects.get(pk=self.kwargs.get('pk', None))
+        except:
+            pass
+
         context['request'] = self.request
 
         return context
 
-    def get_order(self):
-        """
-        Returns the PurchaseOrder associated with this API endpoint
-        """
-
-        pk = self.kwargs.get('pk', None)
-
-        try:
-            order = PurchaseOrder.objects.get(pk=pk)
-        except (PurchaseOrder.DoesNotExist, ValueError):
-            raise ValidationError(_("Matching purchase order does not exist"))
-        
-        return order
-
 
 class POLineItemFilter(rest_filters.FilterSet):
     """
diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py
index 227109c46c..6f0bc43c46 100644
--- a/InvenTree/order/forms.py
+++ b/InvenTree/order/forms.py
@@ -9,7 +9,7 @@ from django import forms
 from django.utils.translation import ugettext_lazy as _
 
 from InvenTree.forms import HelperForm
-from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
+from InvenTree.fields import InvenTreeMoneyField
 
 from InvenTree.helpers import clean_decimal
 
@@ -19,7 +19,6 @@ import part.models
 
 from .models import PurchaseOrder
 from .models import SalesOrder, SalesOrderLineItem
-from .models import SalesOrderAllocation
 
 
 class IssuePurchaseOrderForm(HelperForm):
@@ -81,6 +80,8 @@ class AllocateSerialsToSalesOrderForm(forms.Form):
     """
     Form for assigning stock to a sales order,
     by serial number lookup
+
+    TODO: Refactor this form / view to use the new API forms interface
     """
 
     line = forms.ModelChoiceField(
@@ -115,22 +116,6 @@ class AllocateSerialsToSalesOrderForm(forms.Form):
         ]
 
 
-class EditSalesOrderAllocationForm(HelperForm):
-    """
-    Form for editing a SalesOrderAllocation item
-    """
-
-    quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
-
-    class Meta:
-        model = SalesOrderAllocation
-
-        fields = [
-            'line',
-            'item',
-            'quantity']
-
-
 class OrderMatchItemForm(MatchItemForm):
     """ Override MatchItemForm fields """
 
diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html
index 8b98755900..ca7e70c4e3 100644
--- a/InvenTree/order/templates/order/order_base.html
+++ b/InvenTree/order/templates/order/order_base.html
@@ -36,31 +36,39 @@ src="{% static 'img/blank_image.png' %}"
 <p>{{ order.description }}{% include "clip.html"%}</p>
 <div class='btn-row'>
     <div class='btn-group action-buttons' role='group'>
-        <button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
-            <span class='fas fa-print'></span> 
-        </button>
-        <button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
-            <span class='fas fa-file-download'></span>
-        </button>
+        <!-- Printing options -->
+        <div class='btn-group'>
+            <button id='print-options' title='{% trans "Print actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
+                <span class='fas fa-print'></span> <span class='caret'></span>
+            </button>
+            <ul class='dropdown-menu' role='menu'>
+                <li><a href='#' id='print-order-report'><span class='fas fa-file-pdf'></span> {% trans "Print purchase order report" %}</a></li>
+                <li><a href='#' id='export-order'><span class='fas fa-file-download'></span> {% trans "Export order to file" %}</a></li>
+            </ul>
+        </div>
         {% if roles.purchase_order.change %}
-        <button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
-            <span class='fas fa-edit icon-green'></span>
-        </button>
+        <!-- order actions -->
+        <div class='btn-group'>
+            <button id='order-options' title='{% trans "Order actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
+                <span class='fas fa-tools'></span> <span class='caret'></span>
+            </button>
+            <ul class='dropdown-menu' role='menu'>
+                <li><a href='#' id='edit-order'><span class='fas fa-edit icon-green'></span> {% trans "Edit order" %}</a></li>
+                {% if order.can_cancel %}
+                <li><a href='#' id='cancel-order'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}</a></li>
+                {% endif %}
+            </ul>
+        </div>
         {% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %}
         <button type='button' class='btn btn-default' id='place-order' title='{% trans "Place order" %}'>
-            <span class='fas fa-paper-plane icon-blue'></span>
+            <span class='fas fa-shopping-cart icon-blue'></span>
         </button>
         {% elif order.status == PurchaseOrderStatus.PLACED %}
         <button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'>
-            <span class='fas fa-sign-in-alt'></span>
+            <span class='fas fa-sign-in-alt icon-blue'></span>
         </button>
         <button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'>
-            <span class='fas fa-check-circle'></span>
-        </button>
-        {% endif %}
-        {% if order.can_cancel %}
-        <button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
-            <span class='fas fa-times-circle icon-red'></span>
+            <span class='fas fa-check-circle icon-green'></span>
         </button>
         {% endif %}
         {% endif %}
diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html
index 3fd34e42b9..f7595cc182 100644
--- a/InvenTree/order/templates/order/sales_order_base.html
+++ b/InvenTree/order/templates/order/sales_order_base.html
@@ -47,30 +47,39 @@ src="{% static 'img/blank_image.png' %}"
 <p>{{ order.description }}{% include "clip.html"%}</p>
 <div class='btn-row'>
     <div class='btn-group action-buttons'>
-        <button type='button' class='btn btn-default' id='print-order-report' title='{% trans "Print" %}'>
-            <span class='fas fa-print'></span> 
-        </button>
-        <button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'>
-            <span class='fas fa-file-download'></span>
-        </button>
+        <!-- Printing actions -->
+        <div class='btn-group'>
+            <button id='print-options' title='{% trans "Print actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
+                <span class='fas fa-print'></span> <span class='caret'></span>
+            </button>
+            <ul class='dropdown-menu' role='menu'>
+                <li><a href='#' id='print-order-report'><span class='fas fa-file-pdf'></span> {% trans "Print sales order report" %}</a></li>
+                <li><a href='#' id='export-order'><span class='fas fa-file-download'></span> {% trans "Export order to file" %}</a></li>
+                <!--
+                <li><a href='#' id='print-packing-list'><span class='fas fa-clipboard-list'></span>{% trans "Print packing list" %}</a></li>
+                -->
+            </ul>
+        </div>
         {% if roles.sales_order.change %}
-        <button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'>
-            <span class='fas fa-edit icon-green'></span>
-        </button>
+        <!-- Order actions -->
+        <div class='btn-group'>
+            <button id='order-options' title='{% trans "Order actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
+                <span class='fas fa-tools'></span> <span class='caret'></span>
+            </button>
+            <ul class='dropdown-menu' role='menu'>
+                <li><a href='#' id='edit-order'><span class='fas fa-edit icon-green'></span> {% trans "Edit order" %}</a></li>
+                {% if order.status == SalesOrderStatus.PENDING %}
+                <li><a href='#' id='cancel-order'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}</a></li>
+                {% endif %}
+            </ul>
+
+        </div>
         {% if order.status == SalesOrderStatus.PENDING %}
         <button type='button' class='btn btn-default' id='ship-order' title='{% trans "Ship order" %}'>
-            <span class='fas fa-paper-plane icon-blue'></span>
-        </button>
-        <button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
-            <span class='fas fa-times-circle icon-red'></span>
+            <span class='fas fa-truck icon-blue'></span>
         </button>
         {% endif %}
         {% endif %}
-        <!--
-        <button type='button' disabled='' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'>
-            <span class='fas fa-clipboard-list'></span>
-        </button>
-        -->
     </div>
 </div>
 {% endblock %}
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
index 1f7905d1e3..899fa9a6fc 100644
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -203,6 +203,23 @@ class PurchaseOrderTest(OrderTest):
         # And if we try to access the detail view again, it has gone
         response = self.get(url, expected_code=404)
 
+    def test_po_create(self):
+        """
+        Test that we can create a new PurchaseOrder via the API
+        """
+
+        self.assignRole('purchase_order.add')
+
+        self.post(
+            reverse('api-po-list'),
+            {
+                'reference': '12345678',
+                'supplier': 1,
+                'description': 'A test purchase order',
+            },
+            expected_code=201
+        )
+
 
 class PurchaseOrderReceiveTest(OrderTest):
     """
@@ -607,3 +624,20 @@ class SalesOrderTest(OrderTest):
 
         # And the resource should no longer be available
         response = self.get(url, expected_code=404)
+
+    def test_so_create(self):
+        """
+        Test that we can create a new SalesOrder via the API
+        """
+
+        self.assignRole('sales_order.add')
+
+        self.post(
+            reverse('api-so-list'),
+            {
+                'reference': '1234566778',
+                'customer': 4,
+                'description': 'A test sales order',
+            },
+            expected_code=201
+        )
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index f5d7d39266..ddcb78ac2a 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -18,7 +18,7 @@ import common.models
 from common.forms import MatchItemForm
 
 from .models import Part, PartCategory, PartRelated
-from .models import PartParameterTemplate, PartParameter
+from .models import PartParameterTemplate
 from .models import PartCategoryParameterTemplate
 from .models import PartSellPriceBreak, PartInternalPriceBreak
 
@@ -188,18 +188,6 @@ class EditPartParameterTemplateForm(HelperForm):
         ]
 
 
-class EditPartParameterForm(HelperForm):
-    """ Form for editing a PartParameter object """
-
-    class Meta:
-        model = PartParameter
-        fields = [
-            'part',
-            'template',
-            'data'
-        ]
-
-
 class EditCategoryForm(HelperForm):
     """ Form for editing a PartCategory object """
 
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index 7e739306b0..96b6e0ba91 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -16,8 +16,6 @@ from InvenTree.forms import HelperForm
 from InvenTree.fields import RoundingDecimalFormField
 from InvenTree.fields import DatePickerFormField
 
-from report.models import TestReport
-
 from part.models import Part
 
 from .models import StockLocation, StockItem, StockItemTracking
@@ -26,6 +24,8 @@ from .models import StockLocation, StockItem, StockItemTracking
 class AssignStockItemToCustomerForm(HelperForm):
     """
     Form for manually assigning a StockItem to a Customer
+
+    TODO: This could be a simple API driven form!
     """
 
     class Meta:
@@ -38,6 +38,8 @@ class AssignStockItemToCustomerForm(HelperForm):
 class ReturnStockItemForm(HelperForm):
     """
     Form for manually returning a StockItem into stock
+
+    TODO: This could be a simple API driven form!
     """
 
     class Meta:
@@ -48,7 +50,11 @@ class ReturnStockItemForm(HelperForm):
 
 
 class EditStockLocationForm(HelperForm):
-    """ Form for editing a StockLocation """
+    """
+    Form for editing a StockLocation
+
+    TODO: Migrate this form to the modern API forms interface
+    """
 
     class Meta:
         model = StockLocation
@@ -63,6 +69,8 @@ class EditStockLocationForm(HelperForm):
 class ConvertStockItemForm(HelperForm):
     """
     Form for converting a StockItem to a variant of its current part.
+
+    TODO: Migrate this form to the modern API forms interface
     """
 
     class Meta:
@@ -73,7 +81,11 @@ class ConvertStockItemForm(HelperForm):
 
 
 class CreateStockItemForm(HelperForm):
-    """ Form for creating a new StockItem """
+    """
+    Form for creating a new StockItem
+    
+    TODO: Migrate this form to the modern API forms interface
+    """
 
     expiry_date = DatePickerFormField(
         label=_('Expiry Date'),
@@ -129,7 +141,11 @@ class CreateStockItemForm(HelperForm):
 
 
 class SerializeStockForm(HelperForm):
-    """ Form for serializing a StockItem. """
+    """
+    Form for serializing a StockItem.
+    
+    TODO: Migrate this form to the modern API forms interface
+    """
 
     destination = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label=_('Destination'), required=True, help_text=_('Destination for serialized stock (by default, will remain in current location)'))
 
@@ -160,73 +176,11 @@ class SerializeStockForm(HelperForm):
         ]
 
 
-class StockItemLabelSelectForm(HelperForm):
-    """ Form for selecting a label template for a StockItem """
-
-    label = forms.ChoiceField(
-        label=_('Label'),
-        help_text=_('Select test report template')
-    )
-
-    class Meta:
-        model = StockItem
-        fields = [
-            'label',
-        ]
-
-    def get_label_choices(self, labels):
-
-        choices = []
-
-        if len(labels) > 0:
-            for label in labels:
-                choices.append((label.pk, label))
-
-        return choices
-
-    def __init__(self, labels, *args, **kwargs):
-
-        super().__init__(*args, **kwargs)
-
-        self.fields['label'].choices = self.get_label_choices(labels)
-
-
-class TestReportFormatForm(HelperForm):
-    """ Form for selection a test report template """
-
-    class Meta:
-        model = StockItem
-        fields = [
-            'template',
-        ]
-
-    def __init__(self, stock_item, *args, **kwargs):
-        self.stock_item = stock_item
-
-        super().__init__(*args, **kwargs)
-        self.fields['template'].choices = self.get_template_choices()
-
-    def get_template_choices(self):
-        """
-        Generate a list of of TestReport options for the StockItem
-        """
-
-        choices = []
-
-        templates = TestReport.objects.filter(enabled=True)
-
-        for template in templates:
-            if template.enabled and template.matches_stock_item(self.stock_item):
-                choices.append((template.pk, template))
-
-        return choices
-
-    template = forms.ChoiceField(label=_('Template'), help_text=_('Select test report template'))
-
-
 class InstallStockForm(HelperForm):
     """
     Form for manually installing a stock item into another stock item
+
+    TODO: Migrate this form to the modern API forms interface
     """
 
     part = forms.ModelChoiceField(
@@ -275,6 +229,8 @@ class InstallStockForm(HelperForm):
 class UninstallStockForm(forms.ModelForm):
     """
     Form for uninstalling a stock item which is installed in another item.
+
+    TODO: Migrate this form to the modern API forms interface
     """
 
     location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label=_('Location'), help_text=_('Destination location for uninstalled items'))
@@ -301,6 +257,8 @@ class EditStockItemForm(HelperForm):
     location - Must be updated in a 'move' transaction
     quantity - Must be updated in a 'stocktake' transaction
     part - Cannot be edited after creation
+
+    TODO: Migrate this form to the modern API forms interface
     """
 
     expiry_date = DatePickerFormField(
diff --git a/InvenTree/stock/templates/stock/stock_adjust.html b/InvenTree/stock/templates/stock/stock_adjust.html
deleted file mode 100644
index 60a9ec2658..0000000000
--- a/InvenTree/stock/templates/stock/stock_adjust.html
+++ /dev/null
@@ -1,50 +0,0 @@
-{% load i18n %}
-{% load inventree_extras %}
-
-{% block pre_form_content %}
-
-{% endblock %}
-
-<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
-  {% csrf_token %}
-  {% load crispy_forms_tags %}
-
-  <input type='hidden' name='stock_action' value='{{ stock_action }}'/>
-
-  <table class='table table-condensed table-striped' id='stock-table'>
-      <tr>
-          <th>{% trans "Stock Item" %}</th>
-          <th>{% trans "Location" %}</th>
-          <th>{% trans "Quantity" %}</th>
-          {% if edit_quantity %}
-          <th>{{ stock_action_title }}</th>
-          {% endif %}
-          <th></th>
-      </tr>
-      {% for item in stock_items %}
-      <tr id='stock-row-{{ item.id }}' class='error'>
-          <td>{% include "hover_image.html" with image=item.part.image hover=True %}
-            {{ item.part.full_name }} <small><em>{{ item.part.description }}</em></small></td> 
-          <td>{{ item.location.pathstring }}</td> 
-          <td>{% decimal item.quantity %}</td>
-          <td>
-            {% if edit_quantity %}
-            <input class='numberinput'
-              min='0'
-              {% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
-              value='{% decimal item.new_quantity %}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
-            {% if item.error %}
-            <br><span class='help-inline'>{{ item.error }}</span>
-            {% endif %}
-            {% else %}
-            <input type='hidden' name='stock-id-{{ item.id }}' value='{{ item.new_quantity }}'/>
-            {% endif %}  
-          </td>
-          <td><button class='btn btn-default btn-remove' onclick='removeStockRow()' id='del-{{ item.id }}' title='{% trans "Remove item" %}' type='button'><span row='stock-row-{{ item.id }}' class='fas fa-trash-alt icon-red'></span></button></td>
-      </tr>
-      {% endfor %}
-    </table>
-
-  {% crispy form %}
-
-</form>
\ No newline at end of file
diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html
deleted file mode 100644
index c7de8c74b2..0000000000
--- a/InvenTree/stock/templates/stock/stock_move.html
+++ /dev/null
@@ -1 +0,0 @@
-{% extends "modal_form.html" %}
\ No newline at end of file
diff --git a/InvenTree/templates/attachment_delete.html b/InvenTree/templates/attachment_delete.html
deleted file mode 100644
index 4ee7f03cb1..0000000000
--- a/InvenTree/templates/attachment_delete.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% extends "modal_delete_form.html" %}
-{% load i18n %}
-
-{% block pre_form_content %}
-{% trans "Are you sure you want to delete this attachment?" %}
-<br>
-{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html
index c2316ce4b0..98d83aa2d9 100644
--- a/InvenTree/templates/base.html
+++ b/InvenTree/templates/base.html
@@ -143,7 +143,6 @@
 
 <!-- general InvenTree -->
 <script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
-<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
 
 <!-- dynamic javascript templates -->
 <script type='text/javascript' src="{% url 'inventree.js' %}"></script>
diff --git a/InvenTree/templates/collapse.html b/InvenTree/templates/collapse.html
deleted file mode 100644
index 5624f34094..0000000000
--- a/InvenTree/templates/collapse.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% block collapse_preamble %}
-{% endblock %}
-<div class='panel-group'>
-    <div class='panel panel-default'>
-        <div {% block collapse_panel_setup %}class='panel panel-heading'{% endblock %}>
-            <div class='row'>
-                <div class='col-sm-6'>
-                    <div class='panel-title'>
-                        <a data-toggle='collapse' href="#collapse-item-{{ collapse_id }}">{% block collapse_title %}Title{% endblock %}</a>
-                    </div>
-                </div>
-                {% block collapse_heading %}
-                {% endblock %}
-            </div>
-        </div>
-        <div class='panel-collapse collapse' id='collapse-item-{{ collapse_id }}'>
-            <div class='panel-body'>
-                {% block collapse_content %}
-                {% endblock %}
-            </div>
-        </div>
-    </div>
-</div>
\ No newline at end of file
diff --git a/InvenTree/templates/collapse_index.html b/InvenTree/templates/collapse_index.html
deleted file mode 100644
index 6e918d7217..0000000000
--- a/InvenTree/templates/collapse_index.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% block collapse_preamble %}
-{% endblock %}
-<div class='panel-group panel-index'>
-    <div class='panel panel-default'>
-        <div {% block collapse_panel_setup %}class='panel panel-heading'{% endblock %}>
-            <div class='panel-title'>
-                <a data-toggle='collapse' href="#collapse-item-{{ collapse_id }}">{% block collapse_title %}Title{% endblock %}</a>
-            </div>
-        {% block collapse_heading %}
-        {% endblock %}
-        </div>
-        <div class='panel-collapse collapse' id='collapse-item-{{ collapse_id }}'>
-            <div class='panel-body'>
-                {% block collapse_content %}
-                {% endblock %}
-            </div>
-        </div>
-    </div>
-</div>
\ No newline at end of file
diff --git a/InvenTree/templates/js/dynamic/inventree.js b/InvenTree/templates/js/dynamic/inventree.js
index 0324d72e3c..7513b4863a 100644
--- a/InvenTree/templates/js/dynamic/inventree.js
+++ b/InvenTree/templates/js/dynamic/inventree.js
@@ -290,3 +290,8 @@ function loadBrandIcon(element, name) {
         element.addClass('fab fa-' + name);
     }
 }
+
+// Convenience function to determine if an element exists
+$.fn.exists = function() {
+    return this.length !== 0;
+};
diff --git a/InvenTree/templates/js/dynamic/nav.js b/InvenTree/templates/js/dynamic/nav.js
index cf652724ed..ddf3cb8c12 100644
--- a/InvenTree/templates/js/dynamic/nav.js
+++ b/InvenTree/templates/js/dynamic/nav.js
@@ -3,6 +3,9 @@
 
 /* exported
     attachNavCallbacks,
+    enableNavbar,
+    initNavTree,
+    loadTree,
     onPanelLoad,
 */
 
@@ -113,3 +116,253 @@ function onPanelLoad(panel, callback) {
 
     });
 }
+
+function loadTree(url, tree, options={}) {
+    /* Load the side-nav tree view
+
+    Args:
+        url: URL to request tree data
+        tree: html ref to treeview
+        options:
+            data: data object to pass to the AJAX request
+            selected: ID of currently selected item
+            name: name of the tree
+    */
+
+    var data = {};
+
+    if (options.data) {
+        data = options.data;
+    }
+
+    var key = 'inventree-sidenav-items-';
+
+    if (options.name) {
+        key += options.name;
+    }
+
+    $.ajax({
+        url: url,
+        type: 'get',
+        dataType: 'json',
+        data: data,
+        success: function(response) {
+            if (response.tree) {
+                $(tree).treeview({
+                    data: response.tree,
+                    enableLinks: true,
+                    showTags: true,
+                });
+
+                if (localStorage.getItem(key)) {
+                    var saved_exp = localStorage.getItem(key).split(',');
+
+                    // Automatically expand the desired notes
+                    for (var q = 0; q < saved_exp.length; q++) {
+                        $(tree).treeview('expandNode', parseInt(saved_exp[q]));
+                    }
+                }
+
+                // Setup a callback whenever a node is toggled
+                $(tree).on('nodeExpanded nodeCollapsed', function(event, data) {
+                    
+                    // Record the entire list of expanded items
+                    var expanded = $(tree).treeview('getExpanded');
+
+                    var exp = [];
+
+                    for (var i = 0; i < expanded.length; i++) {
+                        exp.push(expanded[i].nodeId);
+                    }
+
+                    // Save the expanded nodes
+                    localStorage.setItem(key, exp);
+                });
+            }
+        },
+        error: function(xhr, ajaxOptions, thrownError) {
+            // TODO
+        }
+    });
+}
+
+
+/**
+ * Initialize navigation tree display
+ */
+function initNavTree(options) {
+
+    var resize = true;
+
+    if ('resize' in options) {
+        resize = options.resize;
+    }
+
+    var label = options.label || 'nav';
+
+    var stateLabel = `${label}-tree-state`;
+    var widthLabel = `${label}-tree-width`;
+
+    var treeId = options.treeId || '#sidenav-left';
+    var toggleId = options.toggleId;
+
+    // Initially hide the tree
+    $(treeId).animate({
+        width: '0px',
+    }, 0, function() {
+
+        if (resize) {
+            $(treeId).resizable({
+                minWidth: '0px',
+                maxWidth: '500px',
+                handles: 'e, se',
+                grid: [5, 5],
+                stop: function(event, ui) {
+                    var width = Math.round(ui.element.width());
+
+                    if (width < 75) {
+                        $(treeId).animate({
+                            width: '0px'
+                        }, 50);
+
+                        localStorage.setItem(stateLabel, 'closed');
+                    } else {
+                        localStorage.setItem(stateLabel, 'open');
+                        localStorage.setItem(widthLabel, `${width}px`);
+                    }
+                }
+            });
+        }
+
+        var state = localStorage.getItem(stateLabel);
+        var width = localStorage.getItem(widthLabel) || '300px';
+
+        if (state && state == 'open') {
+
+            $(treeId).animate({
+                width: width,
+            }, 50);
+        }
+    });
+
+    // Register callback for 'toggle' button
+    if (toggleId) {
+        
+        $(toggleId).click(function() {
+
+            var state = localStorage.getItem(stateLabel) || 'closed';
+            var width = localStorage.getItem(widthLabel) || '300px';
+
+            if (state == 'open') {
+                $(treeId).animate({
+                    width: '0px'
+                }, 50);
+
+                localStorage.setItem(stateLabel, 'closed');
+            } else {
+                $(treeId).animate({
+                    width: width,
+                }, 50);
+
+                localStorage.setItem(stateLabel, 'open');
+            }
+        });
+    }
+}
+
+
+/**
+ * Handle left-hand icon menubar display
+ */
+function enableNavbar(options) {
+
+    var resize = true;
+
+    if ('resize' in options) {
+        resize = options.resize;
+    }
+
+    var label = options.label || 'nav';
+
+    label = `navbar-${label}`;
+
+    var stateLabel = `${label}-state`;
+    var widthLabel = `${label}-width`;
+
+    var navId = options.navId || '#sidenav-right';
+
+    var toggleId = options.toggleId;
+
+    // Extract the saved width for this element
+    $(navId).animate({
+        'width': '45px',
+        'min-width': '45px',
+        'display': 'block',
+    }, 50, function() {
+
+        // Make the navbar resizable
+        if (resize) {
+            $(navId).resizable({
+                minWidth: options.minWidth || '100px',
+                maxWidth: options.maxWidth || '500px',
+                handles: 'e, se',
+                grid: [5, 5],
+                stop: function(event, ui) {
+                    // Record the new width
+                    var width = Math.round(ui.element.width());
+
+                    // Reasonably narrow? Just close it!
+                    if (width <= 75) {
+                        $(navId).animate({
+                            width: '45px'
+                        }, 50);
+
+                        localStorage.setItem(stateLabel, 'closed');
+                    } else {
+                        localStorage.setItem(widthLabel, `${width}px`);
+                        localStorage.setItem(stateLabel, 'open');
+                    }
+                }
+            });
+        }
+
+        var state = localStorage.getItem(stateLabel);
+
+        var width = localStorage.getItem(widthLabel) || '250px';
+        
+        if (state && state == 'open') {
+
+            $(navId).animate({
+                width: width
+            }, 100);
+        }
+
+    });
+
+    // Register callback for 'toggle' button
+    if (toggleId) {
+
+        $(toggleId).click(function() {
+
+            var state = localStorage.getItem(stateLabel) || 'closed';
+            var width = localStorage.getItem(widthLabel) || '250px';
+
+            if (state == 'open') {
+                $(navId).animate({
+                    width: '45px',
+                    minWidth: '45px',
+                }, 50);
+
+                localStorage.setItem(stateLabel, 'closed');
+
+            } else {
+
+                $(navId).animate({
+                    'width': width
+                }, 50);
+
+                localStorage.setItem(stateLabel, 'open');
+            }
+        });
+    }
+}
diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index d3499deedf..0c291dd8da 100644
--- a/InvenTree/templates/js/translated/build.js
+++ b/InvenTree/templates/js/translated/build.js
@@ -24,7 +24,7 @@
     loadAllocationTable,
     loadBuildOrderAllocationTable,
     loadBuildOutputAllocationTable,
-    loadBuildPartsTable,
+    loadBuildOutputTable,
     loadBuildTable,
 */
 
@@ -108,126 +108,56 @@ function newBuildOrder(options={}) {
 }
 
 
-function makeBuildOutputActionButtons(output, buildInfo, lines) {
-    /* Generate action buttons for a build output.
-     */
-
-    var buildId = buildInfo.pk;
-    var partId = buildInfo.part;
-
-    var outputId = 'untracked';
-
-    if (output) {
-        outputId = output.pk;
-    }
-
-    var panel = `#allocation-panel-${outputId}`;
-
-    function reloadTable() {
-        $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('refresh');
-    }
-
-    // Find the div where the buttons will be displayed
-    var buildActions = $(panel).find(`#output-actions-${outputId}`);
-
+/*
+ * Construct a set of output buttons for a particular build output
+ */
+function makeBuildOutputButtons(output_id, build_info, options={}) {
+ 
     var html = `<div class='btn-group float-right' role='group'>`;
 
-    if (lines > 0) {
-        html += makeIconButton(
-            'fa-sign-in-alt icon-blue', 'button-output-auto', outputId,
-            '{% trans "Allocate stock items to this build output" %}',
-        );
-    }
+    // Tracked parts? Must be individually allocated
+    if (build_info.tracked_parts) {
 
-    if (lines > 0) {
-        // Add a button to "cancel" the particular build output (unallocate)
+        // Add a button to allocate stock against this build output
         html += makeIconButton(
-            'fa-minus-circle icon-red', 'button-output-unallocate', outputId,
+            'fa-sign-in-alt icon-blue',
+            'button-output-allocate',
+            output_id,
+            '{% trans "Allocate stock items to this build output" %}',
+            {
+                disabled: true,
+            }
+        );
+
+        // Add a button to unallocate stock from this build output
+        html += makeIconButton(
+            'fa-minus-circle icon-red',
+            'button-output-unallocate',
+            output_id,
             '{% trans "Unallocate stock from build output" %}',
         );
     }
 
-    if (output) {
+    // Add a button to "complete" this build output
+    html += makeIconButton(
+        'fa-check-circle icon-green',
+        'button-output-complete',
+        output_id,
+        '{% trans "Complete build output" %}',
+    );
 
-        // Add a button to "complete" the particular build output
-        html += makeIconButton(
-            'fa-check icon-green', 'button-output-complete', outputId,
-            '{% trans "Complete build output" %}',
-            {
-                // disabled: true
-            }
-        );
+    // Add a button to "delete" this build output
+    html += makeIconButton(
+        'fa-trash-alt icon-red',
+        'button-output-delete',
+        output_id,
+        '{% trans "Delete build output" %}',
+    );
 
-        // Add a button to "delete" the particular build output
-        html += makeIconButton(
-            'fa-trash-alt icon-red', 'button-output-delete', outputId,
-            '{% trans "Delete build output" %}',
-        );
+    html += `</div>`;
 
-        // TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap)
-    }
+    return html;
 
-    html += '</div>';
-
-    buildActions.html(html);
-
-    // Add callbacks for the buttons
-    $(panel).find(`#button-output-auto-${outputId}`).click(function() {
-
-        var bom_items = $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('getData');
-
-        // Launch modal dialog to perform auto-allocation
-        allocateStockToBuild(
-            buildId,
-            partId,
-            bom_items,
-            {
-                source_location: buildInfo.source_location,
-                output: outputId,
-                success: reloadTable,
-            }
-        );
-    });
-
-    $(panel).find(`#button-output-complete-${outputId}`).click(function() {
-
-        var pk = $(this).attr('pk');
-
-        launchModalForm(
-            `/build/${buildId}/complete-output/`,
-            {
-                data: {
-                    output: pk,
-                },
-                reload: true,
-            }
-        );  
-    });
-
-    $(panel).find(`#button-output-unallocate-${outputId}`).click(function() {
-
-        var pk = $(this).attr('pk');
-
-        unallocateStock(buildId, {
-            output: pk,
-            table: table,
-        });
-    });
-
-    $(panel).find(`#button-output-delete-${outputId}`).click(function() {
-
-        var pk = $(this).attr('pk');
-
-        launchModalForm(
-            `/build/${buildId}/delete-output/`,
-            {
-                reload: true,
-                data: {
-                    output: pk
-                }
-            }
-        );
-    });
 }
 
 
@@ -270,14 +200,160 @@ function unallocateStock(build_id, options={}) {
             }
         }
     });
-
 }
 
 
+/**
+ * Launch a modal form to complete selected build outputs
+ */
+function completeBuildOutputs(build_id, outputs, options={}) {
+    
+    if (outputs.length == 0) {
+        showAlertDialog(
+            '{% trans "Select Build Outputs" %}',
+            '{% trans "At least one build output must be selected" %}',
+        );
+        return;
+    }
+
+    // Render a single build output (StockItem)
+    function renderBuildOutput(output, opts={}) {
+        var pk = output.pk;
+
+        var output_html = imageHoverIcon(output.part_detail.thumbnail);
+
+        if (output.quantity == 1 && output.serial) {
+            output_html += `{% trans "Serial Number" %}: ${output.serial}`;
+        } else {
+            output_html += `{% trans "Quantity" %}: ${output.quantity}`;
+        }
+
+        var buttons = `<div class='btn-group float-right' role='group'>`;
+
+        buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}');
+
+        buttons += '</div>';
+
+        var field = constructField(
+            `outputs_output_${pk}`,
+            {
+                type: 'raw',
+                html: output_html,
+            },
+            {
+                hideLabels: true,
+            }
+        );
+
+        var html = `
+        <tr id='output_row_${pk}'>
+            <td>${field}</td>
+            <td>${output.part_detail.full_name}</td>
+            <td>${buttons}</td>
+        </tr>`;
+
+        return html;
+    }
+
+    // Construct table entries
+    var table_entries = '';
+
+    outputs.forEach(function(output) {
+        table_entries += renderBuildOutput(output);
+    });
+
+    var html = `
+    <table class='table table-striped table-condensed' id='build-complete-table'>
+        <thead>
+            <th colspan='2'>{% trans "Output" %}</th>
+            <th><!-- Actions --></th>
+        </thead>
+        <tbody>
+            ${table_entries}
+        </tbody>
+    </table>`;
+
+    constructForm(`/api/build/${build_id}/complete/`, {
+        method: 'POST',
+        preFormContent: html,
+        fields: {
+            status: {},
+            location: {},
+        },
+        confirm: true,
+        title: '{% trans "Complete Build Outputs" %}',
+        afterRender: function(fields, opts) {
+            // Setup callbacks to remove outputs
+            $(opts.modal).find('.button-row-remove').click(function() {
+                var pk = $(this).attr('pk');
+
+                $(opts.modal).find(`#output_row_${pk}`).remove();
+            });
+        },
+        onSubmit: function(fields, opts) {
+            
+            // Extract data elements from the form
+            var data = {
+                outputs: [],
+                status: getFormFieldValue('status', {}, opts),
+                location: getFormFieldValue('location', {}, opts),
+            };
+
+            var output_pk_values = [];
+
+            outputs.forEach(function(output) {
+                var pk = output.pk;
+
+                var row = $(opts.modal).find(`#output_row_${pk}`);
+
+                if (row.exists()) {
+                    data.outputs.push({
+                        output: pk,
+                    });
+                    output_pk_values.push(pk);
+                }
+            });
+
+            // Provide list of nested values
+            opts.nested = {
+                'outputs': output_pk_values,
+            };
+
+            inventreePut(
+                opts.url,
+                data,
+                {
+                    method: 'POST',
+                    success: function(response) {
+                        // Hide the modal
+                        $(opts.modal).modal('hide');
+
+                        if (options.success) {
+                            options.success(response);
+                        }
+                    },
+                    error: function(xhr) {
+                        switch (xhr.status) {
+                        case 400:
+                            handleFormErrors(xhr.responseJSON, fields, opts);
+                            break;
+                        default:
+                            $(opts.modal).modal('hide');
+                            showApiError(xhr);
+                            break;
+                        }
+                    }
+                }
+            );
+        }
+    });
+}
+
+
+/**
+ * Load a table showing all the BuildOrder allocations for a given part
+ */
 function loadBuildOrderAllocationTable(table, options={}) {
-    /**
-     * Load a table showing all the BuildOrder allocations for a given part
-     */
 
     options.params['part_detail'] = true;
     options.params['build_detail'] = true;
@@ -357,17 +433,256 @@ function loadBuildOrderAllocationTable(table, options={}) {
 }
 
 
-function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
+/*
+ * Display a "build output" table for a particular build.
+ *
+ * This displays a list of "active" (i.e. "in production") build outputs for a given build
+ * 
+ */
+function loadBuildOutputTable(build_info, options={}) {
+
+    var table = options.table || '#build-output-table';
+
+    var params = options.params || {};
+
+    // Mandatory query filters
+    params.part_detail = true;
+    params.is_building = true;
+    params.build = build_info.pk;
+
+    // Construct a list of "tracked" BOM items
+    var tracked_bom_items = [];
+
+    var has_tracked_items = false;
+
+    build_info.bom_items.forEach(function(bom_item) {
+        if (bom_item.sub_part_detail.trackable) {
+            tracked_bom_items.push(bom_item);
+            has_tracked_items = true;
+        };
+    });
+
+    var filters = {};
+
+    for (var key in params) {
+        filters[key] = params[key];
+    }
+
+    // TODO: Initialize filter list
+
+    function setupBuildOutputButtonCallbacks() {
+        
+        // Callback for the "allocate" button
+        $(table).find('.button-output-allocate').click(function() {
+            var pk = $(this).attr('pk');
+
+            // Find the "allocation" sub-table associated with this output
+            var subtable = $(`#output-sub-table-${pk}`);
+
+            if (subtable.exists()) {
+                var rows = subtable.bootstrapTable('getSelections');
+
+                // None selected? Use all!
+                if (rows.length == 0) {
+                    rows = subtable.bootstrapTable('getData');
+                }
+
+                allocateStockToBuild(
+                    build_info.pk,
+                    build_info.part,
+                    rows,
+                    {
+                        output: pk,
+                        success: function() {
+                            $(table).bootstrapTable('refresh');
+                        }
+                    }
+                );
+            } else {
+                console.log(`WARNING: Could not locate sub-table for output ${pk}`);
+            }
+        });
+
+        // Callack for the "unallocate" button
+        $(table).find('.button-output-unallocate').click(function() {
+            var pk = $(this).attr('pk');
+
+            unallocateStock(build_info.pk, {
+                output: pk,
+                table: table
+            });
+        });
+
+        // Callback for the "complete" button
+        $(table).find('.button-output-complete').click(function() {
+            var pk = $(this).attr('pk');
+
+            var output = $(table).bootstrapTable('getRowByUniqueId', pk);
+
+            completeBuildOutputs(
+                build_info.pk,
+                [
+                    output,
+                ],
+                {
+                    success: function() {
+                        $(table).bootstrapTable('refresh');
+                    }
+                }
+            );
+        });
+
+        // Callback for the "delete" button
+        $(table).find('.button-output-delete').click(function() {
+            var pk = $(this).attr('pk');
+
+            // TODO: Move this to the API
+            launchModalForm(
+                `/build/${build_info.pk}/delete-output/`,
+                {
+                    data: {
+                        output: pk
+                    },
+                    onSuccess: function() {
+                        $(table).bootstrapTable('refresh');
+                    }
+                }
+            );
+        });
+    }
+
     /*
-     * Load the "allocation table" for a particular build output.
-     * 
-     * Args:
-     * - buildId: The PK of the Build object
-     * - partId: The PK of the Part object
-     * - output: The StockItem object which is the "output" of the build
-     * - options:
-     * -- table: The #id of the table (will be auto-calculated if not provided)
+     * Construct a "sub table" showing the required BOM items
      */
+    function constructBuildOutputSubTable(index, row, element) {
+        var sub_table_id = `output-sub-table-${row.pk}`;
+
+        var html = `
+        <div class='sub-table'>
+            <table class='table table-striped table-condensed' id='${sub_table_id}'></table>
+        </div>
+        `;
+
+        element.html(html);
+
+        loadBuildOutputAllocationTable(
+            build_info,
+            row,
+            {
+                table: `#${sub_table_id}`,
+                parent_table: table,
+            }
+        );
+    }
+
+    $(table).inventreeTable({
+        url: '{% url "api-stock-list" %}',
+        queryParams: filters,
+        original: params,
+        showColumns: false,
+        uniqueId: 'pk',
+        name: 'build-outputs',
+        sortable: true,
+        search: false,
+        sidePagination: 'server',
+        detailView: has_tracked_items,
+        detailFilter: function(index, row) {
+            return true;
+        },
+        detailFormatter: function(index, row, element) {
+            constructBuildOutputSubTable(index, row, element);
+        },
+        formatNoMatches: function() {
+            return '{% trans "No active build outputs found" %}';
+        },
+        onPostBody: function() {
+            // Add callbacks for the buttons
+            setupBuildOutputButtonCallbacks();
+
+            $(table).bootstrapTable('expandAllRows');
+        },
+        columns: [
+            {
+                title: '',
+                visible: true,
+                checkbox: true,
+                switchable: false,
+            },
+            {
+                field: 'part',
+                title: '{% trans "Part" %}',
+                formatter: function(value, row) {
+                    var thumb = row.part_detail.thumbnail;
+
+                    return imageHoverIcon(thumb) + row.part_detail.full_name + makePartIcons(row.part_detail);
+                }
+            },
+            {
+                field: 'quantity',
+                title: '{% trans "Quantity" %}',
+                formatter: function(value, row) {
+
+                    var url = `/stock/item/${row.pk}/`;
+
+                    var text = '';
+
+                    if (row.serial && row.quantity == 1) {
+                        text = `{% trans "Serial Number" %}: ${row.serial}`;
+                    } else {
+                        text = `{% trans "Quantity" %}: ${row.quantity}`;
+                    }
+
+                    return renderLink(text, url);
+                }
+            },
+            {
+                field: 'allocated',
+                title: '{% trans "Allocated Parts" %}',
+                visible: has_tracked_items,
+                formatter: function(value, row) {
+                    return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`;
+                }
+            },
+            {
+                field: 'actions',
+                title: '',
+                switchable: false,
+                formatter: function(value, row) {
+                    return makeBuildOutputButtons(
+                        row.pk,
+                        build_info,
+                    );
+                }
+            }
+        ]
+    });
+
+    // Enable the "allocate" button when the sub-table is exanded
+    $(table).on('expand-row.bs.table', function(detail, index, row) {
+        $(`#button-output-allocate-${row.pk}`).prop('disabled', false);
+    });
+    
+    // Disable the "allocate" button when the sub-table is collapsed
+    $(table).on('collapse-row.bs.table', function(detail, index, row) {
+        $(`#button-output-allocate-${row.pk}`).prop('disabled', true);
+    });
+}
+
+
+/*
+ * Display the "allocation table" for a particular build output.
+ * 
+ * This displays a table of required allocations for a particular build output
+ * 
+ * Args:
+ * - buildId: The PK of the Build object
+ * - partId: The PK of the Part object
+ * - output: The StockItem object which is the "output" of the build
+ * - options:
+ * -- table: The #id of the table (will be auto-calculated if not provided)
+ */
+function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
+    
 
     var buildId = buildInfo.pk;
     var partId = buildInfo.part;
@@ -534,7 +849,11 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
         },
         name: 'build-allocation',
         uniqueId: 'sub_part',
-        onPostBody: setupCallbacks,
+        search: options.search || false,
+        onPostBody: function(data) {
+            // Setup button callbacks
+            setupCallbacks();
+        },
         onLoadSuccess: function(tableData) {
             // Once the BOM data are loaded, request allocation data for this build output
 
@@ -610,31 +929,31 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
                             $(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
                         }
 
-                        // Update the total progress for this build output
-                        var buildProgress = $(`#allocation-panel-${outputId}`).find($(`#output-progress-${outputId}`));
+                        // Update the progress bar for this build output
+                        var build_progress = $(`#output-progress-${outputId}`);
 
-                        if (totalLines > 0) {
+                        if (build_progress.exists()) {
+                            if (totalLines > 0) {
 
-                            var progress = makeProgressBar(
-                                allocatedLines,
-                                totalLines
-                            );
-
-                            buildProgress.html(progress);
+                                var progress = makeProgressBar(
+                                    allocatedLines,
+                                    totalLines
+                                );
+    
+                                build_progress.html(progress);
+                            } else {
+                                build_progress.html('');
+                            }
+    
                         } else {
-                            buildProgress.html('');
+                            console.log(`WARNING: Could not find progress bar for output ${outputId}`);
                         }
-
-                        // Update the available actions for this build output
-
-                        makeBuildOutputActionButtons(output, buildInfo, totalLines);
                     }
                 }
             );
         },
         sortable: true,
         showColumns: false,
-        detailViewByClick: true,
         detailView: true,
         detailFilter: function(index, row) {
             return row.allocations != null;
@@ -883,9 +1202,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
             },
         ]
     });
-
-    // Initialize the action buttons
-    makeBuildOutputActionButtons(output, buildInfo, 0);
 }
 
 
@@ -995,10 +1311,13 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
             remaining = 0;
         }
 
-        table_entries += renderBomItemRow(bom_item, remaining);
+        // We only care about entries which are not yet fully allocated
+        if (remaining > 0) {
+            table_entries += renderBomItemRow(bom_item, remaining);
+        }
     }
 
-    if (bom_items.length == 0) {
+    if (table_entries.length == 0) {
 
         showAlertDialog(
             '{% trans "Select Parts" %}',
@@ -1198,9 +1517,10 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
 }
 
 
-
+/*
+ * Display a table of Build orders
+ */
 function loadBuildTable(table, options) {
-    // Display a table of Build objects
 
     var params = options.params || {};
 
@@ -1467,190 +1787,4 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
             }
         });
     });
-
-}
-
-
-function loadBuildPartsTable(table, options={}) {
-    /**
-     * Display a "required parts" table for build view.
-     * 
-     * This is a simplified BOM view:
-     * - Does not display sub-bom items
-     * - Does not allow editing of BOM items
-     * 
-     * Options:
-     * 
-     * part: Part ID
-     * build: Build ID
-     * build_quantity: Total build quantity
-     * build_remaining: Number of items remaining
-     */
-
-    // Query params
-    var params = {
-        sub_part_detail: true,
-        part: options.part,
-    };
-
-    var filters = {};
-
-    if (!options.disableFilters) {
-        filters = loadTableFilters('bom');
-    }
-
-    setupFilterList('bom', $(table));
-
-    for (var key in params) {
-        filters[key] = params[key];
-    }
-
-    function setupTableCallbacks() {
-        // Register button callbacks once the table data are loaded
-
-        // Callback for 'buy' button
-        $(table).find('.button-buy').click(function() {
-            var pk = $(this).attr('pk');
-
-            launchModalForm('{% url "order-parts" %}', {
-                data: {
-                    parts: [
-                        pk,
-                    ]
-                }
-            });
-        });
-
-        // Callback for 'build' button
-        $(table).find('.button-build').click(function() {
-            var pk = $(this).attr('pk');
-
-            newBuildOrder({
-                part: pk,
-                parent: options.build,
-            });
-        });
-    }
-
-    var columns = [
-        {
-            field: 'sub_part',
-            title: '{% trans "Part" %}',
-            switchable: false,
-            sortable: true,
-            formatter: function(value, row) {
-                var url = `/part/${row.sub_part}/`;
-                var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url);
-
-                var sub_part = row.sub_part_detail;
-
-                html += makePartIcons(row.sub_part_detail);
-
-                // Display an extra icon if this part is an assembly
-                if (sub_part.assembly) {
-                    var text = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream label-right'></span>`;
-
-                    html += renderLink(text, `/part/${row.sub_part}/bom/`);
-                }
-
-                return html;
-            }
-        },
-        {
-            field: 'sub_part_detail.description',
-            title: '{% trans "Description" %}',
-        },
-        {
-            field: 'reference',
-            title: '{% trans "Reference" %}',
-            searchable: true,
-            sortable: true,
-        },
-        {
-            field: 'quantity',
-            title: '{% trans "Quantity" %}',
-            sortable: true
-        },
-        {
-            sortable: true,
-            switchable: false,
-            field: 'sub_part_detail.stock',
-            title: '{% trans "Available" %}',
-            formatter: function(value, row) {
-                return makeProgressBar(
-                    value,
-                    row.quantity * options.build_remaining,
-                    {
-                        id: `part-progress-${row.part}`
-                    }
-                );
-            },
-            sorter: function(valA, valB, rowA, rowB) {
-                if (rowA.received == 0 && rowB.received == 0) {
-                    return (rowA.quantity > rowB.quantity) ? 1 : -1;
-                }
-
-                var progressA = parseFloat(rowA.sub_part_detail.stock) / (rowA.quantity * options.build_remaining);
-                var progressB = parseFloat(rowB.sub_part_detail.stock) / (rowB.quantity * options.build_remaining);
-
-                return (progressA < progressB) ? 1 : -1;
-            }
-        },
-        {
-            field: 'actions',
-            title: '{% trans "Actions" %}',
-            switchable: false,
-            formatter: function(value, row) {
-
-                // Generate action buttons against the part
-                var html = `<div class='btn-group float-right' role='group'>`;
-
-                if (row.sub_part_detail.assembly) {
-                    html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}');
-                }
-
-                if (row.sub_part_detail.purchaseable) {
-                    html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}');
-                }
-
-                html += `</div>`;
-
-                return html;
-            }
-        }
-    ];
-
-    table.inventreeTable({
-        url: '{% url "api-bom-list" %}',
-        showColumns: true,
-        name: 'build-parts',
-        sortable: true,
-        search: true,
-        onPostBody: setupTableCallbacks,
-        rowStyle: function(row) {
-            var classes = [];
-
-            // Shade rows differently if they are for different parent parts
-            if (row.part != options.part) {
-                classes.push('rowinherited');
-            }
-
-            if (row.validated) {
-                classes.push('rowvalid');
-            } else {
-                classes.push('rowinvalid');
-            }
-
-            return {
-                classes: classes.join(' '),
-            };
-        },
-        formatNoMatches: function() {
-            return '{% trans "No BOM items found" %}';
-        },
-        clickToSelect: true,
-        queryParams: filters,
-        original: params,
-        columns: columns,
-    });    
 }
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index 2483263219..1bfe196286 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -1843,6 +1843,8 @@ function constructInput(name, parameters, options) {
     case 'candy':
         func = constructCandyInput;
         break;
+    case 'raw':
+        func = constructRawInput;
     default:
         // Unsupported field type!
         break;
@@ -2086,6 +2088,17 @@ function constructCandyInput(name, parameters) {
 }
 
 
+/*
+ * Construct a "raw" field input
+ * No actual field data!
+ */
+function constructRawInput(name, parameters) {
+
+    return parameters.html;
+
+}
+
+
 /*
  * Construct a 'help text' div based on the field parameters
  * 
diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js
index 164452952d..1bc15ea402 100644
--- a/InvenTree/templates/js/translated/helpers.js
+++ b/InvenTree/templates/js/translated/helpers.js
@@ -87,8 +87,10 @@ function select2Thumbnail(image) {
 }
 
 
+/*
+ * Construct an 'icon badge' which floats to the right of an object
+ */
 function makeIconBadge(icon, title) {
-    // Construct an 'icon badge' which floats to the right of an object
 
     var html = `<span class='fas ${icon} label-right' title='${title}'></span>`;
 
@@ -96,8 +98,10 @@ function makeIconBadge(icon, title) {
 }
 
 
+/*
+ * Construct an 'icon button' using the fontawesome set
+ */
 function makeIconButton(icon, cls, pk, title, options={}) {
-    // Construct an 'icon button' using the fontawesome set
 
     var classes = `btn btn-default btn-glyph ${cls}`;
 
diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js
index 3138c1e73d..fed7a5d980 100644
--- a/InvenTree/templates/js/translated/tables.js
+++ b/InvenTree/templates/js/translated/tables.js
@@ -56,10 +56,10 @@ function enableButtons(elements, enabled) {
 }
 
 
+/* Link a bootstrap-table object to one or more buttons.
+ * The buttons will only be enabled if there is at least one row selected
+ */
 function linkButtonsToSelection(table, buttons) {
-    /* Link a bootstrap-table object to one or more buttons.
-     * The buttons will only be enabled if there is at least one row selected
-     */
 
     if (typeof table === 'string') {
         table = $(table);
diff --git a/InvenTree/templates/required_part_table.html b/InvenTree/templates/required_part_table.html
deleted file mode 100644
index a1e26e2894..0000000000
--- a/InvenTree/templates/required_part_table.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<table class='table table-striped table-condensed' id='{{ table_id }}'>
-    <tr>
-        <th data-field='part' data-sortable='true' data-searchable='true'>Part</th>
-        <th data-field='part' data-sortable='true' data-searchable='true'>Description</th>
-        <th data-field='part' data-sortable='true' data-searchable='true'>In Stock</th>
-        <th data-field='part' data-sortable='true' data-searchable='true'>On Order</th>
-        <th data-field='part' data-sortable='true' data-searchable='true'>Allocted</th>
-        <th data-field='part' data-sortable='true' data-searchable='true'>Net Stock</th>
-    </tr>
-    {% for part in parts %}
-    <tr>
-        <td>
-            {% include "hover_image.html" with image=part.image hover=True %}
-            <a href="{% url 'part-detail' part.id %}">{{ part.full_name }}</a>
-        </td>
-        <td>{{ part.description }}</td>
-        <td>{{ part.total_stock }}</td>
-        <td>{{ part.on_order }}</td>
-        <td>{{ part.allocation_count }}</td>
-        <td{% if part.net_stock < 0 %} class='red-cell'{% endif %}>{{ part.net_stock }}</td>
-    </tr>
-    {% endfor %}
-</table>
\ No newline at end of file
diff --git a/InvenTree/templates/slide.html b/InvenTree/templates/slide.html
deleted file mode 100644
index edd39e75a2..0000000000
--- a/InvenTree/templates/slide.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<div>
-    <input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled or not roles.part.change %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off">
-</div>
\ No newline at end of file