diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index b71aaecc61..550a3c3a85 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -28,7 +28,7 @@ class BuildSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() - overdue = serializers.BooleanField() + overdue = serializers.BooleanField(required=False, read_only=True) @staticmethod def annotate_queryset(queryset): diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index f0505728f6..5463feb26f 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -181,7 +181,7 @@ class SalesOrderSerializer(InvenTreeModelSerializer): status_text = serializers.CharField(source='get_status_display', read_only=True) - overdue = serializers.BooleanField() + overdue = serializers.BooleanField(required=False, read_only=True) class Meta: model = SalesOrder diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index f6d9d246db..7d6c09707b 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -134,6 +134,7 @@ fields: name: 'Red chair' variant_of: 10000 + IPN: "R.CH" trackable: true category: 7 tree_id: 1 diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml index 719d8a34ce..45a5f5dd7f 100644 --- a/InvenTree/stock/fixtures/stock.yaml +++ b/InvenTree/stock/fixtures/stock.yaml @@ -69,7 +69,7 @@ part: 25 batch: 'ABCDE' location: 7 - quantity: 3 + quantity: 0 level: 0 tree_id: 0 lft: 0 @@ -220,6 +220,7 @@ tree_id: 0 lft: 0 rght: 0 + expiry_date: "1990-10-10" - model: stock.stockitem pk: 521 @@ -232,6 +233,7 @@ tree_id: 0 lft: 0 rght: 0 + status: 60 - model: stock.stockitem pk: 522 @@ -243,4 +245,6 @@ level: 0 tree_id: 0 lft: 0 - rght: 0 \ No newline at end of file + rght: 0 + expiry_date: "1990-10-10" + status: 70 \ No newline at end of file diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 730018d98c..9db1d95dae 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -135,7 +135,7 @@ class StockItemSerializer(InvenTreeModelSerializer): allocated = serializers.FloatField(source='allocation_count', required=False) - expired = serializers.BooleanField() + expired = serializers.BooleanField(required=False, read_only=True) serial = serializers.CharField(required=False) diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index a34e895ed8..ecca10ba0a 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -1,11 +1,21 @@ +""" +Unit testing for the Stock API +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta + from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model from InvenTree.helpers import addUserPermissions +from InvenTree.status_codes import StockStatus -from .models import StockLocation +from .models import StockItem, StockLocation class StockAPITestCase(APITestCase): @@ -76,6 +86,170 @@ class StockLocationTest(StockAPITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) +class StockItemListTest(StockAPITestCase): + """ + Tests for the StockItem API LIST endpoint + """ + + list_url = reverse('api-stock-list') + + def get_stock(self, **kwargs): + """ + Filter stock and return JSON object + """ + + response = self.client.get(self.list_url, format='json', data=kwargs) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Return JSON-ified data + return response.data + + def test_get_stock_list(self): + """ + List *all* StockItem objects. + """ + + response = self.get_stock() + + self.assertEqual(len(response), 19) + + def test_filter_by_part(self): + """ + Filter StockItem by Part reference + """ + + response = self.get_stock(part=25) + + self.assertEqual(len(response), 7) + + response = self.get_stock(part=10004) + + self.assertEqual(len(response), 12) + + def test_filter_by_IPN(self): + """ + Filter StockItem by IPN reference + """ + + response = self.get_stock(IPN="R.CH") + self.assertEqual(len(response), 3) + + def test_filter_by_location(self): + """ + Filter StockItem by StockLocation reference + """ + + response = self.get_stock(location=5) + self.assertEqual(len(response), 1) + + response = self.get_stock(location=1, cascade=0) + self.assertEqual(len(response), 0) + + response = self.get_stock(location=1, cascade=1) + self.assertEqual(len(response), 2) + + response = self.get_stock(location=7) + self.assertEqual(len(response), 16) + + def test_filter_by_depleted(self): + """ + Filter StockItem by depleted status + """ + + response = self.get_stock(depleted=1) + self.assertEqual(len(response), 1) + + response = self.get_stock(depleted=0) + self.assertEqual(len(response), 18) + + def test_filter_by_in_stock(self): + """ + Filter StockItem by 'in stock' status + """ + + response = self.get_stock(in_stock=1) + self.assertEqual(len(response), 16) + + response = self.get_stock(in_stock=0) + self.assertEqual(len(response), 3) + + def test_filter_by_status(self): + """ + Filter StockItem by 'status' field + """ + + codes = { + StockStatus.OK: 17, + StockStatus.DESTROYED: 1, + StockStatus.LOST: 1, + StockStatus.DAMAGED: 0, + StockStatus.REJECTED: 0, + } + + for code in codes.keys(): + num = codes[code] + + response = self.get_stock(status=code) + self.assertEqual(len(response), num) + + def test_filter_by_batch(self): + """ + Filter StockItem by batch code + """ + + response = self.get_stock(batch='B123') + self.assertEqual(len(response), 1) + + def test_filter_by_serialized(self): + """ + Filter StockItem by serialized status + """ + + response = self.get_stock(serialized=1) + self.assertEqual(len(response), 12) + + for item in response: + self.assertIsNotNone(item['serial']) + + response = self.get_stock(serialized=0) + self.assertEqual(len(response), 7) + + for item in response: + self.assertIsNone(item['serial']) + + def test_filter_by_expired(self): + """ + Filter StockItem by expiry status + """ + + response = self.get_stock(expired=1) + self.assertEqual(len(response), 1) + + for item in response: + self.assertTrue(item['expired']) + + response = self.get_stock(expired=0) + self.assertEqual(len(response), 18) + + for item in response: + self.assertFalse(item['expired']) + + # Mark some other stock items as expired + today = datetime.now().date() + + for pk in [510, 511, 512]: + item = StockItem.objects.get(pk=pk) + item.expiry_date = today - timedelta(days=pk) + item.save() + + response = self.get_stock(expired=1) + self.assertEqual(len(response), 4) + + response = self.get_stock(expired=0) + self.assertEqual(len(response), 15) + + class StockItemTest(StockAPITestCase): """ Series of API tests for the StockItem API @@ -94,10 +268,6 @@ class StockItemTest(StockAPITestCase): StockLocation.objects.create(name='B', description='location b', parent=top) StockLocation.objects.create(name='C', description='location c', parent=top) - def test_get_stock_list(self): - response = self.client.get(self.list_url, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_create_default_location(self): """ Test the default location functionality, diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 274a1328fe..b0b05b6326 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -177,8 +177,10 @@ class StockTest(TestCase): # There should be 9000 screws in stock self.assertEqual(part.total_stock, 9000) - # There should be 18 widgets in stock - self.assertEqual(StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 19) + # There should be 16 widgets "in stock" + self.assertEqual( + StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 16 + ) def test_delete_location(self):