diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 04d1978745..18267f33c4 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -177,7 +177,10 @@ class AjaxCreateView(AjaxMixin, CreateView):
             # Return the PK of the newly-created object
             data['pk'] = obj.pk
 
-            data['url'] = obj.get_absolute_url()
+            try:
+                data['url'] = obj.get_absolute_url()
+            except AttributeError:
+                pass
 
         return self.renderJsonResponse(request, form, data)
 
@@ -223,7 +226,11 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
             
             # Include context data about the updated object
             data['pk'] = obj.id
-            data['url'] = obj.get_absolute_url()
+
+            try:
+                data['url'] = obj.get_absolute_url()
+            except AttributeError:
+                pass
 
         return self.renderJsonResponse(request, form, data)
 
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index bba8c3e973..0363b4ab37 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -9,7 +9,8 @@ from InvenTree.forms import HelperForm
 
 from django import forms
 
-from .models import Part, PartCategory, BomItem
+from .models import Part, PartCategory, PartAttachment
+from .models import BomItem
 from .models import SupplierPart
 
 
@@ -44,6 +45,18 @@ class BomExportForm(HelperForm):
         ]
 
 
+class EditPartAttachmentForm(HelperForm):
+    """ Form for editing a PartAttachment object """
+
+    class Meta:
+        model = PartAttachment
+        fields = [
+            'part',
+            'attachment',
+            'comment'
+        ]
+
+
 class EditPartForm(HelperForm):
     """ Form for editing a Part object """
 
diff --git a/InvenTree/part/migrations/0012_part_active.py b/InvenTree/part/migrations/0012_part_active.py
index c2f3b55f6c..87929e2f85 100644
--- a/InvenTree/part/migrations/0012_part_active.py
+++ b/InvenTree/part/migrations/0012_part_active.py
@@ -14,5 +14,10 @@ class Migration(migrations.Migration):
             model_name='part',
             name='active',
             field=models.BooleanField(default=True, help_text='Is this part active?'),
+        ),
+		migrations.AddField(
+            model_name='partattachment',
+            name='comment',
+            field=models.CharField(blank=True, help_text='File comment', max_length=100),
         ),
     ]
diff --git a/InvenTree/part/migrations/0014_partattachment_comment.py b/InvenTree/part/migrations/0014_partattachment_comment.py
deleted file mode 100644
index a51f588a59..0000000000
--- a/InvenTree/part/migrations/0014_partattachment_comment.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-04-30 23:34
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('part', '0013_auto_20190429_2229'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='partattachment',
-            name='comment',
-            field=models.CharField(blank=True, help_text='Attachment description', max_length=100),
-        ),
-    ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 19682929cf..ab83b584cd 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -125,7 +125,7 @@ class Part(models.Model):
     IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
 
     # Provide a URL for an external link
-    URL = models.URLField(blank=True, help_text='Link to external URL')
+    URL = models.URLField(blank=True, help_text='Link to extenal URL')
 
     # Part category - all parts must be assigned to a category
     category = models.ForeignKey(PartCategory, related_name='parts',
@@ -307,12 +307,6 @@ class Part(models.Model):
     def used_in_count(self):
         return self.used_in.count()
 
-    def required_parts(self):
-        parts = []
-        for bom in self.bom_items.all():
-            parts.append(bom.sub_part)
-        return parts
-
     @property
     def supplier_count(self):
         # Return the number of supplier parts available for this part
@@ -366,7 +360,7 @@ class PartAttachment(models.Model):
 
     attachment = models.FileField(upload_to=attach_file, null=True, blank=True)
 
-    comment = models.CharField(max_length=100, blank=True, help_text="Attachment description")
+    comment = models.CharField(max_length=100, blank=True, help_text='File comment')
 
     @property
     def basename(self):
@@ -407,11 +401,8 @@ class BomItem(models.Model):
         - A part cannot refer to a part which refers to it
         """
 
-        if self.part is None or self.sub_part is None:
-            # Field validation will catch these None values
-            pass
         # A part cannot refer to itself in its BOM
-        elif self.part == self.sub_part:
+        if self.part == self.sub_part:
             raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')})
 
         # Test for simple recursion
diff --git a/InvenTree/part/templates/part/attachment_delete.html b/InvenTree/part/templates/part/attachment_delete.html
new file mode 100644
index 0000000000..db98b7f6d6
--- /dev/null
+++ b/InvenTree/part/templates/part/attachment_delete.html
@@ -0,0 +1,3 @@
+Are you sure you wish to delete this attachment?
+<br>
+This will remove the file '{{ attachment.basename }}'.
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html
new file mode 100644
index 0000000000..24c69f23f7
--- /dev/null
+++ b/InvenTree/part/templates/part/attachments.html
@@ -0,0 +1,62 @@
+{% extends "part/part_base.html" %}
+{% load static %}
+
+{% block details %}
+
+{% include 'part/tabs.html' with tab='attachments' %}
+
+<h4>Attachments</h4>
+
+<div id='toolbar' class='btn-group'>
+    <button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
+</div>
+
+<table class='table table-striped table-condensed' data-toolbar='#toolbar' id='attachment-table'>
+<tr>
+    <th>File</th>
+    <th>Comment</th>
+    <th></th>
+</tr>
+{% for attachment in part.attachments.all %}
+<tr>
+    <td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
+    <td>{{ attachment.comment }}</td>
+    <td>
+        <div class='btn-group' style='float: right;'>    
+            <button type='button' class='btn btn-primary attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'>Edit</button>
+            <button type='button' class='btn btn-danger attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='Delete attachment ({{ attachment.basename }})'>Delete</button>
+        </div>
+    </td>
+</tr>
+{% endfor %}
+</table>
+
+{% endblock %}
+
+{% block js_ready %}
+{{ block.super }}
+
+    $("#new-attachment").click(function() {
+        launchModalForm("{% url 'part-attachment-create' %}?part={{ part.id }}");
+    });
+
+    $("#attachment-table").on('click', '.attachment-edit-button', function() {
+        var button = $(this);
+
+        launchModalForm(button.attr('url'), 
+            {
+                success: function() {
+            }
+        });
+    });
+
+    $("#attachment-table").on('click', '.attachment-delete-button', function() {
+        var button = $(this);
+
+        launchDeleteForm(button.attr('url'), {
+            success: function() {
+            }
+        });
+    });
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 79be275307..a1b4ddc2d1 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -66,13 +66,6 @@
         </tr>
         {% endif %}
     </table>
-
-    <h4>Attachments</h4>
-    <ul>
-    {% for attachment in part.attachments.all %}
-    <li><a href="/media/{{ attachment.attachment }}">{{ attachment.basename }}</a></li>
-    {% endfor %}
-    </ul>
 </div>
 </div>
 
diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html
index aa9d798bb7..2bcf894b2a 100644
--- a/InvenTree/part/templates/part/tabs.html
+++ b/InvenTree/part/templates/part/tabs.html
@@ -1,6 +1,15 @@
 <ul class="nav nav-tabs">
     <li{% ifequal tab 'detail' %} class="active"{% endifequal %}>
-        <a href="{% url 'part-detail' part.id %}">Details</a></li>
+        <a href="{% url 'part-detail' part.id %}">Details</a>
+    </li>
+    <li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
+        <a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.total_stock }}</span></a>
+    </li>
+    {% if part.allocation_count > 0 %}
+    <li{% ifequal tab 'allocation' %} class="active"{% endifequal %}>
+        <a href="{% url 'part-allocation' part.id %}">Allocated <span class="badge">{{ part.allocation_count }}</span></a>
+    </li>
+    {% endif %}
     {% if part.buildable %}
     <li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
         <a href="{% url 'part-bom' part.id %}">BOM<span class="badge">{{ part.bom_count }}</span></a></li>
@@ -11,12 +20,6 @@
     <li{% ifequal tab 'used' %} class="active"{% endifequal %}>
         <a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
     {% endif %}
-    <li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
-        <a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.total_stock }}</span></a></li>
-    {% if part.allocation_count > 0 %}
-    <li{% ifequal tab 'allocation' %} class="active"{% endifequal %}>
-        <a href="{% url 'part-allocation' part.id %}">Allocated <span class="badge">{{ part.allocation_count }}</span></a></li>
-    {% endif %}
     {% if part.purchaseable %}
     <li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
         <a href="{% url 'part-suppliers' part.id %}">Suppliers
@@ -31,4 +34,7 @@
         {% endif %}
     </a></li>
     {% endif %}
+    <li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
+        <a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachments.all|length > 0 %}<span class="badge">{{ part.attachments.all|length }}</span>{% endif %}</a>
+    </li>
 </ul>
\ No newline at end of file
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 96c830f556..401a038193 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -1,5 +1,11 @@
 """
-URL lookup for Part app
+URL lookup for Part app. Provides URL endpoints for:
+
+- Display / Create / Edit / Delete PartCategory
+- Display / Create / Edit / Delete Part
+- Create / Edit / Delete PartAttachment
+- Display / Create / Edit / Delete SupplierPart
+
 """
 
 from django.conf.urls import url, include
@@ -19,11 +25,18 @@ supplier_part_urls = [
     url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
 ]
 
+part_attachment_urls = [
+    url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
+    url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
+    url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'),
+]
+
 part_detail_urls = [
     url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
     url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
     url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
     url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
+    url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
     url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
     url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
     url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
@@ -69,6 +82,10 @@ part_urls = [
     # Part category
     url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),
 
+    # Part attachments
+    url(r'^attachment/', include(part_attachment_urls)),
+
+    # Bom Items
     url(r'^bom/(?P<pk>\d+)/', include(part_bom_urls)),
 
     # Top level part list (display top level parts and categories)
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 7ea8e5efbf..bdc3147e9b 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -13,11 +13,13 @@ from django.forms.models import model_to_dict
 from django.forms import HiddenInput
 
 from company.models import Company
-from .models import PartCategory, Part, BomItem
+from .models import PartCategory, Part, PartAttachment
+from .models import BomItem
 from .models import SupplierPart
 
 from .forms import PartImageForm
 from .forms import EditPartForm
+from .forms import EditPartAttachmentForm
 from .forms import EditCategoryForm
 from .forms import EditBomItemForm
 from .forms import BomExportForm
@@ -51,6 +53,81 @@ class PartIndex(ListView):
         return context
 
 
+class PartAttachmentCreate(AjaxCreateView):
+    """ View for creating a new PartAttachment object
+
+    - The view only makes sense if a Part object is passed to it
+    """
+    model = PartAttachment
+    form_class = EditPartAttachmentForm
+    ajax_form_title = "Add part attachment"
+    ajax_template_name = "modal_form.html"
+
+    def get_data(self):
+        return {
+            'success': 'Added attachment'
+        }
+
+    def get_initial(self):
+        """ Get initial data for new PartAttachment object.
+
+        - Client should have requested this form with a parent part in mind
+        - e.g. ?part=<pk>
+        """
+
+        initials = super(AjaxCreateView, self).get_initial()
+
+        # TODO - If the proper part was not sent, return an error message
+        initials['part'] = Part.objects.get(id=self.request.GET.get('part'))
+
+        return initials
+
+    def get_form(self):
+        """ Create a form to upload a new PartAttachment
+
+        - Hide the 'part' field
+        """
+
+        form = super(AjaxCreateView, self).get_form()
+
+        form.fields['part'].widget = HiddenInput()
+
+        return form
+
+
+class PartAttachmentEdit(AjaxUpdateView):
+    """ View for editing a PartAttachment object """
+    model = PartAttachment
+    form_class = EditPartAttachmentForm
+    ajax_template_name = 'modal_form.html'
+    ajax_form_title = 'Edit attachment'
+    
+    def get_data(self):
+        return {
+            'success': 'Part attachment updated'
+        }
+
+    def get_form(self):
+        form = super(AjaxUpdateView, self).get_form()
+
+        form.fields['part'].widget = HiddenInput()
+
+        return form
+
+
+class PartAttachmentDelete(AjaxDeleteView):
+    """ View for deleting a PartAttachment """
+
+    model = PartAttachment
+    ajax_template_name = "part/attachment_delete.html"
+    context_object_name = "attachment"
+
+    def get_data(self):
+        return {
+            'danger': 'Deleted part attachment'
+        }
+
+
 class PartCreate(AjaxCreateView):
     """ View for creating a new Part object.
 
@@ -102,7 +179,6 @@ class PartCreate(AjaxCreateView):
 
         return form
 
-    # Pre-fill the category field if a valid category is provided
     def get_initial(self):
         """ Get initial data for the new Part object:
 
diff --git a/setup.cfg b/setup.cfg
index f6f50b0c03..697f6d1f02 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,5 +4,5 @@ ignore =
 	W293,
 	# - E501 - line too long (82 characters)
 	E501
-exclude = .git,__pycache__
+exclude = .git,__pycache__,*/migrations/*
 max-complexity = 20