Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-13 19:00:51 +10:00
commit c140ecf14b
9 changed files with 241 additions and 71 deletions

View File

@ -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'

View File

@ -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())

View File

@ -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')

View File

@ -64,7 +64,7 @@ function updateStock(items, options={}) {
html += '<tr>';
html += '<td>' + item.part.name + '</td>';
html += '<td>' + item.part.full_name + '</td>';
if (item.location) {
html += '<td>' + item.location.name + '</td>';
@ -289,7 +289,7 @@ function moveStockItems(items, options) {
html += "<tr>";
html += "<td>" + item.part.name + "</td>";
html += "<td>" + item.part.full_name + "</td>";
html += "<td>" + item.location.pathstring + "</td>";
html += "<td>" + item.quantity + "</td>";

View File

@ -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

View File

@ -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

View File

@ -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),
),
]

View File

@ -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:

View File

@ -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)