From 7efb4c21d3c9d4e9e3a8ae128661e0e6de600c31 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 16 Apr 2017 17:50:28 +1000 Subject: [PATCH 1/2] Added ProjectRun API --- InvenTree/InvenTree/urls.py | 3 +- InvenTree/project/models.py | 2 +- InvenTree/project/serializers.py | 14 ++++++- InvenTree/project/urls.py | 9 +++++ InvenTree/project/views.py | 69 +++++++++++++++++++++++++------- 5 files changed, 79 insertions(+), 18 deletions(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index bab9e71793..e50470dc1c 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -5,7 +5,7 @@ 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, stock_track_urls -from project.urls import prj_urls, prj_part_urls, prj_cat_urls +from project.urls import prj_urls, prj_part_urls, prj_cat_urls, prj_run_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 @@ -39,6 +39,7 @@ apipatterns = [ url(r'^project/', include(prj_urls)), url(r'^project-category/', include(prj_cat_urls)), url(r'^project-part/', include(prj_part_urls)), + url(r'^project-run/', include(prj_run_urls)), ] urlpatterns = [ diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index 99228deb22..a36be41562 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -93,4 +93,4 @@ class ProjectRun(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)]) - run_date = models.DateField(auto_now_add=True) + run_date = models.DateField(blank=True, null=True) diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py index 1b854da4f1..2088fc5f52 100644 --- a/InvenTree/project/serializers.py +++ b/InvenTree/project/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import ProjectCategory, Project, ProjectPart +from .models import ProjectCategory, Project, ProjectPart, ProjectRun class ProjectPartSerializer(serializers.HyperlinkedModelSerializer): @@ -33,3 +33,15 @@ class ProjectCategorySerializer(serializers.HyperlinkedModelSerializer): 'description', 'parent', 'path') + + +class ProjectRunSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = ProjectRun + fields = ('url', + 'project', + 'quantity', + 'run_date') + + read_only_fields = ('run_date',) diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py index ad150a00ca..e95a78fd82 100644 --- a/InvenTree/project/urls.py +++ b/InvenTree/project/urls.py @@ -28,3 +28,12 @@ prj_urls = [ url(r'^\?.*/?$', views.ProjectList.as_view()), url(r'^$', views.ProjectList.as_view()) ] + +prj_run_urls = [ + # Individual project URL + url(r'^(?P[0-9]+)/?$', views.ProjectRunDetail.as_view(), name='projectrun-detail'), + + # List of all projects + url(r'^\?.*/?$', views.ProjectRunList.as_view()), + url(r'^$', views.ProjectRunList.as_view()) +] diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index 593407ae63..4aa12efa2b 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -2,10 +2,11 @@ from django_filters.rest_framework import FilterSet, DjangoFilterBackend from rest_framework import generics, permissions from InvenTree.models import FilterChildren -from .models import ProjectCategory, Project, ProjectPart +from .models import ProjectCategory, Project, ProjectPart, ProjectRun from .serializers import ProjectSerializer from .serializers import ProjectCategorySerializer from .serializers import ProjectPartSerializer +from .serializers import ProjectRunSerializer class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): @@ -96,6 +97,13 @@ class ProjectCategoryList(generics.ListCreateAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) +class ProjectPartFilter(FilterSet): + + class Meta: + model = ProjectPart + fields = ['project', 'part'] + + class ProjectPartsList(generics.ListCreateAPIView): """ @@ -109,20 +117,9 @@ class ProjectPartsList(generics.ListCreateAPIView): serializer_class = ProjectPartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - def get_queryset(self): - parts = ProjectPart.objects.all() - params = self.request.query_params - - project_id = params.get('project', None) - if project_id: - parts = parts.filter(project=project_id) - - part_id = params.get('part', None) - if part_id: - parts = parts.filter(part=part_id) - - return parts + queryset = ProjectPart.objects.all() + filter_backends = (DjangoFilterBackend,) + filter_class = ProjectPartFilter class ProjectPartDetail(generics.RetrieveUpdateDestroyAPIView): @@ -142,3 +139,45 @@ class ProjectPartDetail(generics.RetrieveUpdateDestroyAPIView): queryset = ProjectPart.objects.all() serializer_class = ProjectPartSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class ProjectRunDetail(generics.RetrieveUpdateDestroyAPIView): + """ + + get: + Return a single ProjectRun + + post: + Update a ProjectRun + + delete: + Remove a ProjectRun + """ + + queryset = ProjectRun.objects.all() + serializer_class = ProjectRunSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class ProjectRunFilter(FilterSet): + + class Meta: + model = ProjectRun + fields = ['project'] + + +class ProjectRunList(generics.ListCreateAPIView): + """ + + get: + Return a list of all ProjectRun objects + + post: + Create a new ProjectRun object + """ + + queryset = ProjectRun.objects.all() + serializer_class = ProjectRunSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + filter_backends = (DjangoFilterBackend,) + filter_class = ProjectRunFilter From 2c28ef6b3ce60b31de6cfa75e4f91139a53d6d64 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 20 Apr 2017 22:08:27 +1000 Subject: [PATCH 2/2] Added update endpoints for StockItem - Stocktake - Take-Stock --- InvenTree/stock/models.py | 63 +++++++++++++++++++++++++++++++++- InvenTree/stock/serializers.py | 14 +++++++- InvenTree/stock/urls.py | 12 +++++-- InvenTree/stock/views.py | 35 +++++++++++++++++-- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 0c847e80d5..5154bfe1a1 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -7,6 +7,8 @@ from supplier.models import SupplierPart from part.models import Part from InvenTree.models import InvenTreeTree +from datetime import datetime + class StockLocation(InvenTreeTree): """ Organization tree for StockItem objects @@ -26,7 +28,7 @@ class StockItem(models.Model): updated = models.DateField(auto_now=True) # last time the stock was checked / counted - last_checked = models.DateField(blank=True, null=True) + stocktake_date = models.DateField(blank=True, null=True) review_needed = models.BooleanField(default=False) @@ -59,6 +61,65 @@ class StockItem(models.Model): # If stock item is incoming, an (optional) ETA field expected_arrival = models.DateField(null=True, blank=True) + infinite = models.BooleanField(default=False) + + def stocktake(self, count): + """ Perform item stocktake. + When the quantity of an item is counted, + record the date of stocktake + """ + + count = int(count) + + if count < 0 or self.infinite: + return + + self.quantity = count + self.stocktake_date = datetime.now().date() + self.save() + + def take_stock(self, amount): + """ Take items from stock + This function can be called by initiating a ProjectRun, + or by manually taking the items from the stock location + """ + + if self.infinite: + return + + amount = int(amount) + if amount < 0: + raise ValueError("Stock amount must be positive") + + q = self.quantity - amount + + if q < 0: + q = 0 + + self.quantity = q + self.save() + + def add_stock(self, amount): + """ Add items to stock + This function can be called by initiating a ProjectRun, + or by manually adding the items to the stock location + """ + + amount = int(amount) + + if self.infinite or amount == 0: + return + + amount = int(amount) + + q = self.quantity + amount + if q < 0: + q = 0 + + self.quantity = q + self.save() + + def __str__(self): return "{n} x {part} @ {loc}".format( n=self.quantity, diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index f0ba1c805d..e73be8531b 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -17,10 +17,22 @@ class StockItemSerializer(serializers.HyperlinkedModelSerializer): 'status', 'notes', 'updated', - 'last_checked', + 'stocktake_date', 'review_needed', 'expected_arrival') + """ These fields are read-only in this context. + They can be updated by accessing the appropriate API endpoints + """ + read_only_fields = ('stocktake_date', 'quantity',) + + +class StockQuantitySerializer(serializers.ModelSerializer): + + class Meta: + model = StockItem + fields = ('quantity',) + class LocationSerializer(serializers.HyperlinkedModelSerializer): """ Detailed information about a stock location diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 068f4f4d65..f8086c7eed 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -1,10 +1,18 @@ -from django.conf.urls import url +from django.conf.urls import url, include from . import views +stock_endpoints = [ + url(r'^$', views.StockDetail.as_view(), name='stockitem-detail'), + + url(r'^stocktake/?$', views.StockStocktakeEndpoint.as_view(), name='stockitem-stocktake'), + + url(r'^add-stock/?$', views.AddStockEndpoint.as_view(), name='stockitem-add-stock'), +] + stock_urls = [ # Detail for a single stock item - url(r'^(?P[0-9]+)/?$', views.StockDetail.as_view(), name='stockitem-detail'), + url(r'^(?P[0-9]+)/', include(stock_endpoints)), # List all stock items, with optional filters url(r'^\?.*/?$', views.StockList.as_view()), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index a0a9cf4beb..26d6aa1e74 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1,11 +1,12 @@ from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters import NumberFilter -from rest_framework import generics, permissions +from rest_framework import generics, permissions, response # from InvenTree.models import FilterChildren from .models import StockLocation, StockItem, StockTracking -from .serializers import StockItemSerializer, LocationSerializer, StockTrackingSerializer +from .serializers import StockItemSerializer, StockQuantitySerializer +from .serializers import LocationSerializer, StockTrackingSerializer class StockDetail(generics.RetrieveUpdateDestroyAPIView): @@ -53,6 +54,36 @@ class StockList(generics.ListCreateAPIView): filter_class = StockFilter +class StockStocktakeEndpoint(generics.UpdateAPIView): + + queryset = StockItem.objects.all() + serializer_class = StockQuantitySerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + def update(self, request, *args, **kwargs): + object = self.get_object() + object.stocktake(request.data['quantity']) + + serializer = self.get_serializer(object) + + return response.Response(serializer.data) + + +class AddStockEndpoint(generics.UpdateAPIView): + + queryset = StockItem.objects.all() + serializer_class = StockQuantitySerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + def update(self, request, *args, **kwargs): + object = self.get_object() + object.add_stock(request.data['quantity']) + + serializer = self.get_serializer(object) + + return response.Response(serializer.data) + + class LocationDetail(generics.RetrieveUpdateDestroyAPIView): """