diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 9e71cb3e86..bab9e71793 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -4,7 +4,7 @@ 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 stock.urls import stock_urls, stock_loc_urls, stock_track_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 @@ -16,6 +16,7 @@ apipatterns = [ # Stock URLs url(r'^stock/', include(stock_urls)), url(r'^stock-location/', include(stock_loc_urls)), + url(r'^stock-track/', include(stock_track_urls)), # Part URLs url(r'^part/', include(part_urls)), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f5daf0915a..b9d7e71c86 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -56,6 +56,8 @@ class Part(models.Model): class Meta: verbose_name = "Part" verbose_name_plural = "Parts" + unique_together = (("name", "category"), + ("IPN", "category")) @property def stock(self): @@ -94,7 +96,7 @@ class PartParameterTemplate(models.Model): ready to be copied for use with a given Part. A PartParameterTemplate can be optionally associated with a PartCategory """ - name = models.CharField(max_length=20) + name = models.CharField(max_length=20, unique=True) units = models.CharField(max_length=10, blank=True) # Parameter format @@ -136,32 +138,13 @@ class CategoryParameterLink(models.Model): class Meta: verbose_name = "Category Parameter" 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'] - - params = self.filter(part=part_id, template=template_id) - if len(params) > 0: - return params[0] - - return super(PartParameterManager, self).create(*args, **kwargs) + unique_together = ('category', 'template') 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) @@ -187,3 +170,4 @@ class PartParameter(models.Model): class Meta: verbose_name = "Part Parameter" verbose_name_plural = "Part Parameters" + unique_together = ('part', 'template') diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 48719e9cd5..1b4bd9a428 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -3,13 +3,13 @@ from rest_framework import serializers from .models import Part, PartCategory, PartParameter, PartParameterTemplate -class PartParameterSerializer(serializers.ModelSerializer): +class PartParameterSerializer(serializers.HyperlinkedModelSerializer): """ Serializer for a PartParameter """ class Meta: model = PartParameter - fields = ('pk', + fields = ('url', 'part', 'template', 'name', @@ -45,11 +45,11 @@ class PartCategorySerializer(serializers.HyperlinkedModelSerializer): 'path') -class PartTemplateSerializer(serializers.ModelSerializer): +class PartTemplateSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PartParameterTemplate - fields = ('pk', + fields = ('url', 'name', 'units', 'format') diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index 6cc8a01ce0..8038184215 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -31,44 +31,26 @@ class Project(models.Model): description = models.CharField(max_length=500, blank=True) category = models.ForeignKey(ProjectCategory, on_delete=models.CASCADE, related_name='projects') + class Meta: + unique_together = ('name', 'category') + def __str__(self): return self.name -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'] - - project_parts = self.filter(project=project_id, part=part_id) - if len(project_parts) > 0: - return project_parts[0] - - 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) + class Meta: + unique_together = ('part', 'project') + """ # TODO - Add overage model fields diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index dc5f412420..9984d1d74a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.utils.translation import ugettext as _ from django.db import models +from supplier.models import SupplierPart from part.models import Part from InvenTree.models import InvenTreeTree @@ -17,9 +18,8 @@ class StockLocation(InvenTreeTree): class StockItem(models.Model): - part = models.ForeignKey(Part, - on_delete=models.CASCADE, - related_name='locations') + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='locations') + supplier_part = models.ForeignKey(SupplierPart, blank=True, null=True, on_delete=models.SET_NULL) location = models.ForeignKey(StockLocation, on_delete=models.CASCADE) quantity = models.PositiveIntegerField() updated = models.DateField(auto_now=True) @@ -52,6 +52,8 @@ class StockItem(models.Model): default=ITEM_IN_STOCK, choices=ITEM_STATUS_CODES.items()) + notes = models.CharField(max_length=100, blank=True) + # If stock item is incoming, an (optional) ETA field expected_arrival = models.DateField(null=True, blank=True) @@ -60,3 +62,16 @@ class StockItem(models.Model): n=self.quantity, part=self.part.name, loc=self.location.name) + + +class StockTracking(models.Model): + """ Tracks a single movement of stock + - Used to track stock being taken from a location + - Used to track stock being added to a location + - "Pending" flag shows that stock WILL be taken / added + """ + + item = models.ForeignKey(StockItem, on_delete=models.CASCADE, related_name='tracking') + quantity = models.IntegerField() + pending = models.BooleanField(default=False) + when = models.DateTimeField(auto_now=True) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 5c69329f82..c7e958e4e9 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import StockItem, StockLocation +from .models import StockItem, StockLocation, StockTracking class StockItemSerializer(serializers.HyperlinkedModelSerializer): @@ -14,6 +14,7 @@ class StockItemSerializer(serializers.HyperlinkedModelSerializer): 'location', 'quantity', 'status', + 'notes', 'updated', 'last_checked', 'review_needed', @@ -31,3 +32,14 @@ class LocationSerializer(serializers.HyperlinkedModelSerializer): 'description', 'parent', 'path') + + +class StockTrackingSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = StockTracking + fields = ('url', + 'item', + 'quantity', + 'pending', + 'when') diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index bf58ddbb89..068f4f4d65 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -18,3 +18,11 @@ stock_loc_urls = [ url(r'^$', views.LocationList.as_view()) ] + +stock_track_urls = [ + url(r'^(?P[0-9]+)/?$', views.StockTrackingDetail.as_view(), name='stocktracking-detail'), + + url(r'^\?.*/?$', views.StockTrackingList.as_view()), + + url(r'^$', views.StockTrackingList.as_view()) +] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 6c19410fc4..026c9084ab 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -4,8 +4,8 @@ from django_filters import NumberFilter from rest_framework import generics, permissions # from InvenTree.models import FilterChildren -from .models import StockLocation, StockItem -from .serializers import StockItemSerializer, LocationSerializer +from .models import StockLocation, StockItem, StockTracking +from .serializers import StockItemSerializer, LocationSerializer, StockTrackingSerializer class StockDetail(generics.RetrieveUpdateDestroyAPIView): @@ -100,3 +100,49 @@ class LocationList(generics.ListCreateAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) filter_backends = (DjangoFilterBackend,) filter_class = StockLocationFilter + + +class StockTrackingDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single StockTracking object + + post: + Update a StockTracking object + + delete: + Remove a StockTracking object + + """ + + queryset = StockTracking.objects.all() + serializer_class = StockTrackingSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class StockTrackingFilter(FilterSet): + + item = NumberFilter(name='item', lookup_expr='exact') + + class Meta: + model = StockTracking + fields = ['item'] + + +class StockTrackingList(generics.ListCreateAPIView): + """ + + get: + Return a list of all StockTracking items + + post: + Create a new StockTracking item + + """ + + queryset = StockTracking.objects.all() + serializer_class = StockTrackingSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + filter_backends = (DjangoFilterBackend,) + filter_class = StockTrackingFilter diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 894621ea97..9f5067caf8 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -78,6 +78,9 @@ class SupplierPriceBreak(models.Model): quantity = models.PositiveIntegerField() cost = models.DecimalField(max_digits=10, decimal_places=3) + class Meta: + unique_together = ("part", "quantity") + def __str__(self): return "{mpn} - {cost}{currency} @ {quan}".format( mpn=self.part.MPN, diff --git a/InvenTree/track/models.py b/InvenTree/track/models.py index 445e6d30fc..b03cdae52c 100644 --- a/InvenTree/track/models.py +++ b/InvenTree/track/models.py @@ -12,8 +12,6 @@ class UniquePartManager(models.Manager): def create(self, *args, **kwargs): - print(kwargs) - part = kwargs.get('part', None) if not part.trackable: