mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
608 lines
17 KiB
Python
608 lines
17 KiB
Python
"""
|
|
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 common.models import InvenTreeSetting
|
|
|
|
from .models import StockItem, StockLocation
|
|
|
|
|
|
class StockAPITestCase(APITestCase):
|
|
|
|
fixtures = [
|
|
'category',
|
|
'part',
|
|
'company',
|
|
'location',
|
|
'supplier_part',
|
|
'stock',
|
|
'stock_tests',
|
|
]
|
|
|
|
def setUp(self):
|
|
# Create a user for auth
|
|
user = get_user_model()
|
|
|
|
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
|
|
|
self.user.is_staff = True
|
|
self.user.save()
|
|
|
|
# Add the necessary permissions to the user
|
|
perms = [
|
|
'view_stockitemtestresult',
|
|
'change_stockitemtestresult',
|
|
'add_stockitemtestresult',
|
|
'add_stocklocation',
|
|
'change_stocklocation',
|
|
'add_stockitem',
|
|
'change_stockitem',
|
|
]
|
|
|
|
addUserPermissions(self.user, perms)
|
|
|
|
self.client.login(username='testuser', password='password')
|
|
|
|
def doPost(self, url, data={}):
|
|
response = self.client.post(url, data=data, format='json')
|
|
|
|
return response
|
|
|
|
|
|
class StockLocationTest(StockAPITestCase):
|
|
"""
|
|
Series of API tests for the StockLocation API
|
|
"""
|
|
list_url = reverse('api-location-list')
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Add some stock locations
|
|
StockLocation.objects.create(name='top', description='top category')
|
|
|
|
def test_list(self):
|
|
# Check that we can request the StockLocation list
|
|
response = self.client.get(self.list_url, format='json')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertGreaterEqual(len(response.data), 1)
|
|
|
|
def test_add(self):
|
|
# Check that we can add a new StockLocation
|
|
data = {
|
|
'parent': 1,
|
|
'name': 'Location',
|
|
'description': 'Another location for stock'
|
|
}
|
|
response = self.client.post(self.list_url, data, format='json')
|
|
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
|
|
"""
|
|
|
|
# First, we can assume that the 'stock expiry' feature is disabled
|
|
response = self.get_stock(expired=1)
|
|
self.assertEqual(len(response), 19)
|
|
|
|
# Now, ensure that the expiry date feature is enabled!
|
|
InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user)
|
|
|
|
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
|
|
"""
|
|
|
|
list_url = reverse('api-stock-list')
|
|
|
|
def detail_url(self, pk):
|
|
return reverse('api-stock-detail', kwargs={'pk': pk})
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
# Create some stock locations
|
|
top = StockLocation.objects.create(name='A', description='top')
|
|
|
|
StockLocation.objects.create(name='B', description='location b', parent=top)
|
|
StockLocation.objects.create(name='C', description='location c', parent=top)
|
|
|
|
def test_create_default_location(self):
|
|
"""
|
|
Test the default location functionality,
|
|
if a 'location' is not specified in the creation request.
|
|
"""
|
|
|
|
# The part 'R_4K7_0603' (pk=4) has a default location specified
|
|
|
|
response = self.client.post(
|
|
self.list_url,
|
|
data={
|
|
'part': 4,
|
|
'quantity': 10
|
|
}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
self.assertEqual(response.data['location'], 2)
|
|
|
|
# What if we explicitly set the location to a different value?
|
|
|
|
response = self.client.post(
|
|
self.list_url,
|
|
data={
|
|
'part': 4,
|
|
'quantity': 20,
|
|
'location': 1,
|
|
}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
self.assertEqual(response.data['location'], 1)
|
|
|
|
# And finally, what if we set the location explicitly to None?
|
|
|
|
response = self.client.post(
|
|
self.list_url,
|
|
data={
|
|
'part': 4,
|
|
'quantity': 20,
|
|
'location': '',
|
|
}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
self.assertEqual(response.data['location'], None)
|
|
|
|
def test_stock_item_create(self):
|
|
"""
|
|
Test creation of a StockItem via the API
|
|
"""
|
|
|
|
# POST with an empty part reference
|
|
|
|
response = self.client.post(
|
|
self.list_url,
|
|
data={
|
|
'quantity': 10,
|
|
'location': 1
|
|
}
|
|
)
|
|
|
|
self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# POST with an invalid part reference
|
|
|
|
response = self.client.post(
|
|
self.list_url,
|
|
data={
|
|
'quantity': 10,
|
|
'location': 1,
|
|
'part': 10000000,
|
|
}
|
|
)
|
|
|
|
self.assertContains(response, 'does not exist', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# POST without quantity
|
|
response = self.client.post(
|
|
self.list_url,
|
|
data={
|
|
'part': 1,
|
|
'location': 1,
|
|
}
|
|
)
|
|
|
|
self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# POST with quantity and part and location
|
|
response = self.client.post(
|
|
self.list_url,
|
|
data={
|
|
'part': 1,
|
|
'location': 1,
|
|
'quantity': 10,
|
|
}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
def test_default_expiry(self):
|
|
"""
|
|
Test that the "default_expiry" functionality works via the API.
|
|
|
|
- If an expiry_date is specified, use that
|
|
- Otherwise, check if the referenced part has a default_expiry defined
|
|
- If so, use that!
|
|
- Otherwise, no expiry
|
|
|
|
Notes:
|
|
- Part <25> has a default_expiry of 10 days
|
|
|
|
"""
|
|
|
|
# First test - create a new StockItem without an expiry date
|
|
data = {
|
|
'part': 4,
|
|
'quantity': 10,
|
|
}
|
|
|
|
response = self.client.post(self.list_url, data)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
self.assertIsNone(response.data['expiry_date'])
|
|
|
|
# Second test - create a new StockItem with an explicit expiry date
|
|
data['expiry_date'] = '2022-12-12'
|
|
|
|
response = self.client.post(self.list_url, data)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
self.assertIsNotNone(response.data['expiry_date'])
|
|
self.assertEqual(response.data['expiry_date'], '2022-12-12')
|
|
|
|
# Third test - create a new StockItem for a Part which has a default expiry time
|
|
data = {
|
|
'part': 25,
|
|
'quantity': 10
|
|
}
|
|
|
|
response = self.client.post(self.list_url, data)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
# Expected expiry date is 10 days in the future
|
|
expiry = datetime.now().date() + timedelta(10)
|
|
|
|
self.assertEqual(response.data['expiry_date'], expiry.isoformat())
|
|
|
|
|
|
class StocktakeTest(StockAPITestCase):
|
|
"""
|
|
Series of tests for the Stocktake API
|
|
"""
|
|
|
|
def test_action(self):
|
|
"""
|
|
Test each stocktake action endpoint,
|
|
for validation
|
|
"""
|
|
|
|
for endpoint in ['api-stock-count', 'api-stock-add', 'api-stock-remove']:
|
|
|
|
url = reverse(endpoint)
|
|
|
|
data = {}
|
|
|
|
# POST with a valid action
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, "must contain list", status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
data['items'] = [{
|
|
'no': 'aa'
|
|
}]
|
|
|
|
# POST without a PK
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# POST with a PK but no quantity
|
|
data['items'] = [{
|
|
'pk': 10
|
|
}]
|
|
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
data['items'] = [{
|
|
'pk': 1234
|
|
}]
|
|
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
data['items'] = [{
|
|
'pk': 1234,
|
|
'quantity': '10x0d'
|
|
}]
|
|
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
data['items'] = [{
|
|
'pk': 1234,
|
|
'quantity': "-1.234"
|
|
}]
|
|
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Test with a single item
|
|
data = {
|
|
'item': {
|
|
'pk': 1234,
|
|
'quantity': '10',
|
|
}
|
|
}
|
|
|
|
response = self.doPost(url, data)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
def test_transfer(self):
|
|
"""
|
|
Test stock transfers
|
|
"""
|
|
|
|
data = {
|
|
'item': {
|
|
'pk': 1234,
|
|
'quantity': 10,
|
|
},
|
|
'location': 1,
|
|
'notes': "Moving to a new location"
|
|
}
|
|
|
|
url = reverse('api-stock-transfer')
|
|
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, "Moved 1 parts to", status_code=status.HTTP_200_OK)
|
|
|
|
# Now try one which will fail due to a bad location
|
|
data['location'] = 'not a location'
|
|
|
|
response = self.doPost(url, data)
|
|
self.assertContains(response, 'Valid location must be specified', status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class StockTestResultTest(StockAPITestCase):
|
|
|
|
def get_url(self):
|
|
return reverse('api-stock-test-result-list')
|
|
|
|
def test_list(self):
|
|
|
|
url = self.get_url()
|
|
response = self.client.get(url)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertGreaterEqual(len(response.data), 4)
|
|
|
|
response = self.client.get(url, data={'stock_item': 105})
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertGreaterEqual(len(response.data), 4)
|
|
|
|
def test_post_fail(self):
|
|
# Attempt to post a new test result without specifying required data
|
|
|
|
url = self.get_url()
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data={
|
|
'test': 'A test',
|
|
'result': True,
|
|
},
|
|
format='json'
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
# This one should pass!
|
|
response = self.client.post(
|
|
url,
|
|
data={
|
|
'test': 'A test',
|
|
'stock_item': 105,
|
|
'result': True,
|
|
},
|
|
format='json'
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
def test_post(self):
|
|
# Test creation of a new test result
|
|
|
|
url = self.get_url()
|
|
|
|
response = self.client.get(url)
|
|
n = len(response.data)
|
|
|
|
data = {
|
|
'stock_item': 105,
|
|
'test': 'Checked Steam Valve',
|
|
'result': False,
|
|
'value': '150kPa',
|
|
'notes': 'I guess there was just too much pressure?',
|
|
}
|
|
|
|
response = self.client.post(url, data, format='json')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
response = self.client.get(url)
|
|
self.assertEqual(len(response.data), n + 1)
|
|
|
|
# And read out again
|
|
response = self.client.get(url, data={'test': 'Checked Steam Valve'})
|
|
|
|
self.assertEqual(len(response.data), 1)
|
|
|
|
test = response.data[0]
|
|
self.assertEqual(test['value'], '150kPa')
|
|
self.assertEqual(test['user'], self.user.pk)
|