diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d750b6388c..5ab4076fad 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/models.py b/InvenTree/part/models.py index 5afa01a293..4edbf6f7f0 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -149,11 +149,9 @@ class PartParameterManager(models.Manager): part_id = kwargs['part'] template_id = kwargs['template'] - try: - params = self.filter(part=part_id, template=template_id) + params = self.filter(part=part_id, template=template_id) + if len(params) > 0: return params[0] - except: - pass return super(PartParameterManager, self).create(*args, **kwargs) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 7e5cfd3aaa..47bbaedf40 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -32,16 +32,11 @@ class PartSerializer(serializers.ModelSerializer): 'stock') -class PartCategoryBriefSerializer(serializers.ModelSerializer): +class PartCategorySerializer(serializers.ModelSerializer): - class Meta: - model = PartCategory - fields = ('pk', - 'name', - 'description') + children = serializers.PrimaryKeyRelatedField(many=True, read_only=True) - -class PartCategoryDetailSerializer(serializers.ModelSerializer): + parts = serializers.PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = PartCategory @@ -49,7 +44,9 @@ class PartCategoryDetailSerializer(serializers.ModelSerializer): 'name', 'description', 'parent', - 'path') + 'path', + 'children', + 'parts') class PartTemplateSerializer(serializers.ModelSerializer): diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index da9cb5ab4a..eb930606b1 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -5,7 +5,7 @@ from rest_framework import generics, permissions from InvenTree.models import FilterChildren from .models import PartCategory, Part, PartParameter, PartParameterTemplate from .serializers import PartSerializer -from .serializers import PartCategoryDetailSerializer +from .serializers import PartCategorySerializer from .serializers import PartParameterSerializer from .serializers import PartTemplateSerializer @@ -32,13 +32,6 @@ class PartParamList(generics.ListCreateAPIView): 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 @@ -83,7 +76,7 @@ class PartCategoryDetail(generics.RetrieveUpdateDestroyAPIView): """ Return information on a single PartCategory """ queryset = PartCategory.objects.all() - serializer_class = PartCategoryDetailSerializer + serializer_class = PartCategorySerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) @@ -102,7 +95,7 @@ class PartCategoryList(generics.ListCreateAPIView): return categories queryset = PartCategory.objects.filter(parent=None) - serializer_class = PartCategoryDetailSerializer + serializer_class = PartCategorySerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index 0b063af036..6cc8a01ce0 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -29,17 +29,11 @@ class Project(models.Model): name = models.CharField(max_length=100) description = models.CharField(max_length=500, blank=True) - category = models.ForeignKey(ProjectCategory, on_delete=models.CASCADE) + category = models.ForeignKey(ProjectCategory, on_delete=models.CASCADE, related_name='projects') def __str__(self): return self.name - @property - def projectParts(self): - """ Return a list of all project parts associated with this project - """ - return self.projectpart_set.all() - class ProjectPartManager(models.Manager): """ Manager for handling ProjectParts @@ -56,11 +50,9 @@ class ProjectPartManager(models.Manager): project_id = kwargs['project'] part_id = kwargs['part'] - try: - project_parts = self.filter(project=project_id, part=part_id) + project_parts = self.filter(project=project_id, part=part_id) + if len(project_parts) > 0: return project_parts[0] - except: - pass return super(ProjectPartManager, self).create(*args, **kwargs) diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py index 00c70fed18..5e3904cb3e 100644 --- a/InvenTree/project/serializers.py +++ b/InvenTree/project/serializers.py @@ -26,14 +26,11 @@ class ProjectSerializer(serializers.ModelSerializer): 'category') -class ProjectCategoryBriefSerializer(serializers.ModelSerializer): +class ProjectCategorySerializer(serializers.ModelSerializer): - class Meta: - model = ProjectCategory - fields = ('pk', 'name', 'description') + children = serializers.PrimaryKeyRelatedField(many=True, read_only=True) - -class ProjectCategoryDetailSerializer(serializers.ModelSerializer): + projects = serializers.PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = ProjectCategory @@ -41,4 +38,6 @@ class ProjectCategoryDetailSerializer(serializers.ModelSerializer): 'name', 'description', 'parent', - 'path') + 'path', + 'children', + 'projects') diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index 716cd19a79..1aeeb5d6b6 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -3,7 +3,7 @@ from rest_framework import generics, permissions from InvenTree.models import FilterChildren from .models import ProjectCategory, Project, ProjectPart from .serializers import ProjectSerializer -from .serializers import ProjectCategoryDetailSerializer +from .serializers import ProjectCategorySerializer from .serializers import ProjectPartSerializer @@ -40,7 +40,7 @@ class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): """ queryset = ProjectCategory.objects.all() - serializer_class = ProjectCategoryDetailSerializer + serializer_class = ProjectCategorySerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) @@ -57,7 +57,7 @@ class ProjectCategoryList(generics.ListCreateAPIView): return categories - serializer_class = ProjectCategoryDetailSerializer + serializer_class = ProjectCategorySerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) @@ -82,13 +82,6 @@ class ProjectPartsList(generics.ListCreateAPIView): return parts - 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 diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 96ffd8633c..60f3a09c82 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -45,13 +45,6 @@ class StockList(generics.ListCreateAPIView): return items - def create(self, request, *args, **kwargs): - # If the PART parameter is passed in the URL, use that - part_id = self.request.query_params.get('part', None) - if part_id: - request.data['part'] = part_id - return super(StockList, self).create(request, *args, **kwargs) - class LocationDetail(generics.RetrieveUpdateDestroyAPIView): """ Return information on a specific stock location diff --git a/InvenTree/track/models.py b/InvenTree/track/models.py index 4fed0fdd60..d50b6d7578 100644 --- a/InvenTree/track/models.py +++ b/InvenTree/track/models.py @@ -1,4 +1,5 @@ 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 @@ -7,12 +8,37 @@ from supplier.models import Customer from part.models import Part, PartRevision +class UniquePartManager(models.Manager): + """ Ensures UniqueParts are correctly handled + """ + + def create(self, *args, **kwargs): + + part_id = kwargs['part'] + sn = kwargs.get('serial', None) + + if not sn: + raise ValidationError(_("Serial number must be supplied")) + + 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!")) + + return super(UniquePartManager, self).create(*args, **kwargs) + + class UniquePart(models.Model): """ A unique instance of a Part object. Used for tracking parts based on serial numbers, and tracking all events in the life of a part """ + objects = UniquePartManager() + part = models.ForeignKey(Part, on_delete=models.CASCADE) revision = models.ForeignKey(PartRevision, @@ -50,6 +76,17 @@ 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 @@ -57,7 +94,6 @@ class PartTrackingInfo(models.Model): a new PartTrackingInfo object should be created. """ - part = models.ForeignKey(UniquePart, on_delete=models.CASCADE) - date = models.DateField(auto_now_add=True, - editable=False) + part = models.ForeignKey(UniquePart, on_delete=models.CASCADE, related_name='tracking_info') + date = models.DateField(auto_now_add=True, editable=False) notes = models.CharField(max_length=500) diff --git a/InvenTree/track/serializers.py b/InvenTree/track/serializers.py new file mode 100644 index 0000000000..6e958acbf3 --- /dev/null +++ b/InvenTree/track/serializers.py @@ -0,0 +1,27 @@ +from rest_framework import serializers + +from .models import UniquePart, PartTrackingInfo + + +class UniquePartSerializer(serializers.ModelSerializer): + + tracking_info = serializers.PrimaryKeyRelatedField(many=True, read_only=True) + + class Meta: + model = UniquePart + fields = ['pk', + 'part', + 'revision', + 'creation_date', + 'serial', + 'createdBy', + 'customer', + 'status', + 'tracking_info'] + + +class PartTrackingInfoSerializer(serializers.ModelSerializer): + + class Meta: + model = PartTrackingInfo + fields = '__all__' diff --git a/InvenTree/track/urls.py b/InvenTree/track/urls.py index 9cb3403af4..447c1c3d38 100644 --- a/InvenTree/track/urls.py +++ b/InvenTree/track/urls.py @@ -1,7 +1,19 @@ -from django.conf.urls import url +from django.conf.urls import url, include from . import views -urlpatterns = [ - url(r'^$', views.index, name='index') +infopatterns = [ + url(r'^(?P[0-9]+)/?$', views.PartTrackingDetail.as_view()), + + url(r'^\?*[^/]*/?$', views.PartTrackingList.as_view()) +] + +urlpatterns = [ + url(r'info/?', include(infopatterns)), + + # Detail for a single unique part + url(r'^(?P[0-9]+)$', views.UniquePartDetail.as_view()), + + # List all unique parts, with optional filters + url(r'^\?*[^/]*/?$', views.UniquePartList.as_view()), ] diff --git a/InvenTree/track/views.py b/InvenTree/track/views.py index 0a26c7bf12..436e98344e 100644 --- a/InvenTree/track/views.py +++ b/InvenTree/track/views.py @@ -1,5 +1,75 @@ -from django.http import HttpResponse +import django_filters + +from rest_framework import generics, permissions + +from .models import UniquePart, PartTrackingInfo +from .serializers import UniquePartSerializer, PartTrackingInfoSerializer -def index(request): - return HttpResponse("This is the Tracking page") +class UniquePartDetail(generics.RetrieveUpdateDestroyAPIView): + + queryset = UniquePart.objects.all() + serializer_class = UniquePartSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class UniquePartFilter(django_filters.rest_framework.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') + + class Meta: + model = UniquePart + fields = ['serial', ] + + +class UniquePartList(generics.ListCreateAPIView): + + serializer_class = UniquePartSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + filter_backends = (django_filters.rest_framework.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): + + queryset = PartTrackingInfo.objects.all() + serializer_class = PartTrackingInfoSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class PartTrackingList(generics.ListCreateAPIView): + + 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