From 3c40418f048a823a0a7e5efe4ca7f7f5076aeedd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 04:20:40 +1000 Subject: [PATCH 1/6] 100% coverage on company/models.py --- InvenTree/company/tests.py | 44 +++++++++++++++++++++++++++++++++++++- Makefile | 2 +- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index 925d9eb104..e5f5d99869 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -1,6 +1,9 @@ from django.test import TestCase -from .models import Company +import os + +from .models import Company, Contact +from .models import rename_company_image class CompanySimpleTest(TestCase): @@ -16,4 +19,43 @@ class CompanySimpleTest(TestCase): def test_company_model(self): c = Company.objects.get(pk=1) self.assertEqual(c.name, 'ABC Co.') + self.assertEqual(str(c), 'ABC Co. - Seller of ABC products') + + def test_company_url(self): + c = Company.objects.get(pk=1) self.assertEqual(c.get_absolute_url(), '/company/1/') + + def test_image_renamer(self): + c = Company.objects.get(pk=1) + rn = rename_company_image(c, 'test.png') + self.assertEqual(rn, 'company_images' + os.path.sep + 'company_1_img.png') + + rn = rename_company_image(c, 'test2') + self.assertEqual(rn, 'company_images' + os.path.sep + 'company_1_img') + + def test_part_count(self): + # Initially the company should have no associated parts + c = Company.objects.get(pk=1) + self.assertEqual(c.has_parts, False) + + # TODO - Add some supplier parts here + + +class ContactSimpleTest(TestCase): + + def setUp(self): + # Create a simple company + c = Company.objects.create(name='Test Corp.', description='We make stuff good') + + # Add some contacts + Contact.objects.create(name='Joe Smith', company=c) + Contact.objects.create(name='Fred Smith', company=c) + Contact.objects.create(name='Sally Smith', company=c) + + def test_exists(self): + self.assertEqual(Contact.objects.count(), 3) + + def test_delete(self): + # Remove the parent company + Company.objects.get(pk=1).delete() + self.assertEqual(Contact.objects.count(), 0) diff --git a/Makefile b/Makefile index 8db2a2110c..dadbb44999 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ style: test: python InvenTree/manage.py check - python manage.py test build company part stock + python InvenTree/manage.py test build company part stock migrate: python InvenTree/manage.py makemigrations company From f36f02b27f58ac0198bcecebdd081cc9038966fb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 17:30:44 +1000 Subject: [PATCH 2/6] Tests for stock app - Increase coverage of Stock/models.py to 84% --- .coveragerc | 3 +- InvenTree/part/test_category.py | 2 +- InvenTree/stock/models.py | 25 ++-- InvenTree/stock/test_stock_item.py | 7 -- InvenTree/stock/test_stock_location.py | 7 -- InvenTree/stock/test_stock_tracking.py | 7 -- InvenTree/stock/tests.py | 162 +++++++++++++++++++++++++ 7 files changed, 176 insertions(+), 37 deletions(-) delete mode 100644 InvenTree/stock/test_stock_item.py delete mode 100644 InvenTree/stock/test_stock_location.py delete mode 100644 InvenTree/stock/test_stock_tracking.py create mode 100644 InvenTree/stock/tests.py diff --git a/.coveragerc b/.coveragerc index ed8dd1d86e..0b7bc7b9dc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,4 +2,5 @@ source = ./InvenTree omit = # Do not run coverage on migration files - */migrations/* \ No newline at end of file + */migrations/* + InvenTree/manage.py \ No newline at end of file diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index 9fee29332c..bebe2c96ac 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -42,7 +42,7 @@ class CategoryTest(TestCase): childs = self.p1.getUniqueChildren() self.assertIn(self.p2.id, childs) - self.assertIn(self.p2.id, childs) + self.assertIn(self.p3.id, childs) def test_unique_parents(self): parents = self.p2.getUniqueParents() diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 312bff9bbe..7333cc4f7f 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -28,11 +28,6 @@ class StockLocation(InvenTreeTree): def get_absolute_url(self): return reverse('stock-location-detail', kwargs={'pk': self.id}) - @property - def stock_items(self): - return self.stockitem_set.all() - - @property def has_items(self): return self.stock_items.count() > 0 @@ -128,7 +123,7 @@ class StockItem(models.Model): # Where the part is stored. If the part has been used to build another stock item, the location may not make sense location = models.ForeignKey(StockLocation, on_delete=models.DO_NOTHING, - related_name='items', blank=True, null=True, + related_name='stock_items', blank=True, null=True, help_text='Where is this stock item located?') # If this StockItem belongs to another StockItem (e.g. as part of a sub-assembly) @@ -253,7 +248,7 @@ class StockItem(models.Model): count = int(count) if count < 0 or self.infinite: - return + return False self.quantity = count self.stocktake_date = datetime.now().date() @@ -265,6 +260,8 @@ class StockItem(models.Model): notes=notes, system=True) + return True + @transaction.atomic def add_stock(self, quantity, user, notes=''): """ Add items to stock @@ -276,7 +273,7 @@ class StockItem(models.Model): # Ignore amounts that do not make sense if quantity <= 0 or self.infinite: - return + return False self.quantity += quantity @@ -287,18 +284,20 @@ class StockItem(models.Model): notes=notes, system=True) + return True + @transaction.atomic def take_stock(self, quantity, user, notes=''): """ Remove items from stock """ if self.quantity == 0: - return + return False quantity = int(quantity) if quantity <= 0 or self.infinite: - return + return False self.quantity -= quantity @@ -312,6 +311,8 @@ class StockItem(models.Model): notes=notes, system=True) + return True + def __str__(self): s = '{n} x {part}'.format( n=self.quantity, @@ -322,10 +323,6 @@ class StockItem(models.Model): return s - @property - def is_trackable(self): - return self.part.trackable - class StockItemTracking(models.Model): """ Stock tracking entry diff --git a/InvenTree/stock/test_stock_item.py b/InvenTree/stock/test_stock_item.py deleted file mode 100644 index becd6e4e69..0000000000 --- a/InvenTree/stock/test_stock_item.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.test import TestCase - - -class StockItemTest(TestCase): - - def setUp(self): - pass diff --git a/InvenTree/stock/test_stock_location.py b/InvenTree/stock/test_stock_location.py deleted file mode 100644 index 0a89aeed33..0000000000 --- a/InvenTree/stock/test_stock_location.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.test import TestCase - - -class StockLocationTest(TestCase): - - def setUp(self): - pass diff --git a/InvenTree/stock/test_stock_tracking.py b/InvenTree/stock/test_stock_tracking.py deleted file mode 100644 index 15b79cbe66..0000000000 --- a/InvenTree/stock/test_stock_tracking.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.test import TestCase - - -class StockTrackingTest(TestCase): - - def setUp(self): - pass diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py new file mode 100644 index 0000000000..d5b1f8c3f1 --- /dev/null +++ b/InvenTree/stock/tests.py @@ -0,0 +1,162 @@ +from django.test import TestCase + +from .models import StockLocation, StockItem, StockItemTracking +from part.models import Part + + +class StockTest(TestCase): + """ + Tests to ensure that the stock location tree functions correcly + """ + + def setUp(self): + # Initialize some categories + self.loc1 = StockLocation.objects.create(name='L0', + description='Top level category', + parent=None) + + self.loc2 = StockLocation.objects.create(name='L1.1', + description='Second level 1/2', + parent=self.loc1) + + self.loc3 = StockLocation.objects.create(name='L1.2', + description='Second level 2/2', + parent=self.loc1) + + self.loc4 = StockLocation.objects.create(name='L2.1', + description='Third level 1/2', + parent=self.loc2) + + self.loc5 = StockLocation.objects.create(name='L2.2', + description='Third level 2/2', + parent=self.loc3) + + # Add some items to loc4 (all copies of a single part) + p = Part.objects.create(name='ACME Part', description='This is a part!') + + StockItem.objects.create(part=p, location=self.loc4, quantity=1000) + StockItem.objects.create(part=p, location=self.loc4, quantity=250) + StockItem.objects.create(part=p, location=self.loc4, quantity=12) + + def test_simple(self): + it = StockItem.objects.get(pk=2) + self.assertEqual(it.get_absolute_url(), '/stock/item/2/') + self.assertEqual(self.loc4.get_absolute_url(), '/stock/location/4/') + + def test_strings(self): + it = StockItem.objects.get(pk=2) + self.assertEqual(str(it), '250 x ACME Part @ L2.1') + + def test_parent(self): + self.assertEqual(StockLocation.objects.count(), 5) + self.assertEqual(self.loc1.parent, None) + self.assertEqual(self.loc2.parent, self.loc1) + self.assertEqual(self.loc5.parent, self.loc3) + + def test_children(self): + self.assertTrue(self.loc1.has_children) + self.assertFalse(self.loc5.has_children) + + childs = self.loc1.getUniqueChildren() + + self.assertIn(self.loc2.id, childs) + self.assertIn(self.loc4.id, childs) + + def test_paths(self): + self.assertEqual(self.loc5.pathstring, 'L0/L1.2/L2.2') + + def test_items(self): + # Location 5 should have no items + self.assertFalse(self.loc5.has_items()) + self.assertFalse(self.loc3.has_items()) + + # Location 4 should have three stock items + self.assertEqual(self.loc4.stock_items.count(), 3) + + def test_stock_count(self): + part = Part.objects.get(pk=1) + + # There should be 1262 items in stock + self.assertEqual(part.total_stock, 1262) + + def test_delete_location(self): + # Delete location - parts should move to parent location + self.loc4.delete() + + # There should still be 3 stock items + self.assertEqual(StockItem.objects.count(), 3) + + # Parent location should have moved up to loc2 + for it in StockItem.objects.all(): + self.assertEqual(it.location, self.loc2) + + def test_move(self): + # Move the first stock item to loc5 + it = StockItem.objects.get(pk=1) + self.assertNotEqual(it.location, self.loc5) + self.assertTrue(it.move(self.loc5, 'Moved to another place', None)) + self.assertEqual(it.location, self.loc5) + + # Check that a tracking item was added + track = StockItemTracking.objects.filter(item=it).latest('id') + + self.assertEqual(track.item, it) + self.assertIn('Moved to', track.title) + self.assertEqual(track.notes, 'Moved to another place') + + def test_self_move(self): + # Try to move an item to its current location (should fail) + it = StockItem.objects.get(pk=1) + + n = it.tracking_info.count() + self.assertFalse(it.move(it.location, 'Moved to same place', None)) + + # Ensure tracking info was not added + self.assertEqual(it.tracking_info.count(), n) + + def test_stocktake(self): + # Perform stocktake + it = StockItem.objects.get(pk=2) + self.assertEqual(it.quantity, 250) + it.stocktake(255, None, notes='Counted items!') + + self.assertEqual(it.quantity, 255) + + # Check that a tracking item was added + track = StockItemTracking.objects.filter(item=it).latest('id') + + self.assertIn('Stocktake', track.title) + self.assertIn('Counted items', track.notes) + + n = it.tracking_info.count() + self.assertFalse(it.stocktake(-1, None, 'test negative stocktake')) + + # Ensure tracking info was not added + self.assertEqual(it.tracking_info.count(), n) + + def test_add_stock(self): + it = StockItem.objects.get(pk=2) + n = it.quantity + it.add_stock(45, None, notes='Added some items') + + self.assertEqual(it.quantity, n + 45) + + # Check that a tracking item was added + track = StockItemTracking.objects.filter(item=it).latest('id') + + self.assertIn('Added', track.title) + self.assertIn('Added some items', track.notes) + + def test_take_stock(self): + it = StockItem.objects.get(pk=2) + n = it.quantity + it.take_stock(15, None, notes='Removed some items') + + self.assertEqual(it.quantity, n - 15) + + # Check that a tracking item was added + track = StockItemTracking.objects.filter(item=it).latest('id') + + self.assertIn('Removed', track.title) + self.assertIn('Removed some items', track.notes) + self.assertTrue(it.has_tracking_info) From 2ab827667287664fc62dc253a9e69f19aa7c747e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 17:51:02 +1000 Subject: [PATCH 3/6] better coverage for part/models - Increase from 57% to 67% --- InvenTree/part/test_category.py | 10 ++++++++++ InvenTree/part/test_part.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index bebe2c96ac..4d2a5b4441 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -64,3 +64,13 @@ class CategoryTest(TestCase): self.assertEqual(self.p1.partcount, 3) self.assertEqual(self.p2.partcount, 3) self.assertEqual(self.p3.partcount, 1) + + def test_delete(self): + self.assertEqual(Part.objects.filter(category=self.p1).count(), 0) + + # Delete p2 (it has 2 direct parts and one child category) + self.p2.delete() + + self.assertEqual(Part.objects.filter(category=self.p1).count(), 2) + + self.assertEqual(PartCategory.objects.get(pk=self.p3.id).parent, self.p1) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index c9ed2fd7b3..adb2a76413 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -1,6 +1,9 @@ from django.test import TestCase +import os + from .models import Part, PartCategory +from .models import rename_part_image class SimplePartTest(TestCase): @@ -21,3 +24,15 @@ class SimplePartTest(TestCase): def test_category(self): self.assertEqual(self.px.category_path, '') self.assertEqual(self.pz.category_path, 'TLC') + + def test_rename_img(self): + img = rename_part_image(self.px, 'hello.png') + self.assertEqual(img, os.path.join('part_images', 'part_1_img.png')) + + img = rename_part_image(self.pz, 'test') + self.assertEqual(img, os.path.join('part_images', 'part_3_img')) + + def test_stock(self): + # Stock should initially be zero + self.assertEqual(self.px.total_stock, 0) + self.assertEqual(self.py.available_stock, 0) \ No newline at end of file From 1112e5e3e2ce7193b3525d6fa29e18196f4bd3a9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 18:50:22 +1000 Subject: [PATCH 4/6] PEP fixes --- InvenTree/part/test_category.py | 2 +- InvenTree/part/test_part.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index 4d2a5b4441..3b732f80e8 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -72,5 +72,5 @@ class CategoryTest(TestCase): self.p2.delete() self.assertEqual(Part.objects.filter(category=self.p1).count(), 2) - + self.assertEqual(PartCategory.objects.get(pk=self.p3.id).parent, self.p1) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index adb2a76413..93a8652667 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -35,4 +35,4 @@ class SimplePartTest(TestCase): def test_stock(self): # Stock should initially be zero self.assertEqual(self.px.total_stock, 0) - self.assertEqual(self.py.available_stock, 0) \ No newline at end of file + self.assertEqual(self.py.available_stock, 0) From 470dee48b32cd6047cae21db091097a0f2b6c05c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 18:54:36 +1000 Subject: [PATCH 5/6] Ignore some files for coverage report --- .coveragerc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 0b7bc7b9dc..e953c6c86c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,4 +3,7 @@ source = ./InvenTree omit = # Do not run coverage on migration files */migrations/* - InvenTree/manage.py \ No newline at end of file + InvenTree/manage.py + Inventree/InvenTree/middleware.py + Inventree/InvenTree/utils.py + Inventree/InvenTree/wsgi.py \ No newline at end of file From c6b5177f5c6457b8969d97daca00f86acdf3ee5f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 21:04:33 +1000 Subject: [PATCH 6/6] Cleanup requirements file --- .travis.yml | 1 - Makefile | 5 +---- requirements/base.txt => requirements.txt | 3 +++ requirements/build.txt | 4 ---- requirements/dev.txt | 4 ---- 5 files changed, 4 insertions(+), 13 deletions(-) rename requirements/base.txt => requirements.txt (81%) delete mode 100644 requirements/build.txt delete mode 100644 requirements/dev.txt diff --git a/.travis.yml b/.travis.yml index cd2093c55a..124ed02da0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ addons: before_install: - make setup - - make setup_ci script: - make coverage diff --git a/Makefile b/Makefile index dadbb44999..77a38f02f3 100644 --- a/Makefile +++ b/Makefile @@ -23,16 +23,13 @@ migrate: python InvenTree/manage.py check install: - pip install -U -r requirements/base.txt + pip install -U -r requirements.txt # Generate a secret key python InvenTree/key.py setup: install migrate -setup_ci: - pip install -U -r requirements/build.txt - coverage: python InvenTree/manage.py check coverage run InvenTree/manage.py test build company part stock diff --git a/requirements/base.txt b/requirements.txt similarity index 81% rename from requirements/base.txt rename to requirements.txt index f8be0063f8..6cf3834241 100644 --- a/requirements/base.txt +++ b/requirements.txt @@ -10,3 +10,6 @@ django-crispy-forms>=1.7.2 django-import-export>=1.0.0 django-cleanup>=2.1.0 django-qr-code==1.0.0 +flake8==3.3.0 +coverage>=4.5.3 +python-coveralls==2.9.1 \ No newline at end of file diff --git a/requirements/build.txt b/requirements/build.txt deleted file mode 100644 index 9ec2467668..0000000000 --- a/requirements/build.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt -flake8==3.3.0 -coverage>=4.5.3 -python-coveralls==2.9.1 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 3ec7263b93..0000000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,4 +0,0 @@ --r build.txt -django-extensions==1.7.8 -graphviz==0.6 -ipython==5.3.0