InvenTree/InvenTree/stock/api.py
Oliver Walters a9fbbc3a37 Remove references to get_object_or_404
- Properly handle DoesNotExist errors
2019-04-28 11:09:19 +10:00

374 lines
10 KiB
Python

"""
JSON API for the Stock app
"""
from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from django_filters import NumberFilter
from django.conf.urls import url, include
from django.db.models import Q
from .models import StockLocation, StockItem
from .models import StockItemTracking
from .serializers import StockItemSerializer, StockQuantitySerializer
from .serializers import LocationSerializer
from .serializers import StockTrackingSerializer
from InvenTree.views import TreeSerializer
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import generics, response, filters, permissions
class StockCategoryTree(TreeSerializer):
title = 'Stock'
model = StockLocation
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object
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(FilterSet):
""" FilterSet for advanced stock filtering.
Allows greater-than / less-than filtering for stock quantity
"""
min_stock = NumberFilter(name='quantity', lookup_expr='gte')
max_stock = NumberFilter(name='quantity', lookup_expr='lte')
class Meta:
model = StockItem
fields = ['quantity', 'part', 'location']
class StockStocktake(APIView):
""" Stocktake API endpoint provides stock update of multiple items simultaneously.
The 'action' field tells the type of stock action to perform:
- stocktake: Count the stock item(s)
- remove: Remove the quantity provided from stock
- add: Add the quantity provided from stock
"""
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
def post(self, request, *args, **kwargs):
if 'action' not in request.data:
raise ValidationError({'action': 'Stocktake action must be provided'})
action = request.data['action']
ACTIONS = ['stocktake', 'remove', 'add']
if action not in ACTIONS:
raise ValidationError({'action': 'Action must be one of ' + ','.join(ACTIONS)})
elif 'items[]' not in request.data:
raise ValidationError({'items[]:' 'Request must contain list of items'})
items = []
# Ensure each entry is valid
for entry in request.data['items[]']:
if 'pk' not in entry:
raise ValidationError({'pk': 'Each entry must contain pk field'})
elif 'quantity' not in entry:
raise ValidationError({'quantity': 'Each entry must contain quantity field'})
item = {}
try:
item['item'] = StockItem.objects.get(pk=entry['pk'])
except StockItem.DoesNotExist:
raise ValidationError({'pk': 'No matching StockItem found for pk={pk}'.format(pk=entry['pk'])})
try:
item['quantity'] = int(entry['quantity'])
except ValueError:
raise ValidationError({'quantity': 'Quantity must be an integer'})
if item['quantity'] < 0:
raise ValidationError({'quantity': 'Quantity must be >= 0'})
items.append(item)
# Stocktake notes
notes = ''
if 'notes' in request.data:
notes = request.data['notes']
n = 0
for item in items:
quantity = int(item['quantity'])
if action == u'stocktake':
if item['item'].stocktake(quantity, request.user, notes=notes):
n += 1
elif action == u'remove':
if item['item'].take_stock(quantity, request.user, notes=notes):
n += 1
elif action == u'add':
if item['item'].add_stock(quantity, request.user, notes=notes):
n += 1
return Response({'success': 'Updated stock for {n} items'.format(n=n)})
class StockMove(APIView):
""" API endpoint for performing stock movements """
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
def post(self, request, *args, **kwargs):
data = request.data
if u'location' not in data:
raise ValidationError({'location': 'Destination must be specified'})
loc_id = data.get(u'location')
try:
location = StockLocation.objects.get(pk=loc_id)
except StockLocation.DoesNotExist:
raise ValidationError({'location': 'Location does not exist'})
if u'parts[]' not in data:
raise ValidationError({'parts[]': 'Parts list must be specified'})
part_list = data.get(u'parts[]')
parts = []
errors = []
if u'notes' not in data:
errors.append({'notes': 'Notes field must be supplied'})
for pid in part_list:
try:
part = StockItem.objects.get(pk=pid)
parts.append(part)
except StockItem.DoesNotExist:
errors.append({'part': 'Part {id} does not exist'.format(id=pid)})
if len(errors) > 0:
raise ValidationError(errors)
n = 0
for part in parts:
if part.move(location, data.get('notes'), request.user):
n += 1
return Response({'success': 'Moved {n} parts to {loc}'.format(
n=n,
loc=str(location)
)})
class StockLocationList(generics.ListCreateAPIView):
""" API endpoint for list view of StockLocation objects:
- GET: Return list of StockLocation objects
- POST: Create a new StockLocation
"""
queryset = StockLocation.objects.all()
serializer_class = LocationSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
'parent',
]
class StockList(generics.ListCreateAPIView):
""" API endpoint for list view of Stock objects
- GET: Return a list of all StockItem objects (with optional query filters)
- POST: Create a new StockItem
"""
def get_queryset(self):
"""
If the query includes a particular location,
we may wish to also request stock items from all child locations.
This is set by the optional param 'include_child_categories'
"""
# Does the client wish to filter by category?
loc_id = self.request.query_params.get('location', None)
# Start with all objects
stock_list = StockItem.objects.all()
if loc_id:
try:
location = StockLocation.objects.get(pk=loc_id)
# Filter by the supplied category
flt = Q(location=loc_id)
if self.request.query_params.get('include_child_locations', None):
childs = location.getUniqueChildren()
for child in childs:
# Ignore the top-level category (already filtered!)
if str(child) == str(loc_id):
continue
flt |= Q(location=child)
stock_list = stock_list.filter(flt)
except StockLocation.DoesNotExist:
pass
return stock_list
serializer_class = StockItemSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
'part',
'uuid',
'supplier_part',
'customer',
'belongs_to',
# 'status' TODO - There are some issues filtering based on an enumeration field
]
class StockStocktakeEndpoint(generics.UpdateAPIView):
""" API endpoint for performing stocktake """
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'], request.user)
serializer = self.get_serializer(object)
return response.Response(serializer.data)
class StockTrackingList(generics.ListCreateAPIView):
""" API endpoint for list view of StockItemTracking objects.
StockItemTracking objects are read-only
(they are created by internal model functionality)
- GET: Return list of StockItemTracking objects
"""
queryset = StockItemTracking.objects.all()
serializer_class = StockTrackingSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
'item',
'user',
]
ordering = '-date'
ordering_fields = [
'date',
]
search_fields = [
'title',
'notes',
]
class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of StockLocation object
- GET: Return a single StockLocation object
- PATCH: Update a StockLocation object
- DELETE: Remove a StockLocation object
"""
queryset = StockLocation.objects.all()
serializer_class = LocationSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
stock_endpoints = [
url(r'^$', StockDetail.as_view(), name='stockitem-detail'),
]
location_endpoints = [
url(r'^$', LocationDetail.as_view(), name='stocklocation-detail'),
]
stock_api_urls = [
url(r'location/?', StockLocationList.as_view(), name='api-location-list'),
url(r'location/(?P<pk>\d+)/', include(location_endpoints)),
url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'),
url(r'move/?', StockMove.as_view(), name='api-stock-move'),
url(r'track/?', StockTrackingList.as_view(), name='api-stock-track'),
url(r'^tree/?', StockCategoryTree.as_view(), name='api-stock-tree'),
# Detail for a single stock item
url(r'^(?P<pk>\d+)/', include(stock_endpoints)),
url(r'^.*$', StockList.as_view(), name='api-stock-list'),
]