From 80771beee91dc7c215f27c9384397d497a38531b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 15 Aug 2019 21:16:12 +1000 Subject: [PATCH 1/8] FIxtures for build testing --- InvenTree/build/fixtures/build.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 InvenTree/build/fixtures/build.yaml diff --git a/InvenTree/build/fixtures/build.yaml b/InvenTree/build/fixtures/build.yaml new file mode 100644 index 0000000000..d0fc30755a --- /dev/null +++ b/InvenTree/build/fixtures/build.yaml @@ -0,0 +1,21 @@ +# Construct build objects + +- model: build.build + fields: + part: 25 + batch: 'B1' + title: 'Building 7 parts' + quantity: 7 + notes: 'Some simple notes' + status: 10 # PENDING + creation_date: '2019-03-16' + +- model: build.build + fields: + part: 50 + title: 'Making things' + batch: 'B2' + status: 40 # COMPLETE + quantity: 21 + notes: 'Some more simple notes' + creation_date: '2019-03-16' \ No newline at end of file From 62f007529ea68352c1362e2c020ea06cde7c46c5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 15 Aug 2019 21:19:59 +1000 Subject: [PATCH 2/8] Bug fix in build views - Untested code path meant variable was not being set --- InvenTree/build/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index eb267064b9..7a1dceaf23 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -518,7 +518,9 @@ class BuildItemCreate(AjaxCreateView): part = Part.objects.get(pk=part_id) except Part.DoesNotExist: part = None - + else: + part = None + if build_id: try: build = Build.objects.get(pk=build_id) From cd05ed91aa3da0646bbf0362af08fe2e1e011a5a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 15 Aug 2019 21:35:16 +1000 Subject: [PATCH 3/8] More tests for Build API and views --- InvenTree/build/tests.py | 210 +++++++++++++++++++++++++++++++++++---- 1 file changed, 192 insertions(+), 18 deletions(-) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 36e8223a9b..6816b93fcd 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -2,6 +2,13 @@ from __future__ import unicode_literals from django.test import TestCase +from django.urls import reverse +from django.contrib.auth import get_user_model + +from rest_framework.test import APITestCase +from rest_framework import status + +import json from .models import Build from part.models import Part @@ -11,23 +18,20 @@ from InvenTree.status_codes import BuildStatus class BuildTestSimple(TestCase): + fixtures = [ + 'category', + 'part', + 'location', + 'build', + ] + def setUp(self): - part = Part.objects.create(name='Test part', - description='Simple description') + # Create a user for auth + User = get_user_model() + User.objects.create_user('testuser', 'test@testing.com', 'password') - Build.objects.create(part=part, - batch='B1', - status=BuildStatus.PENDING, - title='Building 7 parts', - quantity=7, - notes='Some simple notes') - - Build.objects.create(part=part, - batch='B2', - status=BuildStatus.COMPLETE, - title='Building 21 parts', - quantity=21, - notes='Some simple notes') + self.user = User.objects.get(username='testuser') + self.client.login(username='testuser', password='password') def test_build_objects(self): # Ensure the Build objects were correctly created @@ -36,7 +40,7 @@ class BuildTestSimple(TestCase): self.assertEqual(b.batch, 'B2') self.assertEqual(b.quantity, 21) - self.assertEqual(str(b), 'Build 21 x Test part - Simple description') + self.assertEqual(str(b), 'Build 21 x Orphan - A part without a category') def test_url(self): b1 = Build.objects.get(pk=1) @@ -62,13 +66,183 @@ class BuildTestSimple(TestCase): # TODO - Generate BOM for test part pass - def cancel_build(self): + def test_cancel_build(self): """ Test build cancellation function """ build = Build.objects.get(id=1) self.assertEqual(build.status, BuildStatus.PENDING) - build.cancelBuild() + build.cancelBuild(self.user) self.assertEqual(build.status, BuildStatus.CANCELLED) + + + +class TestBuildAPI(APITestCase): + """ + Series of tests for the Build DRF API + - Tests for Build API + - Tests for BuildItem API + """ + + fixtures = [ + 'category', + 'part', + 'location', + 'build', + ] + + def setUp(self): + # Create a user for auth + User = get_user_model() + User.objects.create_user('testuser', 'test@testing.com', 'password') + + self.client.login(username='testuser', password='password') + + def test_get_build_list(self): + """ Test that we can retrieve list of build objects """ + url = reverse('api-build-list') + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_get_build_item_list(self): + """ Test that we can retrieve list of BuildItem objects """ + url = reverse('api-build-item-list') + + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Test again, filtering by park ID + response = self.client.get(url, {'part': '1'}, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class TestBuildViews(TestCase): + """ Tests for Build app views """ + + fixtures = [ + 'category', + 'part', + 'location', + 'build', + ] + + def setUp(self): + super().setUp() + + # Create a user + User = get_user_model() + User.objects.create_user('username', 'user@email.com', 'password') + + self.client.login(username='username', password='password') + + def test_build_index(self): + """ test build index view """ + + response = self.client.get(reverse('build-index')) + self.assertEqual(response.status_code, 200) + + content = str(response.content) + + # Content should contain build titles + for build in Build.objects.all(): + self.assertIn(build.title, content) + + def test_build_detail(self): + """ Test the detail view for a Build object """ + + pk = 1 + + response = self.client.get(reverse('build-detail', args=(pk,))) + self.assertEqual(response.status_code, 200) + + build = Build.objects.get(pk=pk) + + content = str(response.content) + + self.assertIn(build.title, content) + + def test_build_create(self): + """ Test the build creation view (ajax form) """ + + url = reverse('build-create') + + # Create build without specifying part + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # Create build with valid part + response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # Create build with invalid part + response = self.client.get(url, {'part': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + def test_build_allocate(self): + """ Test the part allocation view for a Build """ + + url = reverse('build-allocate', args=(1,)) + + # Get the page normally + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # Get the page in editing mode + response = self.client.get(url, {'edit': 1}) + self.assertEqual(response.status_code, 200) + + def test_build_item_create(self): + """ Test the BuildItem creation view (ajax form) """ + + url = reverse('build-item-create') + + # Try without a part specified + response = self.client.get(url, {'build': 1,}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # Try with an invalid build ID + response = self.client.get(url, {'build': 9999,}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # Try with a valid part specified + response = self.client.get(url, {'build': 1, 'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # Try with an invalid part specified + response = self.client.get(url, {'build': 1, 'part': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + def test_build_item_edit(self): + """ Test the BuildItem edit view (ajax form) """ + + # TODO + # url = reverse('build-item-edit') + pass + + def test_build_cancel(self): + """ Test the build cancellation form """ + + url = reverse('build-cancel', args=(1,)) + + # Test without confirmation + response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + + self.assertFalse(data['form_valid']) + + b = Build.objects.get(pk=1) + self.assertEqual(b.status, 10) # Build status is still PENDING + + # Test with confirmation + response = self.client.post(url, {'confirm_cancel': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertTrue(data['form_valid']) + + b = Build.objects.get(pk=1) + self.assertEqual(b.status, 30) # Build status is now CANCELLED \ No newline at end of file From bed74f273c4d12a27102dd7cef05393dbb25c73b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 15 Aug 2019 21:47:26 +1000 Subject: [PATCH 4/8] Fix .coveragerc --- .coveragerc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index 948d835f41..b6b5f5a09d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,8 +4,8 @@ omit = # Do not run coverage on migration files */migrations/* InvenTree/manage.py - InvenTree/keygen.py - Inventree/InvenTree/middleware.py - Inventree/InvenTree/utils.py - Inventree/InvenTree/wsgi.py + InvenTree/secret.py + InvenTree/InvenTree/middleware.py + InvenTree/InvenTree/utils.py + InvenTree/InvenTree/wsgi.py InvenTree/users/apps.py \ No newline at end of file From 087492faf8a2b942e712a5ccfeac8e2abb5f9fa6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 15 Aug 2019 21:49:40 +1000 Subject: [PATCH 5/8] More build tests --- .coveragerc | 2 +- InvenTree/build/tests.py | 48 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index b6b5f5a09d..409c378cac 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,7 +4,7 @@ omit = # Do not run coverage on migration files */migrations/* InvenTree/manage.py - InvenTree/secret.py + InvenTree/setup.py InvenTree/InvenTree/middleware.py InvenTree/InvenTree/utils.py InvenTree/InvenTree/wsgi.py diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 6816b93fcd..0092cab091 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -221,6 +221,32 @@ class TestBuildViews(TestCase): # url = reverse('build-item-edit') pass + def test_build_complete(self): + """ Test the build completion form """ + + url = reverse('build-complete', args=(1,)) + + # Test without confirmation + response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertFalse(data['form_valid']) + + # Test with confirmation, valid location + response = self.client.post(url, {'confirm': 1, 'location': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertTrue(data['form_valid']) + + # Test with confirmation, invalid location + response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertFalse(data['form_valid']) + def test_build_cancel(self): """ Test the build cancellation form """ @@ -231,7 +257,6 @@ class TestBuildViews(TestCase): self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertFalse(data['form_valid']) b = Build.objects.get(pk=1) @@ -245,4 +270,23 @@ class TestBuildViews(TestCase): self.assertTrue(data['form_valid']) b = Build.objects.get(pk=1) - self.assertEqual(b.status, 30) # Build status is now CANCELLED \ No newline at end of file + self.assertEqual(b.status, 30) # Build status is now CANCELLED + + def test_build_unallocate(self): + """ Test the build unallocation view (ajax form) """ + + url = reverse('build-unallocate', args=(1,)) + + # Test without confirmation + response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertFalse(data['form_valid']) + + # Test with confirmation + response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + self.assertTrue(data['form_valid']) From 9f5325d61f72046abffcc0bf3dbcdacc1c1c0e3c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 15 Aug 2019 21:50:42 +1000 Subject: [PATCH 6/8] PEP fixes --- InvenTree/build/tests.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 0092cab091..06eafa41ab 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -11,7 +11,6 @@ from rest_framework import status import json from .models import Build -from part.models import Part from InvenTree.status_codes import BuildStatus @@ -78,7 +77,6 @@ class BuildTestSimple(TestCase): self.assertEqual(build.status, BuildStatus.CANCELLED) - class TestBuildAPI(APITestCase): """ Series of tests for the Build DRF API @@ -199,11 +197,11 @@ class TestBuildViews(TestCase): url = reverse('build-item-create') # Try without a part specified - response = self.client.get(url, {'build': 1,}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.get(url, {'build': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - + # Try with an invalid build ID - response = self.client.get(url, {'build': 9999,}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.get(url, {'build': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) # Try with a valid part specified From 41bfdc14325a0e37285f8e06ddc33f91656f0c0a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 15 Aug 2019 21:57:34 +1000 Subject: [PATCH 7/8] Enforce usage of sqlite3 for running tests - Simplifies tests by creating a database in memory - Does not affect the user setup at all --- InvenTree/InvenTree/settings.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 66f22be9e4..553768beeb 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -159,16 +159,28 @@ WSGI_APPLICATION = 'InvenTree.wsgi.application' DATABASES = {} -# Database backend selection -if 'database' in CONFIG: - DATABASES['default'] = CONFIG['database'] -else: - eprint("Warning: Database backend not specified - using default (sqlite)") +""" +When running unit tests, enforce usage of sqlite3 database, +so that the tests can be run in RAM without any setup requirements +""" +if 'test' in sys.argv: + eprint('Running tests - Using sqlite3 memory database') DATABASES['default'] = { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'inventree_db.sqlite3'), + 'NAME': 'test_db.sqlite3' } +# Database backend selection +else: + if 'database' in CONFIG: + DATABASES['default'] = CONFIG['database'] + else: + eprint("Warning: Database backend not specified - using default (sqlite)") + DATABASES['default'] = { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'inventree_db.sqlite3'), + } + CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', From 3d9fffd8ccc03230b0ee985d8d7398862312b726 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 15 Aug 2019 22:01:57 +1000 Subject: [PATCH 8/8] Update README.md Added logo --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40a372b394..962f8186c9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) [![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree) -# InvenTree +InvenTree + +# InvenTree InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications. InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action process and does not require a complex system of work orders or stock transactions.