diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index c6aec418ad..35ed5ccc0c 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -11,7 +11,7 @@ class Company(models.Model): class Meta: abstract = True - name = models.CharField(max_length=100) + name = models.CharField(max_length=100, unique=True) website = models.URLField(blank=True) address = models.CharField(max_length=200, blank=True) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 975af8c20b..5cfdbcf924 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -79,6 +79,10 @@ TEMPLATES = [ }, ] +REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'InvenTree.utils.api_exception_handler' +} + WSGI_APPLICATION = 'InvenTree.wsgi.application' diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 63014a36d1..9e71cb3e86 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -3,19 +3,41 @@ from django.contrib import admin from rest_framework.documentation import include_docs_urls +from part.urls import part_urls, part_cat_urls, part_param_urls, part_param_template_urls +from stock.urls import stock_urls, stock_loc_urls +from project.urls import prj_urls, prj_part_urls, prj_cat_urls +from supplier.urls import cust_urls, manu_urls, supplier_part_urls, price_break_urls, supplier_urls +from track.urls import unique_urls, part_track_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')), - url(r'^project/', include('project.urls')), + + # Stock URLs + url(r'^stock/', include(stock_urls)), + url(r'^stock-location/', include(stock_loc_urls)), + + # Part URLs + url(r'^part/', include(part_urls)), + url(r'^part-category/', include(part_cat_urls)), + url(r'^part-param/', include(part_param_urls)), + url(r'^part-param-template/', include(part_param_template_urls)), + + # Supplier URLs + url(r'^supplier/', include(supplier_urls)), + url(r'^supplier-part/', include(supplier_part_urls)), + url(r'^price-break/', include(price_break_urls)), + url(r'^manufacturer/', include(manu_urls)), + url(r'^customer/', include(cust_urls)), + + # Tracking URLs + url(r'^track/', include(part_track_urls)), + url(r'^unique-part/', include(unique_urls)), + + # Project URLs + url(r'^project/', include(prj_urls)), + url(r'^project-category/', include(prj_cat_urls)), + url(r'^project-part/', include(prj_part_urls)), ] urlpatterns = [ diff --git a/InvenTree/InvenTree/utils.py b/InvenTree/InvenTree/utils.py new file mode 100644 index 0000000000..dc28da81a0 --- /dev/null +++ b/InvenTree/InvenTree/utils.py @@ -0,0 +1,13 @@ +from rest_framework.views import exception_handler + + +def api_exception_handler(exc, context): + response = exception_handler(exc, context) + + # Now add the HTTP status code to the response. + if response is not None: + + data = {'error': response.data} + response.data = data + + return response diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4edbf6f7f0..f5daf0915a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -187,20 +187,3 @@ class PartParameter(models.Model): class Meta: verbose_name = "Part Parameter" verbose_name_plural = "Part Parameters" - - -class PartRevision(models.Model): - """ A PartRevision represents a change-notification to a Part - A Part may go through several revisions in its lifetime, - which should be tracked. - UniqueParts can have a single associated PartRevision - """ - - part = models.ForeignKey(Part, on_delete=models.CASCADE) - - name = models.CharField(max_length=100) - description = models.CharField(max_length=500) - revision_date = models.DateField(auto_now_add=True) - - def __str__(self): - return self.name diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index bc38ca309d..48719e9cd5 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -29,7 +29,9 @@ class PartSerializer(serializers.HyperlinkedModelSerializer): 'IPN', 'description', 'category', - 'stock') + 'stock', + 'units', + 'trackable') class PartCategorySerializer(serializers.HyperlinkedModelSerializer): diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 72fffc5b32..1ed26cac2b 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -1,13 +1,8 @@ -from django.conf.urls import url, include +from django.conf.urls import url from . import views -""" URL patterns associated with part categories: -/category -> List all top-level categories -/category/ -> Detail view of given category -/category/new -> Create a new category -""" -categorypatterns = [ +part_cat_urls = [ # Part category detail url(r'^(?P[0-9]+)/?$', views.PartCategoryDetail.as_view(), name='partcategory-detail'), @@ -17,7 +12,7 @@ categorypatterns = [ url(r'^$', views.PartCategoryList.as_view()) ] -partparampatterns = [ +part_param_urls = [ # Detail of a single part parameter url(r'^(?P[0-9]+)/?$', views.PartParamDetail.as_view(), name='partparameter-detail'), @@ -26,7 +21,7 @@ partparampatterns = [ url(r'^$', views.PartParamList.as_view()), ] -parttemplatepatterns = [ +part_param_template_urls = [ # Detail of a single part field template url(r'^(?P[0-9]+)/?$', views.PartTemplateDetail.as_view(), name='partparametertemplate-detail'), @@ -35,21 +30,7 @@ parttemplatepatterns = [ url(r'^$', views.PartTemplateList.as_view()) ] -""" Top-level URL patterns for the Part app: -/part/ -> List all parts -/part/new -> Create a new part -/part/ -> (refer to partdetailpatterns) -/part/category -> (refer to categorypatterns) -""" -urlpatterns = [ - # Part categories - url(r'^category/', include(categorypatterns)), - - # Part parameters - url(r'^parameter/', include(partparampatterns)), - - # Part templates - url(r'^template/', include(parttemplatepatterns)), +part_urls = [ # Individual part url(r'^(?P[0-9]+)/?$', views.PartDetail.as_view(), name='part-detail'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index eb930606b1..40c7134860 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,4 +1,5 @@ -# import django_filters +from django_filters.rest_framework import FilterSet, DjangoFilterBackend +from django_filters import NumberFilter from rest_framework import generics, permissions @@ -11,30 +12,61 @@ from .serializers import PartTemplateSerializer class PartDetail(generics.RetrieveUpdateDestroyAPIView): - """ Return information on a single part + """ + + get: + Return detail on a single Part + + post: + Update data for a single Part + + delete: + Remove a part from the database + """ queryset = Part.objects.all() serializer_class = PartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) +class PartParamFilter(FilterSet): + + part = NumberFilter(name='part', lookup_expr='exact') + + class Meta: + model = PartParameter + fields = ['part'] + + class PartParamList(generics.ListCreateAPIView): - """ Return all parameters associated with a particular part """ - def get_queryset(self): - part_id = self.request.query_params.get('part', None) - if part_id: - return PartParameter.objects.filter(part=part_id) - else: - return PartParameter.objects.all() + get: + Return a list of all part parameters (with optional filters) + post: + Create a new part parameter + """ + + queryset = PartParameter.objects.all() serializer_class = PartParameterSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + filter_backends = (DjangoFilterBackend,) + filter_class = PartParamFilter class PartParamDetail(generics.RetrieveUpdateDestroyAPIView): - """ Detail view of a single PartParameter + """ + + get: + Detail view of a single PartParameter + + post: + Update data for a PartParameter + + delete: + Remove a PartParameter from the database + """ queryset = PartParameter.objects.all() @@ -42,38 +74,43 @@ class PartParamDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -""" -class PartFilter(django_filters.rest_framework.FilterSet): - min_stock = django_filters.NumberFilter(name="stock", lookup_expr="gte") - max_stock = django_filters.NumberFilter(name="stock", lookup_expr="lte") +class PartFilter(FilterSet): + category = NumberFilter(name='category', lookup_expr='exact') class Meta: model = Part - fields = ['stock'] -""" + fields = ['category'] class PartList(generics.ListCreateAPIView): - """ List of parts, with optional filters """ - def get_queryset(self): - parts = Part.objects.all() - params = self.request.query_params + get: + List of Parts, with optional filters - cat_id = params.get('category', None) - - if cat_id: - parts = parts.filter(category=cat_id) - - return parts + post: + Create a new Part + """ + queryset = Part.objects.all() serializer_class = PartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + filter_backends = (DjangoFilterBackend,) + filter_class = PartFilter class PartCategoryDetail(generics.RetrieveUpdateDestroyAPIView): - """ Return information on a single PartCategory + """ + + get: + Return information on a single PartCategory + + post: + Update a PartCategory + + delete: + Remove a PartCategory + """ queryset = PartCategory.objects.all() serializer_class = PartCategorySerializer @@ -81,8 +118,14 @@ class PartCategoryDetail(generics.RetrieveUpdateDestroyAPIView): class PartCategoryList(generics.ListCreateAPIView): - """ Return a list of all top-level part categories. - Categories are considered "top-level" if they do not have a parent + """ + + get: + Return a list of all categories + (with optional filters) + + post: + Create a new PartCategory """ def get_queryset(self): @@ -100,6 +143,18 @@ class PartCategoryList(generics.ListCreateAPIView): class PartTemplateDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return detail on a single PartParameterTemplate object + + post: + Update a PartParameterTemplate object + + delete: + Remove a PartParameterTemplate object + + """ queryset = PartParameterTemplate.objects.all() serializer_class = PartTemplateSerializer @@ -107,6 +162,16 @@ class PartTemplateDetail(generics.RetrieveUpdateDestroyAPIView): class PartTemplateList(generics.ListCreateAPIView): + """ + + get: + Return a list of all PartParameterTemplate objects + (with optional query filters) + + post: + Create a new PartParameterTemplate object + + """ queryset = PartParameterTemplate.objects.all() serializer_class = PartTemplateSerializer diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py index a0f0bc29b6..ad150a00ca 100644 --- a/InvenTree/project/urls.py +++ b/InvenTree/project/urls.py @@ -1,36 +1,30 @@ -from django.conf.urls import url, include +from django.conf.urls import url from . import views -projectpartpatterns = [ +prj_part_urls = [ # Detail of a single project part url(r'^(?P[0-9]+)/?$', views.ProjectPartDetail.as_view(), name='projectpart-detail'), # List project parts, with optional filters url(r'^\?.*/?$', views.ProjectPartsList.as_view()), - url(r'^$', views.ProjectPartsList.as_view()), + url(r'^$', views.ProjectPartsList.as_view()) ] -projectcategorypatterns = [ +prj_cat_urls = [ # Detail of a single project category url(r'^(?P[0-9]+)/?$', views.ProjectCategoryDetail.as_view(), name='projectcategory-detail'), # List of project categories, with filters url(r'^\?.*/?$', views.ProjectCategoryList.as_view()), - url(r'^$', views.ProjectCategoryList.as_view()), + url(r'^$', views.ProjectCategoryList.as_view()) ] -urlpatterns = [ +prj_urls = [ # Individual project URL url(r'^(?P[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()), - - # Project parts - url(r'^parts/', include(projectpartpatterns)), - - # Project categories - url(r'^category/', include(projectcategorypatterns)), + url(r'^$', views.ProjectList.as_view()) ] diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index 1aeeb5d6b6..2a293a0cca 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -8,7 +8,17 @@ from .serializers import ProjectPartSerializer class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): - """ Project details + """ + + get: + Return a single Project object + + post: + Update a Project + + delete: + Remove a Project + """ queryset = Project.objects.all() @@ -17,7 +27,15 @@ class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): class ProjectList(generics.ListCreateAPIView): - """ List projects + """ + + get: + Return a list of all Project objects + (with optional query filters) + + post: + Create a new Project + """ def get_queryset(self): @@ -36,7 +54,17 @@ class ProjectList(generics.ListCreateAPIView): class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): - """ Project details + """ + + get: + Return a single ProjectCategory object + + post: + Update a ProjectCategory + + delete: + Remove a ProjectCategory + """ queryset = ProjectCategory.objects.all() @@ -45,7 +73,14 @@ class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): class ProjectCategoryList(generics.ListCreateAPIView): - """ List project categories + """ + + get: + Return a list of all ProjectCategory objects + + post: + Create a new ProjectCategory + """ def get_queryset(self): @@ -62,7 +97,14 @@ class ProjectCategoryList(generics.ListCreateAPIView): class ProjectPartsList(generics.ListCreateAPIView): - """ List project parts + """ + + get: + Return a list of all ProjectPart objects + + post: + Create a new ProjectPart + """ serializer_class = ProjectPartSerializer @@ -84,7 +126,17 @@ class ProjectPartsList(generics.ListCreateAPIView): class ProjectPartDetail(generics.RetrieveUpdateDestroyAPIView): - """ Detail for a single project part + """ + + get: + Return a single ProjectPart object + + post: + Update a ProjectPart + + delete: + Remove a ProjectPart + """ queryset = ProjectPart.objects.all() diff --git a/InvenTree/stock/location_urls.py b/InvenTree/stock/location_urls.py deleted file mode 100644 index 9df7a53b28..0000000000 --- a/InvenTree/stock/location_urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf.urls import url -from . import views - -urlpatterns = [ - url(r'^(?P[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 b2822682d5..bf58ddbb89 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -2,7 +2,7 @@ from django.conf.urls import url from . import views -urlpatterns = [ +stock_urls = [ # Detail for a single stock item url(r'^(?P[0-9]+)/?$', views.StockDetail.as_view(), name='stockitem-detail'), @@ -10,3 +10,11 @@ urlpatterns = [ url(r'^\?.*/?$', views.StockList.as_view()), url(r'^$', views.StockList.as_view()), ] + +stock_loc_urls = [ + url(r'^(?P[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/views.py b/InvenTree/stock/views.py index 27fa65bfbe..6c19410fc4 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1,4 +1,3 @@ -import django_filters from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters import NumberFilter @@ -10,13 +9,24 @@ from .serializers import StockItemSerializer, LocationSerializer class StockDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single StockItem object + + post: + Update a StockItem + + delete: + Remove a StockItem + """ queryset = StockItem.objects.all() serializer_class = StockItemSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class StockFilter(django_filters.rest_framework.FilterSet): +class StockFilter(FilterSet): min_stock = NumberFilter(name='quantity', lookup_expr='gte') max_stock = NumberFilter(name='quantity', lookup_expr='lte') part = NumberFilter(name='part', lookup_expr='exact') @@ -28,6 +38,15 @@ class StockFilter(django_filters.rest_framework.FilterSet): class StockList(generics.ListCreateAPIView): + """ + + get: + Return a list of all StockItem objects + (with optional query filters) + + post: + Create a new StockItem + """ queryset = StockItem.objects.all() serializer_class = StockItemSerializer @@ -37,7 +56,17 @@ class StockList(generics.ListCreateAPIView): class LocationDetail(generics.RetrieveUpdateDestroyAPIView): - """ Return information on a specific stock location + """ + + get: + Return a single StockLocation object + + post: + Update a StockLocation object + + delete: + Remove a StockLocation object + """ queryset = StockLocation.objects.all() @@ -55,8 +84,15 @@ class StockLocationFilter(FilterSet): class LocationList(generics.ListCreateAPIView): - """ Return a list of top-level locations - Locations are considered "top-level" if they do not have a parent + """ + + get: + Return a list of all StockLocation objects + (with optional query filter) + + post: + Create a new StockLocation + """ queryset = StockLocation.objects.all() diff --git a/InvenTree/supplier/customer_urls.py b/InvenTree/supplier/customer_urls.py deleted file mode 100644 index 9bfc222074..0000000000 --- a/InvenTree/supplier/customer_urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.conf.urls import url - -from . import views - -urlpatterns = [ - # Customer detail - url(r'^(?P[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 deleted file mode 100644 index 9586990f1b..0000000000 --- a/InvenTree/supplier/manufacturer_urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.conf.urls import url - -from . import views - -urlpatterns = [ - # Manufacturer detail - url(r'^(?P[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/part_urls.py b/InvenTree/supplier/part_urls.py deleted file mode 100644 index 813e4ce37e..0000000000 --- a/InvenTree/supplier/part_urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf.urls import url - -from . import views - -urlpatterns = [ - url(r'^(?P[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 deleted file mode 100644 index 044f22b289..0000000000 --- a/InvenTree/supplier/price_urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf.urls import url - -from . import views - -urlpatterns = [ - url(r'^(?P[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/urls.py b/InvenTree/supplier/urls.py index f810662976..f6491f5bba 100644 --- a/InvenTree/supplier/urls.py +++ b/InvenTree/supplier/urls.py @@ -2,14 +2,39 @@ from django.conf.urls import url from . import views -pricepatterns = [ +cust_urls = [ + # Customer detail + url(r'^(?P[0-9]+)/?$', views.CustomerDetail.as_view(), name='customer-detail'), + + # List customers + url(r'^\?.*/?$', views.CustomerList.as_view()), + url(r'^$', views.CustomerList.as_view()) +] + +manu_urls = [ + # Manufacturer detail + url(r'^(?P[0-9]+)/?$', views.ManufacturerDetail.as_view(), name='manufacturer-detail'), + + # List manufacturers + url(r'^\?.*/?$', views.ManufacturerList.as_view()), + url(r'^$', views.ManufacturerList.as_view()) +] + +supplier_part_urls = [ + url(r'^(?P[0-9]+)/?$', views.SupplierPartDetail.as_view(), name='supplierpart-detail'), + + url(r'^\?.*/?$', views.SupplierPartList.as_view()), + url(r'^$', views.SupplierPartList.as_view()) +] + +price_break_urls = [ url(r'^(?P[0-9]+)/?$', views.SupplierPriceBreakDetail.as_view(), name='supplierpricebreak-detail'), url(r'^\?.*/?$', views.SupplierPriceBreakList.as_view()), url(r'^$', views.SupplierPriceBreakList.as_view()) ] -urlpatterns = [ +supplier_urls = [ # Display details of a supplier url(r'^(?P[0-9]+)/$', views.SupplierDetail.as_view(), name='supplier-detail'), diff --git a/InvenTree/supplier/views.py b/InvenTree/supplier/views.py index 5867ddea67..ac8995de48 100644 --- a/InvenTree/supplier/views.py +++ b/InvenTree/supplier/views.py @@ -13,6 +13,18 @@ from .serializers import CustomerSerializer class ManufacturerDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single Manufacturer + + post: + Update a Manufacturer + + delete: + Remove a Manufacturer + + """ queryset = Manufacturer.objects.all() serializer_class = ManufacturerSerializer @@ -20,6 +32,15 @@ class ManufacturerDetail(generics.RetrieveUpdateDestroyAPIView): class ManufacturerList(generics.ListCreateAPIView): + """ + + get: + Return a list of all Manufacturers + + post: + Create a new Manufacturer + + """ queryset = Manufacturer.objects.all() serializer_class = ManufacturerSerializer @@ -27,6 +48,18 @@ class ManufacturerList(generics.ListCreateAPIView): class CustomerDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single Customer + + post: + Update a Customer + + delete: + Remove a Customer + + """ queryset = Customer.objects.all() serializer_class = CustomerSerializer @@ -34,6 +67,15 @@ class CustomerDetail(generics.RetrieveUpdateDestroyAPIView): class CustomerList(generics.ListCreateAPIView): + """ + + get: + Return a list of all Cutstomers + + post: + Create a new Customer + + """ queryset = Customer.objects.all() serializer_class = CustomerSerializer @@ -41,6 +83,18 @@ class CustomerList(generics.ListCreateAPIView): class SupplierDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single Supplier + + post: + Update a supplier + + delete: + Remove a supplier + + """ queryset = Supplier.objects.all() serializer_class = SupplierSerializer @@ -48,6 +102,15 @@ class SupplierDetail(generics.RetrieveUpdateDestroyAPIView): class SupplierList(generics.ListCreateAPIView): + """ + + get: + Return a list of all Suppliers + + post: + Create a new Supplier + + """ queryset = Supplier.objects.all() serializer_class = SupplierSerializer @@ -55,6 +118,18 @@ class SupplierList(generics.ListCreateAPIView): class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single SupplierPart + + post: + Update a SupplierPart + + delete: + Remove a SupplierPart + + """ queryset = SupplierPart.objects.all() serializer_class = SupplierPartSerializer @@ -75,6 +150,16 @@ class SupplierPartFilter(FilterSet): class SupplierPartList(generics.ListCreateAPIView): + """ + + get: + List all SupplierParts + (with optional query filters) + + post: + Create a new SupplierPart + + """ queryset = SupplierPart.objects.all() serializer_class = SupplierPartSerializer @@ -85,6 +170,18 @@ class SupplierPartList(generics.ListCreateAPIView): class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single SupplierPriceBreak + + post: + Update a SupplierPriceBreak + + delete: + Remove a SupplierPriceBreak + + """ queryset = SupplierPriceBreak.objects.all() serializer_class = SupplierPriceBreakSerializer @@ -101,6 +198,16 @@ class PriceBreakFilter(FilterSet): class SupplierPriceBreakList(generics.ListCreateAPIView): + """ + + get: + Return a list of all SupplierPriceBreaks + (with optional query filters) + + post: + Create a new SupplierPriceBreak + + """ queryset = SupplierPriceBreak.objects.all() serializer_class = SupplierPriceBreakSerializer diff --git a/InvenTree/track/admin.py b/InvenTree/track/admin.py index 573e458878..a2908c2e7c 100644 --- a/InvenTree/track/admin.py +++ b/InvenTree/track/admin.py @@ -4,7 +4,7 @@ from .models import UniquePart class UniquePartAdmin(admin.ModelAdmin): - list_display = ('part', 'revision', 'serial', 'status', 'creation_date') + list_display = ('part', 'serial', 'status', 'creation_date') admin.site.register(UniquePart, UniquePartAdmin) diff --git a/InvenTree/track/models.py b/InvenTree/track/models.py index 1bc189ae7c..445e6d30fc 100644 --- a/InvenTree/track/models.py +++ b/InvenTree/track/models.py @@ -1,32 +1,23 @@ from __future__ import unicode_literals -from django.core.exceptions import ValidationError +from rest_framework.exceptions import ValidationError from django.utils.translation import ugettext as _ from django.db import models # from django.contrib.auth.models import User from supplier.models import Customer -from part.models import Part, PartRevision +from part.models import Part class UniquePartManager(models.Manager): - """ Ensures UniqueParts are correctly handled - """ def create(self, *args, **kwargs): - part_id = kwargs['part'] - sn = kwargs.get('serial', None) + print(kwargs) - if not sn: - raise ValidationError(_("Serial number must be supplied")) + part = kwargs.get('part', None) - if not isinstance(sn, int): - raise ValidationError(_("Serial number must be integer")) - - # Does a part already exists with this serial number? - parts = self.filter(part=part_id, serial=sn) - if len(parts) > 0: - raise ValidationError(_("Matching part and serial number found!")) + if not part.trackable: + raise ValidationError("Unique part cannot be created for a non-trackable part") return super(UniquePartManager, self).create(*args, **kwargs) @@ -37,19 +28,14 @@ class UniquePart(models.Model): and tracking all events in the life of a part """ + objects = UniquePartManager() + class Meta: # Cannot have multiple parts with same serial number unique_together = ('part', 'serial') - objects = UniquePartManager() - part = models.ForeignKey(Part, on_delete=models.CASCADE) - revision = models.ForeignKey(PartRevision, - on_delete=models.CASCADE, - blank=True, - null=True) - creation_date = models.DateField(auto_now_add=True, editable=False) serial = models.IntegerField() diff --git a/InvenTree/track/serializers.py b/InvenTree/track/serializers.py index 3bd84aa0e7..c2c097c28d 100644 --- a/InvenTree/track/serializers.py +++ b/InvenTree/track/serializers.py @@ -11,7 +11,6 @@ class UniquePartSerializer(serializers.HyperlinkedModelSerializer): model = UniquePart fields = ['url', 'part', - 'revision', 'creation_date', 'serial', # 'createdBy', diff --git a/InvenTree/track/urls.py b/InvenTree/track/urls.py index aaf3b539ce..cae9f9d5c2 100644 --- a/InvenTree/track/urls.py +++ b/InvenTree/track/urls.py @@ -1,16 +1,15 @@ -from django.conf.urls import url, include +from django.conf.urls import url from . import views -infopatterns = [ +part_track_urls = [ url(r'^(?P[0-9]+)/?$', views.PartTrackingDetail.as_view(), name='parttrackinginfo-detail'), url(r'^\?.*/?$', views.PartTrackingList.as_view()), url(r'^$', views.PartTrackingList.as_view()) ] -urlpatterns = [ - url(r'info/', include(infopatterns)), +unique_urls = [ # Detail for a single unique part url(r'^(?P[0-9]+)/?$', views.UniquePartDetail.as_view(), name='uniquepart-detail'), diff --git a/InvenTree/track/views.py b/InvenTree/track/views.py index 436e98344e..f45205993d 100644 --- a/InvenTree/track/views.py +++ b/InvenTree/track/views.py @@ -1,4 +1,5 @@ -import django_filters +from django_filters.rest_framework import FilterSet, DjangoFilterBackend +from django_filters import NumberFilter from rest_framework import generics, permissions @@ -7,69 +8,95 @@ from .serializers import UniquePartSerializer, PartTrackingInfoSerializer class UniquePartDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single UniquePart + + post: + Update a UniquePart + + delete: + Remove a UniquePart + + """ queryset = UniquePart.objects.all() serializer_class = UniquePartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class UniquePartFilter(django_filters.rest_framework.FilterSet): +class UniquePartFilter(FilterSet): # Filter based on serial number - min_sn = django_filters.NumberFilter(name='serial', lookup_expr='gte') - max_sn = django_filters.NumberFilter(name='serial', lookup_expr='lte') + min_sn = NumberFilter(name='serial', lookup_expr='gte') + max_sn = NumberFilter(name='serial', lookup_expr='lte') + + sn = NumberFilter(name='serial', lookup_expr='exact') + part = NumberFilter(name='part', lookup_expr='exact') + customer = NumberFilter(name='customer', lookup_expr='exact') class Meta: model = UniquePart - fields = ['serial', ] + fields = ['serial', 'part', 'customer'] class UniquePartList(generics.ListCreateAPIView): + """ + get: + Return a list of all UniqueParts + (with optional query filter) + + post: + Create a new UniquePart + """ + + queryset = UniquePart.objects.all() serializer_class = UniquePartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) + filter_backends = (DjangoFilterBackend,) filter_class = UniquePartFilter - def get_queryset(self): - parts = UniquePart.objects.all() - query = self.request.query_params - - # Filter by associated part - part_id = query.get('part', None) - if part_id: - parts = parts.filter(part=part_id) - - # Filter by serial number - sn = query.get('sn', None) - if sn: - parts = parts.filter(serial=sn) - - # Filter by customer - customer = query.get('customer', None) - if customer: - parts = parts.filter(customer=customer) - - return parts - class PartTrackingDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single PartTrackingInfo object + + post: + Update a PartTrackingInfo object + + delete: + Remove a PartTrackingInfo object + """ queryset = PartTrackingInfo.objects.all() serializer_class = PartTrackingInfoSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class PartTrackingList(generics.ListCreateAPIView): +class PartTrackingFilter(FilterSet): + part = NumberFilter(name='part', lookup_expr='exact') + class Meta: + model = PartTrackingInfo + fields = ['part'] + + +class PartTrackingList(generics.ListCreateAPIView): + """ + + get: + Return a list of all PartTrackingInfo objects + (with optional query filter) + + post: + Create a new PartTrackingInfo object + """ + + queryset = PartTrackingInfo.objects.all() serializer_class = PartTrackingInfoSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - def get_queryset(self): - tracking = PartTrackingInfo.objects.all() - query = self.request.query_params - - part_id = query.get('part', None) - if part_id: - tracking = tracking.filter(part=part_id) - - return tracking + filter_backends = (DjangoFilterBackend,) + filter_class = PartTrackingFilter