diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml
index f9d7c9f4b4..632a265e23 100644
--- a/InvenTree/part/fixtures/part.yaml
+++ b/InvenTree/part/fixtures/part.yaml
@@ -1,12 +1,14 @@
# Create some fasteners
- model: part.part
+ pk: 1
fields:
name: 'M2x4 LPHS'
description: 'M2x4 low profile head screw'
category: 8
- model: part.part
+ pk: 2
fields:
name: 'M3x12 SHCS'
description: 'M3x12 socket head cap screw'
@@ -15,6 +17,7 @@
# Create some resistors
- model: part.part
+ pk: 3
fields:
name: 'R_2K2_0805'
description: '2.2kOhm resistor in 0805 package'
@@ -35,6 +38,7 @@
category: 3
- model: part.part
+ pk: 25
fields:
name: 'Widget'
description: 'A watchamacallit'
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index b781287930..d119c3094e 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -493,6 +493,7 @@ class Part(models.Model):
hash = hashlib.md5('bom seed'.encode())
for item in self.bom_items.all():
+ hash.update(str(item.sub_part.id).encode())
hash.update(str(item.sub_part.full_name).encode())
hash.update(str(item.quantity).encode())
hash.update(str(item.note).encode())
diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py
index ddbcf3babc..ea52396342 100644
--- a/InvenTree/part/test_category.py
+++ b/InvenTree/part/test_category.py
@@ -111,11 +111,11 @@ class CategoryTest(TestCase):
def test_default_locations(self):
""" Test traversal for default locations """
- self.assertEqual(str(self.fasteners.default_location), 'Office/Drawer')
+ self.assertEqual(str(self.fasteners.default_location), 'Office/Drawer_1')
# Test that parts in this location return the same default location, too
for p in self.fasteners.children.all():
- self.assert_equal(p.get_default_location(), 'Office/Drawer')
+ self.assert_equal(p.get_default_location(), 'Office/Drawer_1')
# Any part under electronics should default to 'Home'
R1 = Part.objects.get(name='R_2K2_0805')
diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js
index 4f32c3f753..222bca8f20 100644
--- a/InvenTree/static/script/inventree/stock.js
+++ b/InvenTree/static/script/inventree/stock.js
@@ -64,7 +64,7 @@ function updateStock(items, options={}) {
html += '
';
- html += '' + item.part.name + ' | ';
+ html += '' + item.part.full_name + ' | ';
if (item.location) {
html += '' + item.location.name + ' | ';
@@ -289,7 +289,7 @@ function moveStockItems(items, options) {
html += "
";
- html += "" + item.part.name + " | ";
+ html += "" + item.part.full_name + " | ";
html += "" + item.location.pathstring + " | ";
html += "" + item.quantity + " | ";
diff --git a/InvenTree/stock/fixtures/location.yaml b/InvenTree/stock/fixtures/location.yaml
index f04b192a7d..f34490af29 100644
--- a/InvenTree/stock/fixtures/location.yaml
+++ b/InvenTree/stock/fixtures/location.yaml
@@ -5,12 +5,14 @@
fields:
name: 'Home'
description: 'My house'
+
- model: stock.stocklocation
pk: 2
fields:
name: 'Bathroom'
description: 'Where I keep my bath'
parent: 1
+
- model: stock.stocklocation
pk: 3
fields:
@@ -23,9 +25,24 @@
fields:
name: 'Office'
description: 'Place of work'
+
- model: stock.stocklocation
pk: 5
fields:
- name: 'Drawer'
+ name: 'Drawer_1'
description: 'In my desk'
+ parent: 4
+
+- model: stock.stocklocation
+ pk: 6
+ fields:
+ name: 'Drawer_2'
+ description: 'Also in my desk'
+ parent: 4
+
+- model: stock.stocklocation
+ pk: 7
+ fields:
+ name: 'Drawer_3'
+ description: 'Again, in my desk'
parent: 4
\ No newline at end of file
diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml
new file mode 100644
index 0000000000..96e0a3ab72
--- /dev/null
+++ b/InvenTree/stock/fixtures/stock.yaml
@@ -0,0 +1,47 @@
+# Create some sample stock items
+
+# 4,000 screws in the dining room
+- model: stock.stockitem
+ fields:
+ part: 1
+ location: 3
+ batch: 'B123'
+ quantity: 4000
+
+# 5,000 screws in the bathroom
+- model: stock.stockitem
+ fields:
+ part: 1
+ location: 2
+ quantity: 5000
+
+# 1234 2K2 resistors in 'Drawer_1'
+- model: stock.stockitem
+ pk: 1234
+ fields:
+ part: 3
+ location: 5
+ quantity: 1234
+
+# Some widgets in drawer 3
+- model: stock.stockitem
+ pk: 100
+ fields:
+ part: 25
+ location: 7
+ quantity: 10
+ delete_on_deplete: False
+
+- model: stock.stockitem
+ pk: 101
+ fields:
+ part: 25
+ location: 7
+ quantity: 5
+
+- model: stock.stockitem
+ pk: 102
+ fields:
+ part: 25
+ location: 7
+ quantity: 3
\ No newline at end of file
diff --git a/InvenTree/stock/migrations/0016_auto_20190512_2119.py b/InvenTree/stock/migrations/0016_auto_20190512_2119.py
new file mode 100644
index 0000000000..582e68d277
--- /dev/null
+++ b/InvenTree/stock/migrations/0016_auto_20190512_2119.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-05-12 11:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0015_stockitem_delete_on_deplete'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='stockitem',
+ name='updated',
+ field=models.DateField(auto_now=True, null=True),
+ ),
+ ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index 09b9cafff4..950d6237dd 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -53,11 +53,13 @@ class StockLocation(InvenTreeTree):
""" Return the number of StockItem objects which live in or under this category
"""
- return len(StockItem.objects.filter(location__in=self.getUniqueChildren()))
+ return StockItem.objects.filter(location__in=self.getUniqueChildren()).count()
@property
def item_count(self):
-
+ """ Simply returns the number of stock items in this location.
+ Required for tree view serializer.
+ """
return self.stock_item_count
@@ -211,7 +213,7 @@ class StockItem(models.Model):
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1)
- updated = models.DateField(auto_now=True)
+ updated = models.DateField(auto_now=True, null=True)
# last time the stock was checked / counted
stocktake_date = models.DateField(blank=True, null=True)
@@ -444,9 +446,6 @@ class StockItem(models.Model):
""" Remove items from stock
"""
- if self.quantity == 0:
- return False
-
quantity = int(quantity)
if quantity <= 0 or self.infinite:
diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py
index d5b1f8c3f1..417c995b6c 100644
--- a/InvenTree/stock/tests.py
+++ b/InvenTree/stock/tests.py
@@ -1,4 +1,5 @@
from django.test import TestCase
+from django.db.models import Sum
from .models import StockLocation, StockItem, StockItemTracking
from part.models import Part
@@ -9,100 +10,123 @@ class StockTest(TestCase):
Tests to ensure that the stock location tree functions correcly
"""
+ fixtures = [
+ 'category',
+ 'part',
+ 'location',
+ 'stock',
+ ]
+
def setUp(self):
- # Initialize some categories
- self.loc1 = StockLocation.objects.create(name='L0',
- description='Top level category',
- parent=None)
+ # Extract some shortcuts from the fixtures
+ self.home = StockLocation.objects.get(name='Home')
+ self.bathroom = StockLocation.objects.get(name='Bathroom')
+ self.diningroom = StockLocation.objects.get(name='Dining Room')
- self.loc2 = StockLocation.objects.create(name='L1.1',
- description='Second level 1/2',
- parent=self.loc1)
+ self.office = StockLocation.objects.get(name='Office')
+ self.drawer1 = StockLocation.objects.get(name='Drawer_1')
+ self.drawer2 = StockLocation.objects.get(name='Drawer_2')
+ self.drawer3 = StockLocation.objects.get(name='Drawer_3')
- self.loc3 = StockLocation.objects.create(name='L1.2',
- description='Second level 2/2',
- parent=self.loc1)
+ def test_loc_count(self):
+ self.assertEqual(StockLocation.objects.count(), 7)
- 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):
+ def test_url(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/')
+
+ self.assertEqual(self.home.get_absolute_url(), '/stock/location/1/')
+
+ def test_barcode(self):
+ barcode = self.office.format_barcode()
+
+ self.assertIn('"name": "Office"', barcode)
def test_strings(self):
- it = StockItem.objects.get(pk=2)
- self.assertEqual(str(it), '250 x ACME Part @ L2.1')
+ it = StockItem.objects.get(pk=1)
+ self.assertEqual(str(it), '4000 x M2x4 LPHS @ Dining Room')
- 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_parent_locations(self):
+
+ self.assertEqual(self.office.parent, None)
+ self.assertEqual(self.drawer1.parent, self.office)
+ self.assertEqual(self.drawer2.parent, self.office)
+ self.assertEqual(self.drawer3.parent, self.office)
+
+ self.assertEqual(self.drawer3.pathstring, 'Office/Drawer_3')
+
+ # Move one of the drawers
+ self.drawer3.parent = self.home
+ self.assertNotEqual(self.drawer3.parent, self.office)
+
+ self.assertEqual(self.drawer3.pathstring, 'Home/Drawer_3')
def test_children(self):
- self.assertTrue(self.loc1.has_children)
- self.assertFalse(self.loc5.has_children)
+ self.assertTrue(self.office.has_children)
+ self.assertFalse(self.drawer2.has_children)
- childs = self.loc1.getUniqueChildren()
+ childs = self.office.getUniqueChildren()
- self.assertIn(self.loc2.id, childs)
- self.assertIn(self.loc4.id, childs)
+ self.assertIn(self.drawer1.id, childs)
+ self.assertIn(self.drawer2.id, childs)
- def test_paths(self):
- self.assertEqual(self.loc5.pathstring, 'L0/L1.2/L2.2')
+ self.assertNotIn(self.bathroom.id, childs)
def test_items(self):
- # Location 5 should have no items
- self.assertFalse(self.loc5.has_items())
- self.assertFalse(self.loc3.has_items())
+ self.assertTrue(self.drawer1.has_items())
+ self.assertTrue(self.drawer3.has_items())
+ self.assertFalse(self.drawer2.has_items())
- # Location 4 should have three stock items
- self.assertEqual(self.loc4.stock_items.count(), 3)
+ # Drawer 3 should have three stock items
+ self.assertEqual(self.drawer3.stock_items.count(), 3)
+ self.assertEqual(self.drawer3.item_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)
+ # There should be 5000 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'], 18)
def test_delete_location(self):
+
+ # How many stock items are there?
+ n_stock = StockItem.objects.count()
+
+ # What parts are in drawer 3?
+ stock_ids = [part.id for part in StockItem.objects.filter(location=self.drawer3.id)]
+
# Delete location - parts should move to parent location
- self.loc4.delete()
+ self.drawer3.delete()
- # There should still be 3 stock items
- self.assertEqual(StockItem.objects.count(), 3)
+ # There should still be the same number of parts
+ self.assertEqual(StockItem.objects.count(), n_stock)
- # Parent location should have moved up to loc2
- for it in StockItem.objects.all():
- self.assertEqual(it.location, self.loc2)
+ # stock should have moved
+ for s_id in stock_ids:
+ s_item = StockItem.objects.get(id=s_id)
+ self.assertEqual(s_item.location, self.office)
def test_move(self):
- # Move the first stock item to loc5
+ """ Test stock movement functions """
+
+ # Move 4,000 screws to the bathroom
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)
+ self.assertNotEqual(it.location, self.bathroom)
+ self.assertTrue(it.move(self.bathroom, 'Moved to the bathroom', None))
+ self.assertEqual(it.location, self.bathroom)
+
+ # There now should be 2 lots of screws in the bathroom
+ self.assertEqual(StockItem.objects.filter(part=1, location=self.bathroom).count(), 2)
# 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')
+ self.assertEqual(track.notes, 'Moved to the bathroom')
def test_self_move(self):
# Try to move an item to its current location (should fail)
@@ -114,10 +138,47 @@ class StockTest(TestCase):
# Ensure tracking info was not added
self.assertEqual(it.tracking_info.count(), n)
+ def test_partial_move(self):
+ w1 = StockItem.objects.get(pk=100)
+
+ # Move 6 of the units
+ self.assertTrue(w1.move(self.diningroom, 'Moved', None, quantity=6))
+ self.assertEqual(w1.quantity, 6)
+
+ # There should also be a new object still in drawer3
+ self.assertEqual(StockItem.objects.filter(part=25).count(), 4)
+ widget = StockItem.objects.get(location=self.drawer3.id, part=25, quantity=4)
+
+ # Try to move negative units
+ self.assertFalse(widget.move(self.bathroom, 'Test', None, quantity=-100))
+ self.assertEqual(StockItem.objects.filter(part=25).count(), 4)
+
+ # Try to move to a blank location
+ self.assertFalse(widget.move(None, 'null', None))
+
+ def test_split_stock(self):
+ # Split the 1234 x 2K2 resistors in Drawer_1
+
+ N = StockItem.objects.filter(part=3).count()
+
+ stock = StockItem.objects.get(id=1234)
+ stock.splitStock(1000, None)
+ self.assertEqual(stock.quantity, 234)
+
+ # There should be a new stock item too!
+ self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
+
+ # Try to split a negative quantity
+ stock.splitStock(-10, None)
+ self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
+
+ stock.splitStock(stock.quantity, None)
+ self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1)
+
def test_stocktake(self):
# Perform stocktake
it = StockItem.objects.get(pk=2)
- self.assertEqual(it.quantity, 250)
+ self.assertEqual(it.quantity, 5000)
it.stocktake(255, None, notes='Counted items!')
self.assertEqual(it.quantity, 255)
@@ -147,6 +208,8 @@ class StockTest(TestCase):
self.assertIn('Added', track.title)
self.assertIn('Added some items', track.notes)
+ self.assertFalse(it.add_stock(-10, None))
+
def test_take_stock(self):
it = StockItem.objects.get(pk=2)
n = it.quantity
@@ -160,3 +223,24 @@ class StockTest(TestCase):
self.assertIn('Removed', track.title)
self.assertIn('Removed some items', track.notes)
self.assertTrue(it.has_tracking_info)
+
+ # Test that negative quantity does nothing
+ self.assertFalse(it.take_stock(-10, None))
+
+ def test_deplete_stock(self):
+
+ w1 = StockItem.objects.get(pk=100)
+ w2 = StockItem.objects.get(pk=101)
+
+ # Take 25 units from w1
+ w1.take_stock(30, None, notes='Took 30')
+
+ # Get from database again
+ w1 = StockItem.objects.get(pk=100)
+ self.assertEqual(w1.quantity, 0)
+
+ # Take 25 units from w2 (will be deleted)
+ w2.take_stock(30, None, notes='Took 30')
+
+ with self.assertRaises(StockItem.DoesNotExist):
+ w2 = StockItem.objects.get(pk=101)