diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index ab2205a296..975af8c20b 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -32,6 +32,7 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ + 'django_filters', 'rest_framework', # Core django modules diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index c479037743..5afa01a293 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -95,13 +95,8 @@ class PartParameterTemplate(models.Model): A PartParameterTemplate can be optionally associated with a PartCategory """ name = models.CharField(max_length=20) - description = models.CharField(max_length=100, blank=True) units = models.CharField(max_length=10, blank=True) - default_value = models.CharField(max_length=50, blank=True) - default_min = models.CharField(max_length=50, blank=True) - default_max = models.CharField(max_length=50, blank=True) - # Parameter format PARAM_NUMERIC = 10 PARAM_TEXT = 20 @@ -143,10 +138,32 @@ class CategoryParameterLink(models.Model): verbose_name_plural = "Category Parameters" +class PartParameterManager(models.Manager): + """ Manager for handling PartParameter objects + """ + + def create(self, *args, **kwargs): + """ Prevent creation of duplicate PartParameter + """ + + part_id = kwargs['part'] + template_id = kwargs['template'] + + try: + params = self.filter(part=part_id, template=template_id) + return params[0] + except: + pass + + return super(PartParameterManager, self).create(*args, **kwargs) + + class PartParameter(models.Model): """ PartParameter is associated with a single part """ + objects = PartParameterManager() + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') template = models.ForeignKey(PartParameterTemplate) @@ -155,17 +172,6 @@ class PartParameter(models.Model): min_value = models.CharField(max_length=50, blank=True) max_value = models.CharField(max_length=50, blank=True) - # Prevent multiple parameters of the same template - # from being added to the same part - def save(self, *args, **kwargs): - params = PartParameter.objects.filter(part=self.part, template=self.template) - if len(params) > 1: - return - if len(params) == 1 and params[0].id != self.id: - return - - super(PartParameter, self).save(*args, **kwargs) - def __str__(self): return "{name} : {val}{units}".format( name=self.template.name, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 774815ac1a..06ad724b50 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import Part, PartCategory, PartParameter +from .models import Part, PartCategory, PartParameter, PartParameterTemplate class PartParameterSerializer(serializers.ModelSerializer): @@ -58,3 +58,13 @@ class PartCategoryDetailSerializer(serializers.ModelSerializer): 'path', 'children', 'parts') + + +class PartTemplateSerializer(serializers.ModelSerializer): + + class Meta: + model = PartParameterTemplate + fields = ('pk', + 'name', + 'units', + 'format') diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 01db39c149..b351ce3eda 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -16,16 +16,24 @@ categorypatterns = [ url(r'^$', views.PartCategoryList.as_view()) ] -""" URL patterns associated with a particular part: -/part/ -> Detail view of a given part -/part//parameters -> List parameters associated with a part -""" -partdetailpatterns = [ - # Single part detail - url(r'^$', views.PartDetail.as_view()), +partparampatterns = [ + # Detail of a single part parameter + url(r'^(?P[0-9]+)/$', views.PartParamDetail.as_view()), + + # Parameters associated with a particular part + url(r'^\?[^/]*/$', views.PartParamList.as_view()), + + # All part parameters + url(r'^$', views.PartParamList.as_view()), +] + +parttemplatepatterns = [ + # Detail of a single part field template + url(r'^(?P[0-9]+)/$', views.PartTemplateDetail.as_view()), + + # List all part field templates + url(r'^$', views.PartTemplateList.as_view()) - # View part parameters - url(r'parameters/$', views.PartParameters.as_view()) ] """ Top-level URL patterns for the Part app: @@ -36,11 +44,17 @@ partdetailpatterns = [ """ urlpatterns = [ # Individual part - url(r'^(?P[0-9]+)/', include(partdetailpatterns)), + url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), # Part categories url(r'^category/', views.PartCategoryList.as_view()), - # List of all parts - url(r'^$', views.PartList.as_view()) + # Part parameters + url(r'^parameters/', include(partparampatterns)), + + # Part templates + url(r'^templates/', include(parttemplatepatterns)), + + # List parts with optional filters + url(r'^\?*[^/]*/?$', views.PartList.as_view()), ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 55d5255789..c4646b6a6e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,9 +1,12 @@ +# import django_filters + from rest_framework import generics, permissions -from .models import PartCategory, Part, PartParameter +from .models import PartCategory, Part, PartParameter, PartParameterTemplate from .serializers import PartSerializer from .serializers import PartCategoryDetailSerializer from .serializers import PartParameterSerializer +from .serializers import PartTemplateSerializer class PartDetail(generics.RetrieveUpdateDestroyAPIView): @@ -14,25 +17,69 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class PartParameters(generics.ListCreateAPIView): +class PartParamList(generics.ListCreateAPIView): """ Return all parameters associated with a particular part """ def get_queryset(self): - part_id = self.kwargs['pk'] - return PartParameter.objects.filter(part=part_id) + part_id = self.request.query_params.get('part', None) + + if part_id: + return PartParameter.objects.filter(part=part_id) + else: + return PartParameter.objects.all() serializer_class = PartParameterSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + def create(self, request, *args, **kwargs): + # Ensure part link is set correctly + part_id = self.request.query_params.get('part', None) + if part_id: + request.data['part'] = part_id + return super(PartParamList, self).create(request, *args, **kwargs) + + +class PartParamDetail(generics.RetrieveUpdateDestroyAPIView): + """ Detail view of a single PartParameter + """ + + queryset = PartParameter.objects.all() + serializer_class = PartParameterSerializer + 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 Meta: + model = Part + fields = ['stock'] +""" + class PartList(generics.ListCreateAPIView): + """ Display a list of parts, with optional filters + Filters are specified in the url, e.g. + /part/?category=127 + /part/?min_stock=100 + """ + + def get_queryset(self): + parts = Part.objects.all() + + cat_id = self.request.query_params.get('category', None) + if cat_id: + parts = parts.filter(category=cat_id) + + return parts - queryset = Part.objects.all() serializer_class = PartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) -class PartCategoryDetail(generics.RetrieveUpdateAPIView): +class PartCategoryDetail(generics.RetrieveUpdateDestroyAPIView): """ Return information on a single PartCategory """ queryset = PartCategory.objects.all() @@ -47,3 +94,17 @@ class PartCategoryList(generics.ListCreateAPIView): queryset = PartCategory.objects.filter(parent=None) serializer_class = PartCategoryDetailSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class PartTemplateDetail(generics.RetrieveUpdateDestroyAPIView): + + queryset = PartParameterTemplate.objects.all() + serializer_class = PartTemplateSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class PartTemplateList(generics.ListCreateAPIView): + + queryset = PartParameterTemplate.objects.all() + serializer_class = PartTemplateSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index bf2bd42368..0b063af036 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -41,12 +41,38 @@ class Project(models.Model): return self.projectpart_set.all() +class ProjectPartManager(models.Manager): + """ Manager for handling ProjectParts + """ + + def create(self, *args, **kwargs): + """ Test for validity of new ProjectPart before actually creating it. + If a ProjectPart already exists that references the same: + a) Part + b) Project + then return THAT project instead. + """ + + project_id = kwargs['project'] + part_id = kwargs['part'] + + try: + project_parts = self.filter(project=project_id, part=part_id) + return project_parts[0] + except: + pass + + return super(ProjectPartManager, self).create(*args, **kwargs) + + class ProjectPart(models.Model): """ A project part associates a single part with a project The quantity of parts required for a single-run of that project is stored. The overage is the number of extra parts that are generally used for a single run. """ + objects = ProjectPartManager() + part = models.ForeignKey(Part, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE) quantity = models.PositiveIntegerField(default=1) diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py index 1dd0fba554..27c7ab2f0b 100644 --- a/InvenTree/project/urls.py +++ b/InvenTree/project/urls.py @@ -9,9 +9,14 @@ from . import views projectdetailpatterns = [ # Single project detail url(r'^$', views.ProjectDetail.as_view()), +] - # Parts associated with a project - url(r'^parts/$', views.ProjectPartsList.as_view()), +projectpartpatterns = [ + # Detail of a single project part + url(r'^(?P[0-9]+)/$', views.ProjectPartDetail.as_view()), + + # List project parts, with optional filters + url(r'^\?*[^/]*/?$', views.ProjectPartsList.as_view()), ] projectcategorypatterns = [ @@ -23,7 +28,6 @@ projectcategorypatterns = [ # Create a new category url(r'^new/$', views.NewProjectCategory.as_view()) - ] urlpatterns = [ @@ -34,6 +38,9 @@ urlpatterns = [ # List of all projects url(r'^$', views.ProjectList.as_view()), + # Project parts + url(r'^parts/', include(projectpartpatterns)), + # Project categories url(r'^category/', include(projectcategorypatterns)), ] diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index bb4d415139..923fff27fe 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -6,7 +6,9 @@ from .serializers import ProjectCategoryDetailSerializer from .serializers import ProjectPartSerializer -class ProjectDetail(generics.RetrieveUpdateAPIView): +class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): + """ Project details + """ queryset = Project.objects.all() serializer_class = ProjectSerializer @@ -14,6 +16,8 @@ class ProjectDetail(generics.RetrieveUpdateAPIView): class ProjectList(generics.ListCreateAPIView): + """ List all projects + """ queryset = Project.objects.all() serializer_class = ProjectSerializer @@ -28,6 +32,8 @@ class NewProjectCategory(generics.CreateAPIView): class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): + """ Project details + """ queryset = ProjectCategory.objects.all() serializer_class = ProjectCategoryDetailSerializer @@ -35,6 +41,9 @@ class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): class ProjectCategoryList(generics.ListCreateAPIView): + """ Top-level project categories. + Projects are considered top-level if they do not have a parent + """ queryset = ProjectCategory.objects.filter(parent=None) serializer_class = ProjectCategoryDetailSerializer @@ -42,10 +51,32 @@ class ProjectCategoryList(generics.ListCreateAPIView): class ProjectPartsList(generics.ListCreateAPIView): + """ List all parts associated with a particular project + """ serializer_class = ProjectPartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) def get_queryset(self): - project_id = self.kwargs['pk'] - return ProjectPart.objects.filter(project=project_id) + project_id = self.request.query_params.get('project', None) + + if project_id: + return ProjectPart.objects.filter(project=project_id) + else: + return ProjectPart.objects.all() + + def create(self, request, *args, **kwargs): + # Ensure project link is set correctly + prj_id = self.request.query_params.get('project', None) + if prj_id: + request.data['project'] = prj_id + return super(ProjectPartsList, self).create(request, *args, **kwargs) + + +class ProjectPartDetail(generics.RetrieveUpdateDestroyAPIView): + """ Detail for a single project part + """ + + queryset = ProjectPart.objects.all() + serializer_class = ProjectPartSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index a9dc1bc09e..c7a27fe560 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -10,5 +10,6 @@ urlpatterns = [ url(r'^location/(?P[0-9]+)$', views.LocationDetail.as_view()), # List all top-level locations - url(r'^location/$', views.LocationList.as_view()) + url(r'^location/$', views.LocationList.as_view()), + url(r'^$', views.LocationList.as_view()) ] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 5f76cbebf1..8100abdb92 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -5,7 +5,7 @@ from .models import StockLocation, StockItem from .serializers import StockItemSerializer, LocationDetailSerializer -class PartStockDetail(generics.ListAPIView): +class PartStockDetail(generics.ListCreateAPIView): """ Return a list of all stockitems for a given part """ diff --git a/requirements/base.txt b/requirements/base.txt index 1a4283d80d..0c17b62bfe 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,2 +1,3 @@ Django==1.11 djangorestframework==3.6.2 +django_filter==1.0.2