From 858a7fe089e3baff986614985f230ba740dc2a73 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 21:44:01 +1000
Subject: [PATCH 01/12] Tweaking URL patterns

---
 InvenTree/InvenTree/urls.py    | 14 +++++++-------
 InvenTree/part/urls.py         | 26 +++++++++++++++-----------
 InvenTree/project/urls.py      | 28 +++++++++++-----------------
 InvenTree/stock/serializers.py | 13 +------------
 InvenTree/stock/urls.py        | 13 ++++++++-----
 InvenTree/stock/views.py       |  6 +++---
 InvenTree/track/urls.py        | 12 +++++++-----
 7 files changed, 52 insertions(+), 60 deletions(-)

diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 5ab4076fad..d750b6388c 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -17,13 +17,13 @@ def Inventree404(self):
 
 
 urlpatterns = [
-    url(r'^stock/?', include('stock.urls')),
-    url(r'^part/?', include('part.urls')),
-    url(r'^supplier/?', include('supplier.urls')),
-    url(r'^track/?', include('track.urls')),
-    url(r'^project/?', include('project.urls')),
-    url(r'^admin/?', admin.site.urls),
-    url(r'^auth/?', include('rest_framework.urls', namespace='rest_framework')),
+    url(r'^stock/', include('stock.urls')),
+    url(r'^part/', include('part.urls')),
+    url(r'^supplier/', include('supplier.urls')),
+    url(r'^track/', include('track.urls')),
+    url(r'^project/', include('project.urls')),
+    url(r'^admin/', admin.site.urls),
+    url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
 
     # Any other URL
     url(r'', Inventree404)
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index bb7595a271..e2481fab36 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -10,26 +10,29 @@ from . import views
 categorypatterns = [
 
     # Part category detail
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartCategoryDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartCategoryDetail.as_view(), name='part-category-detail'),
 
     # List of top-level categories
-    url(r'^\?*[^/]*/?$', views.PartCategoryList.as_view())
+    url(r'^\?*.*/?$', views.PartCategoryList.as_view(), name='part-category-list'),
+    url(r'^$', views.PartCategoryList.as_view(), name='part-category-list')
 ]
 
 partparampatterns = [
     # Detail of a single part parameter
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartParamDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartParamDetail.as_view(), name='part-parameter-detail'),
 
     # Parameters associated with a particular part
-    url(r'^\?*[^/]*/?$', views.PartParamList.as_view()),
+    url(r'^\?.*/?$', views.PartParamList.as_view(), name='part-parameter-list'),
+    url(r'^$', views.PartParamList.as_view(), name='part-parameter-list'),
 ]
 
 parttemplatepatterns = [
     # Detail of a single part field template
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartTemplateDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartTemplateDetail.as_view(), name='part-template-detail'),
 
     # List all part field templates
-    url(r'^$', views.PartTemplateList.as_view())
+    url(r'^\?.*/?$', views.PartTemplateList.as_view(), name='part-template-list'),
+    url(r'^$', views.PartTemplateList.as_view(), name='part-template-list')
 ]
 
 """ Top-level URL patterns for the Part app:
@@ -40,17 +43,18 @@ parttemplatepatterns = [
 """
 urlpatterns = [
     # Individual part
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartDetail.as_view(), name='part-detail'),
 
     # Part categories
-    url(r'^category/?', include(categorypatterns)),
+    url(r'^category/', include(categorypatterns)),
 
     # Part parameters
-    url(r'^parameters/?', include(partparampatterns)),
+    url(r'^parameters/', include(partparampatterns)),
 
     # Part templates
-    url(r'^templates/?', include(parttemplatepatterns)),
+    url(r'^templates/', include(parttemplatepatterns)),
 
     # List parts with optional filters
-    url(r'^\?*[^/]*/?$', views.PartList.as_view()),
+    url(r'^\?.*/?$', views.PartList.as_view(), name='part-list'),
+    url(r'^$', views.PartList.as_view(), name='part-list'),
 ]
diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py
index d0f50bac56..ffd14bc6e3 100644
--- a/InvenTree/project/urls.py
+++ b/InvenTree/project/urls.py
@@ -2,41 +2,35 @@ from django.conf.urls import url, include
 
 from . import views
 
-""" URL patterns associated with project
-/project/<pk>       -> Detail view of single project
-/project/<pk>/parts -> Detail all parts associated with project
-"""
-projectdetailpatterns = [
-    # Single project detail
-    url(r'^$', views.ProjectDetail.as_view()),
-]
-
 projectpartpatterns = [
     # Detail of a single project part
-    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectPartDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectPartDetail.as_view(), name='project-part-detail'),
 
     # List project parts, with optional filters
-    url(r'^\?*[^/]*/?$', views.ProjectPartsList.as_view()),
+    url(r'^\?.*/?$', views.ProjectPartsList.as_view(), name='project-part-list'),
+    url(r'^$', views.ProjectPartsList.as_view(), name='project-part-list'),
 ]
 
 projectcategorypatterns = [
     # Detail of a single project category
-    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectCategoryDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectCategoryDetail.as_view(), name='project-category-detail'),
 
     # List of project categories, with filters
-    url(r'^\?*[^/]*/?$', views.ProjectCategoryList.as_view()),
+    url(r'^\?.*/?$', views.ProjectCategoryList.as_view(), name='project-category-list'),
+    url(r'^$', views.ProjectCategoryList.as_view(), name='project-category-list'),
 ]
 
 urlpatterns = [
     # Individual project URL
-    url(r'^(?P<pk>[0-9]+)/?', include(projectdetailpatterns)),
+    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectDetail.as_view(), name='project-detail'),
 
     # List of all projects
-    url(r'^$', views.ProjectList.as_view()),
+    url(r'^\?.*/?$', views.ProjectList.as_view(), name='project-list'),
+    url(r'^$', views.ProjectList.as_view(), name='project-list'),
 
     # Project parts
-    url(r'^parts/?', include(projectpartpatterns)),
+    url(r'^parts/', include(projectpartpatterns)),
 
     # Project categories
-    url(r'^category/?', include(projectcategorypatterns)),
+    url(r'^category/', include(projectcategorypatterns)),
 ]
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index a542edf150..6a02db0439 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -20,18 +20,7 @@ class StockItemSerializer(serializers.ModelSerializer):
                   'expected_arrival')
 
 
-class LocationBriefSerializer(serializers.ModelSerializer):
-    """ Brief information about a stock location
-    """
-
-    class Meta:
-        model = StockLocation
-        fields = ('pk',
-                  'name',
-                  'description')
-
-
-class LocationDetailSerializer(serializers.ModelSerializer):
+class LocationSerializer(serializers.ModelSerializer):
     """ Detailed information about a stock location
     """
 
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index b3496e5768..ad1e2ac28a 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -3,18 +3,21 @@ from django.conf.urls import url, include
 from . import views
 
 locpatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.LocationDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.LocationDetail.as_view(), name='location-detail'),
 
-    url(r'^\?*[^/]*/?$', views.LocationList.as_view())
+    url(r'^\?.*/?$', views.LocationList.as_view(), name='location-list'),
+
+    url(r'^$', views.LocationList.as_view(), name='location-list')
 ]
 
 urlpatterns = [
     # Stock location urls
-    url(r'^location/?', include(locpatterns)),
+    url(r'^location/', include(locpatterns)),
 
     # Detail for a single stock item
-    url(r'^(?P<pk>[0-9]+)$', views.StockDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.StockDetail.as_view(), name='stock-detail'),
 
     # List all stock items, with optional filters
-    url(r'^\?*[^/]*/?$', views.StockList.as_view()),
+    url(r'^\?.*/?$', views.StockList.as_view(), name='stock-list'),
+    url(r'^$', views.StockList.as_view(), name='stock-list'),
 ]
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 60f3a09c82..fa32ce234d 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -3,7 +3,7 @@ import django_filters
 
 from InvenTree.models import FilterChildren
 from .models import StockLocation, StockItem
-from .serializers import StockItemSerializer, LocationDetailSerializer
+from .serializers import StockItemSerializer, LocationSerializer
 
 
 class StockDetail(generics.RetrieveUpdateDestroyAPIView):
@@ -51,7 +51,7 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
     """
 
     queryset = StockLocation.objects.all()
-    serializer_class = LocationDetailSerializer
+    serializer_class = LocationSerializer
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
 
 
@@ -69,5 +69,5 @@ class LocationList(generics.ListCreateAPIView):
 
         return locations
 
-    serializer_class = LocationDetailSerializer
+    serializer_class = LocationSerializer
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
diff --git a/InvenTree/track/urls.py b/InvenTree/track/urls.py
index 447c1c3d38..35410a653a 100644
--- a/InvenTree/track/urls.py
+++ b/InvenTree/track/urls.py
@@ -3,17 +3,19 @@ from django.conf.urls import url, include
 from . import views
 
 infopatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartTrackingDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartTrackingDetail.as_view(), name='tracking-detail'),
 
-    url(r'^\?*[^/]*/?$', views.PartTrackingList.as_view())
+    url(r'^\?.*/?$', views.PartTrackingList.as_view(), name='tracking-list'),
+    url(r'^$', views.PartTrackingList.as_view(), name='tracking-list')
 ]
 
 urlpatterns = [
-    url(r'info/?', include(infopatterns)),
+    url(r'info/', include(infopatterns)),
 
     # Detail for a single unique part
-    url(r'^(?P<pk>[0-9]+)$', views.UniquePartDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.UniquePartDetail.as_view(), name='unique-detail'),
 
     # List all unique parts, with optional filters
-    url(r'^\?*[^/]*/?$', views.UniquePartList.as_view()),
+    url(r'^\?.*/?$', views.UniquePartList.as_view(), name='unique-list'),
+    url(r'^$', views.UniquePartList.as_view(), name='unique-list'),
 ]

From 2c628c8ab40111fc8fe6216a1dc6986d84dd5b02 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 21:59:52 +1000
Subject: [PATCH 02/12] Prepend /api/ to API URL

---
 InvenTree/InvenTree/urls.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index d750b6388c..7b94439c97 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -16,15 +16,21 @@ def Inventree404(self):
     return Response(content, status=status.HTTP_404_NOT_FOUND)
 
 
-urlpatterns = [
+apipatterns = [
     url(r'^stock/', include('stock.urls')),
     url(r'^part/', include('part.urls')),
     url(r'^supplier/', include('supplier.urls')),
     url(r'^track/', include('track.urls')),
     url(r'^project/', include('project.urls')),
-    url(r'^admin/', admin.site.urls),
-    url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
 
     # Any other URL
     url(r'', Inventree404)
 ]
+
+urlpatterns = [
+    # API URL
+    url(r'^api/', include(apipatterns)),
+
+    url(r'^admin/', admin.site.urls),
+    url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
+]

From 41eb427c02c2fc80da0c2be9bcc5844c6b3351af Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 22:05:29 +1000
Subject: [PATCH 03/12] Fix for FilterChildren function

---
 InvenTree/InvenTree/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index e8430694d0..be5de69be1 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -184,7 +184,7 @@ def FilterChildren(queryset, parent):
 
     if not parent:
         return queryset
-    elif isinstance(parent, str) and parent.lower() in ['none', 'false', 'null', 'top', '0']:
+    elif str(parent).lower() in ['none', 'false', 'null', 'top', '0']:
         return queryset.filter(parent=None)
     else:
         try:

From f85489bc0ec402dcfa0d061688937dabb982dd60 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 22:40:59 +1000
Subject: [PATCH 04/12] Supplier API is hyperlinked

---
 InvenTree/InvenTree/models.py     |  2 +-
 InvenTree/supplier/admin.py       |  2 +-
 InvenTree/supplier/serializers.py | 12 +++++++-----
 InvenTree/supplier/urls.py        | 15 +++++++++------
 4 files changed, 18 insertions(+), 13 deletions(-)

diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index be5de69be1..ca602f400c 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -12,7 +12,7 @@ class Company(models.Model):
         abstract = True
 
     name = models.CharField(max_length=100)
-    URL = models.URLField(blank=True)
+    website = models.URLField(blank=True)
     address = models.CharField(max_length=200,
                                blank=True)
     phone = models.CharField(max_length=50,
diff --git a/InvenTree/supplier/admin.py b/InvenTree/supplier/admin.py
index cf19edd2b0..4fe7914019 100644
--- a/InvenTree/supplier/admin.py
+++ b/InvenTree/supplier/admin.py
@@ -4,7 +4,7 @@ from .models import Supplier, SupplierPart, Customer, Manufacturer
 
 
 class CompanyAdmin(admin.ModelAdmin):
-    list_display = ('name', 'URL', 'contact')
+    list_display = ('name', 'website', 'contact')
 
 
 admin.site.register(Customer, CompanyAdmin)
diff --git a/InvenTree/supplier/serializers.py b/InvenTree/supplier/serializers.py
index 3635a93a72..9b80d33445 100644
--- a/InvenTree/supplier/serializers.py
+++ b/InvenTree/supplier/serializers.py
@@ -3,7 +3,7 @@ from rest_framework import serializers
 from .models import Supplier, SupplierPart, SupplierPriceBreak
 
 
-class SupplierSerializer(serializers.ModelSerializer):
+class SupplierSerializer(serializers.HyperlinkedModelSerializer):
 
     class Meta:
         model = Supplier
@@ -12,11 +12,13 @@ class SupplierSerializer(serializers.ModelSerializer):
 
 class SupplierPartSerializer(serializers.ModelSerializer):
 
-    price_breaks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
+    price_breaks = serializers.HyperlinkedRelatedField(many=True,
+                                                       read_only=True,
+                                                       view_name='price_break-detail')
 
     class Meta:
         model = SupplierPart
-        fields = ['pk',
+        fields = ['url',
                   'part',
                   'supplier',
                   'SKU',
@@ -32,11 +34,11 @@ class SupplierPartSerializer(serializers.ModelSerializer):
                   'lead_time']
 
 
-class SupplierPriceBreakSerializer(serializers.ModelSerializer):
+class SupplierPriceBreakSerializer(serializers.HyperlinkedModelSerializer):
 
     class Meta:
         model = SupplierPriceBreak
-        fields = ['pk',
+        fields = ['url',
                   'part',
                   'quantity',
                   'cost']
diff --git a/InvenTree/supplier/urls.py b/InvenTree/supplier/urls.py
index 10fbe8f230..28eb7c1699 100644
--- a/InvenTree/supplier/urls.py
+++ b/InvenTree/supplier/urls.py
@@ -3,15 +3,17 @@ from django.conf.urls import url, include
 from . import views
 
 partpatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPartDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPartDetail.as_view(), name='supplier-part-detail'),
 
-    url(r'^\?*[^/]*/?$', views.SupplierPartList.as_view())
+    url(r'^\?.*/?$', views.SupplierPartList.as_view()),
+    url(r'^$', views.SupplierPartList.as_view())
 ]
 
 pricepatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPriceBreakDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPriceBreakDetail.as_view(), name='price-break-detail'),
 
-    url(r'^\?*[^/]*/?$', views.SupplierPriceBreakList.as_view())
+    url(r'^\?.*/?$', views.SupplierPriceBreakList.as_view()),
+    url(r'^$', views.SupplierPriceBreakList.as_view())
 ]
 
 urlpatterns = [
@@ -23,8 +25,9 @@ urlpatterns = [
     url(r'price/?', include(pricepatterns)),
 
     # Display details of a supplier
-    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierDetail.as_view()),
+    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierDetail.as_view(), name='supplier-detail'),
 
     # List suppliers
-    url(r'^\?*[^/]*/?$', views.SupplierList.as_view())
+    url(r'^\?.*/?$', views.SupplierList.as_view()),
+    url(r'^$', views.SupplierList.as_view())
 ]

From e641a913f25a5f8d35c0b38b5a3040a0a5dcdc6b Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 23:29:58 +1000
Subject: [PATCH 05/12] Supplier API now hyperlinked

---
 InvenTree/InvenTree/urls.py             | 14 ++---------
 InvenTree/supplier/customer_urls.py     | 12 ++++++++++
 InvenTree/supplier/manufacturer_urls.py | 12 ++++++++++
 InvenTree/supplier/serializers.py       | 30 +++++++++++++++++++++++-
 InvenTree/supplier/urls.py              | 10 ++++----
 InvenTree/supplier/views.py             | 31 +++++++++++++++++++++++++
 6 files changed, 91 insertions(+), 18 deletions(-)
 create mode 100644 InvenTree/supplier/customer_urls.py
 create mode 100644 InvenTree/supplier/manufacturer_urls.py

diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 7b94439c97..2b5528cf5e 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -7,24 +7,14 @@ from rest_framework.decorators import api_view
 
 admin.site.site_header = "InvenTree Admin"
 
-
-@api_view()
-def Inventree404(self):
-    """ Supplied URL is invalid
-    """
-    content = {'detail': 'Malformed API URL'}
-    return Response(content, status=status.HTTP_404_NOT_FOUND)
-
-
 apipatterns = [
     url(r'^stock/', include('stock.urls')),
     url(r'^part/', include('part.urls')),
     url(r'^supplier/', include('supplier.urls')),
+    url(r'^manufacturer/', include('supplier.manufacturer_urls')),
+    url(r'^customer/', include('supplier.customer_urls')),
     url(r'^track/', include('track.urls')),
     url(r'^project/', include('project.urls')),
-
-    # Any other URL
-    url(r'', Inventree404)
 ]
 
 urlpatterns = [
diff --git a/InvenTree/supplier/customer_urls.py b/InvenTree/supplier/customer_urls.py
new file mode 100644
index 0000000000..870bba1a31
--- /dev/null
+++ b/InvenTree/supplier/customer_urls.py
@@ -0,0 +1,12 @@
+from django.conf.urls import url, include
+
+from . import views
+
+urlpatterns = [
+    # Customer detail
+    url(r'^(?P<pk>[0-9]+)/?$', views.CustomerDetail.as_view(), name='customer-detail'),
+
+    # List customers
+    url(r'^\?.*/?$', views.CustomerList.as_view()),
+    url(r'^$', views.CustomerList.as_view())
+]
diff --git a/InvenTree/supplier/manufacturer_urls.py b/InvenTree/supplier/manufacturer_urls.py
new file mode 100644
index 0000000000..56a74c8106
--- /dev/null
+++ b/InvenTree/supplier/manufacturer_urls.py
@@ -0,0 +1,12 @@
+from django.conf.urls import url, include
+
+from . import views
+
+urlpatterns = [
+    # Manufacturer detail
+    url(r'^(?P<pk>[0-9]+)/?$', views.ManufacturerDetail.as_view(), name='manufacturer-detail'),
+
+    # List manufacturers
+    url(r'^\?.*/?$', views.ManufacturerList.as_view()),
+    url(r'^$', views.ManufacturerList.as_view())
+]
diff --git a/InvenTree/supplier/serializers.py b/InvenTree/supplier/serializers.py
index 9b80d33445..517ea940d2 100644
--- a/InvenTree/supplier/serializers.py
+++ b/InvenTree/supplier/serializers.py
@@ -1,6 +1,10 @@
 from rest_framework import serializers
 
+from part.models import Part
+
 from .models import Supplier, SupplierPart, SupplierPriceBreak
+from .models import Manufacturer
+from .models import Customer
 
 
 class SupplierSerializer(serializers.HyperlinkedModelSerializer):
@@ -10,14 +14,38 @@ class SupplierSerializer(serializers.HyperlinkedModelSerializer):
         fields = '__all__'
 
 
+class ManufacturerSerializer(serializers.HyperlinkedModelSerializer):
+
+    class Meta:
+        model = Manufacturer
+        fields = '__all__'
+
+
+class CustomerSerializer(serializers.HyperlinkedModelSerializer):
+
+    class Meta:
+        model = Customer
+        fields = '__all__'
+
+
 class SupplierPartSerializer(serializers.ModelSerializer):
 
     price_breaks = serializers.HyperlinkedRelatedField(many=True,
                                                        read_only=True,
-                                                       view_name='price_break-detail')
+                                                       view_name='supplierpricebreak-detail')
+
+    part = serializers.HyperlinkedRelatedField(view_name='part-detail',
+                                               queryset = Part.objects.all())
+
+    supplier = serializers.HyperlinkedRelatedField(view_name='supplier-detail',
+                                                   queryset = Supplier.objects.all())
+
+    manufacturer = serializers.HyperlinkedRelatedField(view_name='manufacturer-detail',
+                                                       queryset = Manufacturer.objects.all())
 
     class Meta:
         model = SupplierPart
+        #extra_kwargs = {'url': {'view_name': 'supplier-part-detail'}}
         fields = ['url',
                   'part',
                   'supplier',
diff --git a/InvenTree/supplier/urls.py b/InvenTree/supplier/urls.py
index 28eb7c1699..5efac75d31 100644
--- a/InvenTree/supplier/urls.py
+++ b/InvenTree/supplier/urls.py
@@ -3,14 +3,14 @@ from django.conf.urls import url, include
 from . import views
 
 partpatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPartDetail.as_view(), name='supplier-part-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPartDetail.as_view(), name='supplierpart-detail'),
 
     url(r'^\?.*/?$', views.SupplierPartList.as_view()),
     url(r'^$', views.SupplierPartList.as_view())
 ]
 
 pricepatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPriceBreakDetail.as_view(), name='price-break-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPriceBreakDetail.as_view(), name='supplierpricebreak-detail'),
 
     url(r'^\?.*/?$', views.SupplierPriceBreakList.as_view()),
     url(r'^$', views.SupplierPriceBreakList.as_view())
@@ -19,13 +19,13 @@ pricepatterns = [
 urlpatterns = [
 
     # Supplier part information
-    url(r'part/?', include(partpatterns)),
+    url(r'part/', include(partpatterns)),
 
     # Supplier price information
-    url(r'price/?', include(pricepatterns)),
+    url(r'price/', include(pricepatterns)),
 
     # Display details of a supplier
-    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierDetail.as_view(), name='supplier-detail'),
+    url(r'^(?P<pk>[0-9]+)/$', views.SupplierDetail.as_view(), name='supplier-detail'),
 
     # List suppliers
     url(r'^\?.*/?$', views.SupplierList.as_view()),
diff --git a/InvenTree/supplier/views.py b/InvenTree/supplier/views.py
index e6fa2e628b..a12a7f5124 100644
--- a/InvenTree/supplier/views.py
+++ b/InvenTree/supplier/views.py
@@ -1,9 +1,40 @@
 from rest_framework import generics, permissions
 
 from .models import Supplier, SupplierPart, SupplierPriceBreak
+from .models import Manufacturer, Customer
 from .serializers import SupplierSerializer
 from .serializers import SupplierPartSerializer
 from .serializers import SupplierPriceBreakSerializer
+from .serializers import ManufacturerSerializer
+from .serializers import CustomerSerializer
+
+
+class ManufacturerDetail(generics.RetrieveUpdateDestroyAPIView):
+
+    queryset = Manufacturer.objects.all()
+    serializer_class = ManufacturerSerializer
+    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
+
+
+class ManufacturerList(generics.ListCreateAPIView):
+
+    queryset = Manufacturer.objects.all()
+    serializer_class = ManufacturerSerializer
+    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
+
+
+class CustomerDetail(generics.RetrieveUpdateDestroyAPIView):
+
+    queryset = Customer.objects.all()
+    serializer_class = CustomerSerializer
+    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
+
+
+class CustomerList(generics.ListCreateAPIView):
+
+    queryset = Customer.objects.all()
+    serializer_class = CustomerSerializer
+    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
 
 
 class SupplierDetail(generics.RetrieveUpdateDestroyAPIView):

From 9e3fa00fdd4f66f7c2d96b5662236271944a90a4 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 23:30:08 +1000
Subject: [PATCH 06/12] Fixed makefile

Added "makemigrations" for each app
---
 Makefile | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index b9047d0085..4e809701e8 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,11 @@ test:
 	python InvenTree/manage.py test --noinput
 
 migrate:
-	python InvenTree/manage.py makemigrations
+	python InvenTree/manage.py makemigrations part
+	python InvenTree/manage.py makemigrations project
+	python InvenTree/manage.py makemigrations stock
+	python InvenTree/manage.py makemigrations supplier
+	python InvenTree/manage.py makemigrations track
 	python InvenTree/manage.py migrate --run-syncdb
 	python InvenTree/manage.py check
 

From 7c5261bc4a6c4caf6f00b78615ef32448f9885f2 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 23:44:24 +1000
Subject: [PATCH 07/12] Stock API now hyperlinkd

---
 InvenTree/InvenTree/models.py  |  1 +
 InvenTree/stock/serializers.py |  8 ++++----
 InvenTree/stock/urls.py        | 12 ++++++------
 InvenTree/supplier/models.py   |  3 +++
 4 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index ca602f400c..c6aec418ad 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -35,6 +35,7 @@ class InvenTreeTree(models.Model):
 
     class Meta:
         abstract = True
+        unique_together = ('name', 'parent')
 
     name = models.CharField(max_length=100)
     description = models.CharField(max_length=250, blank=True)
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 6a02db0439..5c69329f82 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -3,13 +3,13 @@ from rest_framework import serializers
 from .models import StockItem, StockLocation
 
 
-class StockItemSerializer(serializers.ModelSerializer):
+class StockItemSerializer(serializers.HyperlinkedModelSerializer):
     """ Serializer for a StockItem
     """
 
     class Meta:
         model = StockItem
-        fields = ('pk',
+        fields = ('url',
                   'part',
                   'location',
                   'quantity',
@@ -20,13 +20,13 @@ class StockItemSerializer(serializers.ModelSerializer):
                   'expected_arrival')
 
 
-class LocationSerializer(serializers.ModelSerializer):
+class LocationSerializer(serializers.HyperlinkedModelSerializer):
     """ Detailed information about a stock location
     """
 
     class Meta:
         model = StockLocation
-        fields = ('pk',
+        fields = ('url',
                   'name',
                   'description',
                   'parent',
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index ad1e2ac28a..bb026d2a08 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -3,11 +3,11 @@ from django.conf.urls import url, include
 from . import views
 
 locpatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.LocationDetail.as_view(), name='location-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.LocationDetail.as_view(), name='stocklocation-detail'),
 
-    url(r'^\?.*/?$', views.LocationList.as_view(), name='location-list'),
+    url(r'^\?.*/?$', views.LocationList.as_view()),
 
-    url(r'^$', views.LocationList.as_view(), name='location-list')
+    url(r'^$', views.LocationList.as_view())
 ]
 
 urlpatterns = [
@@ -15,9 +15,9 @@ urlpatterns = [
     url(r'^location/', include(locpatterns)),
 
     # Detail for a single stock item
-    url(r'^(?P<pk>[0-9]+)/?$', views.StockDetail.as_view(), name='stock-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.StockDetail.as_view(), name='stockitem-detail'),
 
     # List all stock items, with optional filters
-    url(r'^\?.*/?$', views.StockList.as_view(), name='stock-list'),
-    url(r'^$', views.StockList.as_view(), name='stock-list'),
+    url(r'^\?.*/?$', views.StockList.as_view()),
+    url(r'^$', views.StockList.as_view()),
 ]
diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py
index bd5d20dc86..894621ea97 100644
--- a/InvenTree/supplier/models.py
+++ b/InvenTree/supplier/models.py
@@ -31,6 +31,9 @@ class SupplierPart(models.Model):
     - A Part may be available from multiple suppliers
     """
 
+    class Meta:
+        unique_together = ('part', 'supplier', 'SKU')
+
     part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE)
     supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
     SKU = models.CharField(max_length=100)

From a1db0c90e4e4347716b187b39176863055bf5142 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Fri, 14 Apr 2017 23:53:27 +1000
Subject: [PATCH 08/12] Tracking API is hyperlinked

---
 InvenTree/supplier/serializers.py |  1 -
 InvenTree/track/models.py         | 17 +++++------------
 InvenTree/track/serializers.py    |  8 ++++----
 InvenTree/track/urls.py           | 12 ++++++------
 4 files changed, 15 insertions(+), 23 deletions(-)

diff --git a/InvenTree/supplier/serializers.py b/InvenTree/supplier/serializers.py
index 517ea940d2..88c23a573c 100644
--- a/InvenTree/supplier/serializers.py
+++ b/InvenTree/supplier/serializers.py
@@ -45,7 +45,6 @@ class SupplierPartSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = SupplierPart
-        #extra_kwargs = {'url': {'view_name': 'supplier-part-detail'}}
         fields = ['url',
                   'part',
                   'supplier',
diff --git a/InvenTree/track/models.py b/InvenTree/track/models.py
index d50b6d7578..d0e8a5aa46 100644
--- a/InvenTree/track/models.py
+++ b/InvenTree/track/models.py
@@ -37,6 +37,10 @@ class UniquePart(models.Model):
     and tracking all events in the life of a part
     """
 
+    class Meta:
+        # Cannot have multiple parts with same serial number
+        unique_together = ('part', 'serial')
+
     objects = UniquePartManager()
 
     part = models.ForeignKey(Part, on_delete=models.CASCADE)
@@ -50,7 +54,7 @@ class UniquePart(models.Model):
                                      editable=False)
     serial = models.IntegerField()
 
-    createdBy = models.ForeignKey(User)
+    # createdBy = models.ForeignKey(User)
 
     customer = models.ForeignKey(Customer, blank=True, null=True)
 
@@ -76,17 +80,6 @@ class UniquePart(models.Model):
     def __str__(self):
         return self.part.name
 
-    def save(self, *args, **kwargs):
-
-        # Disallow saving a serial number that already exists
-        matches = UniquePart.objects.filter(serial=self.serial, part=self.part)
-        matches = matches.filter(~models.Q(id=self.id))
-
-        if len(matches) > 0:
-            raise ValidationError(_("Matching serial number already exists"))
-
-        super(UniquePart, self).save(*args, **kwargs)
-
 
 class PartTrackingInfo(models.Model):
     """ Single data-point in the life of a UniquePart
diff --git a/InvenTree/track/serializers.py b/InvenTree/track/serializers.py
index 6e958acbf3..3bd84aa0e7 100644
--- a/InvenTree/track/serializers.py
+++ b/InvenTree/track/serializers.py
@@ -3,24 +3,24 @@ from rest_framework import serializers
 from .models import UniquePart, PartTrackingInfo
 
 
-class UniquePartSerializer(serializers.ModelSerializer):
+class UniquePartSerializer(serializers.HyperlinkedModelSerializer):
 
     tracking_info = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
 
     class Meta:
         model = UniquePart
-        fields = ['pk',
+        fields = ['url',
                   'part',
                   'revision',
                   'creation_date',
                   'serial',
-                  'createdBy',
+                  # 'createdBy',
                   'customer',
                   'status',
                   'tracking_info']
 
 
-class PartTrackingInfoSerializer(serializers.ModelSerializer):
+class PartTrackingInfoSerializer(serializers.HyperlinkedModelSerializer):
 
     class Meta:
         model = PartTrackingInfo
diff --git a/InvenTree/track/urls.py b/InvenTree/track/urls.py
index 35410a653a..aaf3b539ce 100644
--- a/InvenTree/track/urls.py
+++ b/InvenTree/track/urls.py
@@ -3,19 +3,19 @@ from django.conf.urls import url, include
 from . import views
 
 infopatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartTrackingDetail.as_view(), name='tracking-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartTrackingDetail.as_view(), name='parttrackinginfo-detail'),
 
-    url(r'^\?.*/?$', views.PartTrackingList.as_view(), name='tracking-list'),
-    url(r'^$', views.PartTrackingList.as_view(), name='tracking-list')
+    url(r'^\?.*/?$', views.PartTrackingList.as_view()),
+    url(r'^$', views.PartTrackingList.as_view())
 ]
 
 urlpatterns = [
     url(r'info/', include(infopatterns)),
 
     # Detail for a single unique part
-    url(r'^(?P<pk>[0-9]+)/?$', views.UniquePartDetail.as_view(), name='unique-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.UniquePartDetail.as_view(), name='uniquepart-detail'),
 
     # List all unique parts, with optional filters
-    url(r'^\?.*/?$', views.UniquePartList.as_view(), name='unique-list'),
-    url(r'^$', views.UniquePartList.as_view(), name='unique-list'),
+    url(r'^\?.*/?$', views.UniquePartList.as_view()),
+    url(r'^$', views.UniquePartList.as_view()),
 ]

From 9e287d85d84aa1dee501a42613d955d68c80fc24 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 15 Apr 2017 00:00:55 +1000
Subject: [PATCH 09/12] Project API is hyperlinked

---
 InvenTree/project/serializers.py | 22 ++++++++++++----------
 InvenTree/project/urls.py        | 16 ++++++++--------
 2 files changed, 20 insertions(+), 18 deletions(-)

diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py
index 5e3904cb3e..91bc79e619 100644
--- a/InvenTree/project/serializers.py
+++ b/InvenTree/project/serializers.py
@@ -3,38 +3,40 @@ from rest_framework import serializers
 from .models import ProjectCategory, Project, ProjectPart
 
 
-class ProjectPartSerializer(serializers.ModelSerializer):
+class ProjectPartSerializer(serializers.HyperlinkedModelSerializer):
 
     class Meta:
         model = ProjectPart
-        fields = ('pk',
+        fields = ('url',
                   'part',
                   'project',
                   'quantity',
                   'output')
 
 
-class ProjectSerializer(serializers.ModelSerializer):
-    """ Serializer for displaying brief overview of a project
-    """
+class ProjectSerializer(serializers.HyperlinkedModelSerializer):
 
     class Meta:
         model = Project
-        fields = ('pk',
+        fields = ('url',
                   'name',
                   'description',
                   'category')
 
 
-class ProjectCategorySerializer(serializers.ModelSerializer):
+class ProjectCategorySerializer(serializers.HyperlinkedModelSerializer):
 
-    children = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
+    children = serializers.HyperlinkedRelatedField(many=True,
+                                                   read_only=True,
+                                                   view_name='projectcategory-detail')
 
-    projects = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
+    projects = serializers.HyperlinkedRelatedField(many=True,
+                                                   read_only=True,
+                                                   view_name='project-detail')
 
     class Meta:
         model = ProjectCategory
-        fields = ('pk',
+        fields = ('url',
                   'name',
                   'description',
                   'parent',
diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py
index ffd14bc6e3..a0f0bc29b6 100644
--- a/InvenTree/project/urls.py
+++ b/InvenTree/project/urls.py
@@ -4,20 +4,20 @@ from . import views
 
 projectpartpatterns = [
     # Detail of a single project part
-    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectPartDetail.as_view(), name='project-part-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectPartDetail.as_view(), name='projectpart-detail'),
 
     # List project parts, with optional filters
-    url(r'^\?.*/?$', views.ProjectPartsList.as_view(), name='project-part-list'),
-    url(r'^$', views.ProjectPartsList.as_view(), name='project-part-list'),
+    url(r'^\?.*/?$', views.ProjectPartsList.as_view()),
+    url(r'^$', views.ProjectPartsList.as_view()),
 ]
 
 projectcategorypatterns = [
     # Detail of a single project category
-    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectCategoryDetail.as_view(), name='project-category-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.ProjectCategoryDetail.as_view(), name='projectcategory-detail'),
 
     # List of project categories, with filters
-    url(r'^\?.*/?$', views.ProjectCategoryList.as_view(), name='project-category-list'),
-    url(r'^$', views.ProjectCategoryList.as_view(), name='project-category-list'),
+    url(r'^\?.*/?$', views.ProjectCategoryList.as_view()),
+    url(r'^$', views.ProjectCategoryList.as_view()),
 ]
 
 urlpatterns = [
@@ -25,8 +25,8 @@ urlpatterns = [
     url(r'^(?P<pk>[0-9]+)/?$', views.ProjectDetail.as_view(), name='project-detail'),
 
     # List of all projects
-    url(r'^\?.*/?$', views.ProjectList.as_view(), name='project-list'),
-    url(r'^$', views.ProjectList.as_view(), name='project-list'),
+    url(r'^\?.*/?$', views.ProjectList.as_view()),
+    url(r'^$', views.ProjectList.as_view()),
 
     # Project parts
     url(r'^parts/', include(projectpartpatterns)),

From f7107008f08aa5ccceac32f9929103a64bae08b6 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 15 Apr 2017 00:10:35 +1000
Subject: [PATCH 10/12] Part API now hyperlinked

---
 InvenTree/part/serializers.py    | 16 +++++-----------
 InvenTree/part/urls.py           | 32 ++++++++++++++++----------------
 InvenTree/project/serializers.py | 12 +-----------
 3 files changed, 22 insertions(+), 38 deletions(-)

diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 47bbaedf40..bc38ca309d 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -17,14 +17,14 @@ class PartParameterSerializer(serializers.ModelSerializer):
                   'units')
 
 
-class PartSerializer(serializers.ModelSerializer):
+class PartSerializer(serializers.HyperlinkedModelSerializer):
     """ Serializer for complete detail information of a part.
     Used when displaying all details of a single component.
     """
 
     class Meta:
         model = Part
-        fields = ('pk',
+        fields = ('url',
                   'name',
                   'IPN',
                   'description',
@@ -32,21 +32,15 @@ class PartSerializer(serializers.ModelSerializer):
                   'stock')
 
 
-class PartCategorySerializer(serializers.ModelSerializer):
-
-    children = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
-
-    parts = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
+class PartCategorySerializer(serializers.HyperlinkedModelSerializer):
 
     class Meta:
         model = PartCategory
-        fields = ('pk',
+        fields = ('url',
                   'name',
                   'description',
                   'parent',
-                  'path',
-                  'children',
-                  'parts')
+                  'path')
 
 
 class PartTemplateSerializer(serializers.ModelSerializer):
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index e2481fab36..72fffc5b32 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -10,29 +10,29 @@ from . import views
 categorypatterns = [
 
     # Part category detail
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartCategoryDetail.as_view(), name='part-category-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartCategoryDetail.as_view(), name='partcategory-detail'),
 
     # List of top-level categories
-    url(r'^\?*.*/?$', views.PartCategoryList.as_view(), name='part-category-list'),
-    url(r'^$', views.PartCategoryList.as_view(), name='part-category-list')
+    url(r'^\?*.*/?$', views.PartCategoryList.as_view()),
+    url(r'^$', views.PartCategoryList.as_view())
 ]
 
 partparampatterns = [
     # Detail of a single part parameter
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartParamDetail.as_view(), name='part-parameter-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartParamDetail.as_view(), name='partparameter-detail'),
 
     # Parameters associated with a particular part
-    url(r'^\?.*/?$', views.PartParamList.as_view(), name='part-parameter-list'),
-    url(r'^$', views.PartParamList.as_view(), name='part-parameter-list'),
+    url(r'^\?.*/?$', views.PartParamList.as_view()),
+    url(r'^$', views.PartParamList.as_view()),
 ]
 
 parttemplatepatterns = [
     # Detail of a single part field template
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartTemplateDetail.as_view(), name='part-template-detail'),
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartTemplateDetail.as_view(), name='partparametertemplate-detail'),
 
     # List all part field templates
-    url(r'^\?.*/?$', views.PartTemplateList.as_view(), name='part-template-list'),
-    url(r'^$', views.PartTemplateList.as_view(), name='part-template-list')
+    url(r'^\?.*/?$', views.PartTemplateList.as_view()),
+    url(r'^$', views.PartTemplateList.as_view())
 ]
 
 """ Top-level URL patterns for the Part app:
@@ -42,19 +42,19 @@ parttemplatepatterns = [
 /part/category  -> (refer to categorypatterns)
 """
 urlpatterns = [
-    # Individual part
-    url(r'^(?P<pk>[0-9]+)/?$', views.PartDetail.as_view(), name='part-detail'),
-
     # Part categories
     url(r'^category/', include(categorypatterns)),
 
     # Part parameters
-    url(r'^parameters/', include(partparampatterns)),
+    url(r'^parameter/', include(partparampatterns)),
 
     # Part templates
-    url(r'^templates/', include(parttemplatepatterns)),
+    url(r'^template/', include(parttemplatepatterns)),
+
+    # Individual part
+    url(r'^(?P<pk>[0-9]+)/?$', views.PartDetail.as_view(), name='part-detail'),
 
     # List parts with optional filters
-    url(r'^\?.*/?$', views.PartList.as_view(), name='part-list'),
-    url(r'^$', views.PartList.as_view(), name='part-list'),
+    url(r'^\?.*/?$', views.PartList.as_view()),
+    url(r'^$', views.PartList.as_view()),
 ]
diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py
index 91bc79e619..1b854da4f1 100644
--- a/InvenTree/project/serializers.py
+++ b/InvenTree/project/serializers.py
@@ -26,20 +26,10 @@ class ProjectSerializer(serializers.HyperlinkedModelSerializer):
 
 class ProjectCategorySerializer(serializers.HyperlinkedModelSerializer):
 
-    children = serializers.HyperlinkedRelatedField(many=True,
-                                                   read_only=True,
-                                                   view_name='projectcategory-detail')
-
-    projects = serializers.HyperlinkedRelatedField(many=True,
-                                                   read_only=True,
-                                                   view_name='project-detail')
-
     class Meta:
         model = ProjectCategory
         fields = ('url',
                   'name',
                   'description',
                   'parent',
-                  'path',
-                  'children',
-                  'projects')
+                  'path')

From bf1c7125a472187f3e5af88c163afeebc4607bd6 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 15 Apr 2017 00:15:38 +1000
Subject: [PATCH 11/12] Added API documentation

---
 InvenTree/InvenTree/urls.py | 3 +++
 requirements/base.txt       | 2 ++
 2 files changed, 5 insertions(+)

diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 2b5528cf5e..46ab1311d0 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -4,6 +4,7 @@ from django.contrib import admin
 from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.decorators import api_view
+from rest_framework.documentation import include_docs_urls
 
 admin.site.site_header = "InvenTree Admin"
 
@@ -21,6 +22,8 @@ urlpatterns = [
     # API URL
     url(r'^api/', include(apipatterns)),
 
+    url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
+
     url(r'^admin/', admin.site.urls),
     url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
 ]
diff --git a/requirements/base.txt b/requirements/base.txt
index 0c17b62bfe..d80fe55e91 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,3 +1,5 @@
 Django==1.11
 djangorestframework==3.6.2
 django_filter==1.0.2
+coreapi==2.3.0
+pygments==2.2.0

From 7d7579d3d62df45a8c824bba9d2cefb4985d957c Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Sat, 15 Apr 2017 00:55:30 +1000
Subject: [PATCH 12/12] Style fixes

---
 InvenTree/InvenTree/urls.py             |  6 +--
 InvenTree/stock/location_urls.py        | 10 +++++
 InvenTree/stock/urls.py                 | 13 +-----
 InvenTree/stock/views.py                | 55 ++++++++++-------------
 InvenTree/supplier/customer_urls.py     |  2 +-
 InvenTree/supplier/manufacturer_urls.py |  2 +-
 InvenTree/supplier/part_urls.py         | 10 +++++
 InvenTree/supplier/price_urls.py        | 10 +++++
 InvenTree/supplier/serializers.py       |  6 +--
 InvenTree/supplier/urls.py              | 15 +------
 InvenTree/supplier/views.py             | 59 ++++++++++++++-----------
 InvenTree/track/models.py               |  2 +-
 12 files changed, 97 insertions(+), 93 deletions(-)
 create mode 100644 InvenTree/stock/location_urls.py
 create mode 100644 InvenTree/supplier/part_urls.py
 create mode 100644 InvenTree/supplier/price_urls.py

diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 46ab1311d0..63014a36d1 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -1,17 +1,17 @@
 from django.conf.urls import url, include
 from django.contrib import admin
 
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework.decorators import api_view
 from rest_framework.documentation import include_docs_urls
 
 admin.site.site_header = "InvenTree Admin"
 
 apipatterns = [
     url(r'^stock/', include('stock.urls')),
+    url(r'^stock-location/', include('stock.location_urls')),
     url(r'^part/', include('part.urls')),
     url(r'^supplier/', include('supplier.urls')),
+    url(r'^supplier-part/', include('supplier.part_urls')),
+    url(r'^price-break/', include('supplier.price_urls')),
     url(r'^manufacturer/', include('supplier.manufacturer_urls')),
     url(r'^customer/', include('supplier.customer_urls')),
     url(r'^track/', include('track.urls')),
diff --git a/InvenTree/stock/location_urls.py b/InvenTree/stock/location_urls.py
new file mode 100644
index 0000000000..9df7a53b28
--- /dev/null
+++ b/InvenTree/stock/location_urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url
+from . import views
+
+urlpatterns = [
+    url(r'^(?P<pk>[0-9]+)/?$', views.LocationDetail.as_view(), name='stocklocation-detail'),
+
+    url(r'^\?.*/?$', views.LocationList.as_view()),
+
+    url(r'^$', views.LocationList.as_view())
+]
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index bb026d2a08..b2822682d5 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -1,19 +1,8 @@
-from django.conf.urls import url, include
+from django.conf.urls import url
 
 from . import views
 
-locpatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.LocationDetail.as_view(), name='stocklocation-detail'),
-
-    url(r'^\?.*/?$', views.LocationList.as_view()),
-
-    url(r'^$', views.LocationList.as_view())
-]
-
 urlpatterns = [
-    # Stock location urls
-    url(r'^location/', include(locpatterns)),
-
     # Detail for a single stock item
     url(r'^(?P<pk>[0-9]+)/?$', views.StockDetail.as_view(), name='stockitem-detail'),
 
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index fa32ce234d..27fa65bfbe 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -1,7 +1,10 @@
-from rest_framework import generics, permissions
 import django_filters
+from django_filters.rest_framework import FilterSet, DjangoFilterBackend
+from django_filters import NumberFilter
 
-from InvenTree.models import FilterChildren
+from rest_framework import generics, permissions
+
+# from InvenTree.models import FilterChildren
 from .models import StockLocation, StockItem
 from .serializers import StockItemSerializer, LocationSerializer
 
@@ -14,37 +17,24 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
 
 
 class StockFilter(django_filters.rest_framework.FilterSet):
-    min_stock = django_filters.NumberFilter(name='quantity', lookup_expr='gte')
-    max_stock = django_filters.NumberFilter(name='quantity', lookup_expr='lte')
+    min_stock = NumberFilter(name='quantity', lookup_expr='gte')
+    max_stock = NumberFilter(name='quantity', lookup_expr='lte')
+    part = NumberFilter(name='part', lookup_expr='exact')
+    location = NumberFilter(name='location', lookup_expr='exact')
 
     class Meta:
         model = StockItem
-        fields = ['quantity']
+        fields = ['quantity', 'part', 'location']
 
 
 class StockList(generics.ListCreateAPIView):
 
+    queryset = StockItem.objects.all()
     serializer_class = StockItemSerializer
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
-    filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
+    filter_backends = (DjangoFilterBackend,)
     filter_class = StockFilter
 
-    def get_queryset(self):
-        items = StockItem.objects.all()
-
-        # Specify a particular part
-        part_id = self.request.query_params.get('part', None)
-        if part_id:
-            items = items.filter(part=part_id)
-
-        # Specify a particular location
-        loc_id = self.request.query_params.get('location', None)
-
-        if loc_id:
-            items = items.filter(location=loc_id)
-
-        return items
-
 
 class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
     """ Return information on a specific stock location
@@ -55,19 +45,22 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
 
 
+class StockLocationFilter(FilterSet):
+
+    parent = NumberFilter(name='parent', lookup_expr='exact')
+
+    class Meta:
+        model = StockLocation
+        fields = ['parent']
+
+
 class LocationList(generics.ListCreateAPIView):
     """ Return a list of top-level locations
     Locations are considered "top-level" if they do not have a parent
     """
 
-    def get_queryset(self):
-        params = self.request.query_params
-
-        locations = StockLocation.objects.all()
-
-        locations = FilterChildren(locations, params.get('parent', None))
-
-        return locations
-
+    queryset = StockLocation.objects.all()
     serializer_class = LocationSerializer
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
+    filter_backends = (DjangoFilterBackend,)
+    filter_class = StockLocationFilter
diff --git a/InvenTree/supplier/customer_urls.py b/InvenTree/supplier/customer_urls.py
index 870bba1a31..9bfc222074 100644
--- a/InvenTree/supplier/customer_urls.py
+++ b/InvenTree/supplier/customer_urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url, include
+from django.conf.urls import url
 
 from . import views
 
diff --git a/InvenTree/supplier/manufacturer_urls.py b/InvenTree/supplier/manufacturer_urls.py
index 56a74c8106..9586990f1b 100644
--- a/InvenTree/supplier/manufacturer_urls.py
+++ b/InvenTree/supplier/manufacturer_urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url, include
+from django.conf.urls import url
 
 from . import views
 
diff --git a/InvenTree/supplier/part_urls.py b/InvenTree/supplier/part_urls.py
new file mode 100644
index 0000000000..813e4ce37e
--- /dev/null
+++ b/InvenTree/supplier/part_urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPartDetail.as_view(), name='supplierpart-detail'),
+
+    url(r'^\?.*/?$', views.SupplierPartList.as_view()),
+    url(r'^$', views.SupplierPartList.as_view())
+]
diff --git a/InvenTree/supplier/price_urls.py b/InvenTree/supplier/price_urls.py
new file mode 100644
index 0000000000..044f22b289
--- /dev/null
+++ b/InvenTree/supplier/price_urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPriceBreakDetail.as_view(), name='supplierpricebreak-detail'),
+
+    url(r'^\?.*/?$', views.SupplierPriceBreakList.as_view()),
+    url(r'^$', views.SupplierPriceBreakList.as_view())
+]
diff --git a/InvenTree/supplier/serializers.py b/InvenTree/supplier/serializers.py
index 88c23a573c..5aa278ed4b 100644
--- a/InvenTree/supplier/serializers.py
+++ b/InvenTree/supplier/serializers.py
@@ -35,13 +35,13 @@ class SupplierPartSerializer(serializers.ModelSerializer):
                                                        view_name='supplierpricebreak-detail')
 
     part = serializers.HyperlinkedRelatedField(view_name='part-detail',
-                                               queryset = Part.objects.all())
+                                               queryset=Part.objects.all())
 
     supplier = serializers.HyperlinkedRelatedField(view_name='supplier-detail',
-                                                   queryset = Supplier.objects.all())
+                                                   queryset=Supplier.objects.all())
 
     manufacturer = serializers.HyperlinkedRelatedField(view_name='manufacturer-detail',
-                                                       queryset = Manufacturer.objects.all())
+                                                       queryset=Manufacturer.objects.all())
 
     class Meta:
         model = SupplierPart
diff --git a/InvenTree/supplier/urls.py b/InvenTree/supplier/urls.py
index 5efac75d31..f810662976 100644
--- a/InvenTree/supplier/urls.py
+++ b/InvenTree/supplier/urls.py
@@ -1,14 +1,7 @@
-from django.conf.urls import url, include
+from django.conf.urls import url
 
 from . import views
 
-partpatterns = [
-    url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPartDetail.as_view(), name='supplierpart-detail'),
-
-    url(r'^\?.*/?$', views.SupplierPartList.as_view()),
-    url(r'^$', views.SupplierPartList.as_view())
-]
-
 pricepatterns = [
     url(r'^(?P<pk>[0-9]+)/?$', views.SupplierPriceBreakDetail.as_view(), name='supplierpricebreak-detail'),
 
@@ -18,12 +11,6 @@ pricepatterns = [
 
 urlpatterns = [
 
-    # Supplier part information
-    url(r'part/', include(partpatterns)),
-
-    # Supplier price information
-    url(r'price/', include(pricepatterns)),
-
     # Display details of a supplier
     url(r'^(?P<pk>[0-9]+)/$', views.SupplierDetail.as_view(), name='supplier-detail'),
 
diff --git a/InvenTree/supplier/views.py b/InvenTree/supplier/views.py
index a12a7f5124..5867ddea67 100644
--- a/InvenTree/supplier/views.py
+++ b/InvenTree/supplier/views.py
@@ -1,3 +1,6 @@
+from django_filters.rest_framework import FilterSet, DjangoFilterBackend
+from django_filters import NumberFilter
+
 from rest_framework import generics, permissions
 
 from .models import Supplier, SupplierPart, SupplierPriceBreak
@@ -58,28 +61,27 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
 
 
+class SupplierPartFilter(FilterSet):
+
+    supplier = NumberFilter(name='supplier', lookup_expr='exact')
+
+    part = NumberFilter(name='part', lookup_expr='exact')
+
+    manufacturer = NumberFilter(name='manufacturer', lookup_expr='exact')
+
+    class Meta:
+        model = SupplierPart
+        fields = ['supplier', 'part', 'manufacturer']
+
+
 class SupplierPartList(generics.ListCreateAPIView):
 
+    queryset = SupplierPart.objects.all()
     serializer_class = SupplierPartSerializer
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
 
-    def get_queryset(self):
-        parts = SupplierPart.objects.all()
-        params = self.request.query_params
-
-        supplier_id = params.get('supplier', None)
-        if supplier_id:
-            parts = parts.filter(supplier=supplier_id)
-
-        part_id = params.get('part', None)
-        if part_id:
-            parts = parts.filter(part=part_id)
-
-        manu_id = params.get('manufacturer', None)
-        if manu_id:
-            parts = parts.filter(manufacturer=manu_id)
-
-        return parts
+    filter_backends = (DjangoFilterBackend,)
+    filter_class = SupplierPartFilter
 
 
 class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView):
@@ -89,17 +91,20 @@ class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView):
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
 
 
+class PriceBreakFilter(FilterSet):
+
+    part = NumberFilter(name='part', lookup_expr='exact')
+
+    class Meta:
+        model = SupplierPriceBreak
+        fields = ['part']
+
+
 class SupplierPriceBreakList(generics.ListCreateAPIView):
 
-    def get_queryset(self):
-        prices = SupplierPriceBreak.objects.all()
-        params = self.request.query_params
-
-        part_id = params.get('part', None)
-        if part_id:
-            prices = prices.filter(part=part_id)
-
-        return prices
-
+    queryset = SupplierPriceBreak.objects.all()
     serializer_class = SupplierPriceBreakSerializer
     permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
+
+    filter_backends = (DjangoFilterBackend,)
+    filter_class = PriceBreakFilter
diff --git a/InvenTree/track/models.py b/InvenTree/track/models.py
index d0e8a5aa46..1bc189ae7c 100644
--- a/InvenTree/track/models.py
+++ b/InvenTree/track/models.py
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
 from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext as _
 from django.db import models
-from django.contrib.auth.models import User
+# from django.contrib.auth.models import User
 
 from supplier.models import Customer
 from part.models import Part, PartRevision