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 # Create some fasteners
- model: part.part - model: part.part
pk: 1
fields: fields:
name: 'M2x4 LPHS' name: 'M2x4 LPHS'
description: 'M2x4 low profile head screw' description: 'M2x4 low profile head screw'
category: 8 category: 8
- model: part.part - model: part.part
pk: 2
fields: fields:
name: 'M3x12 SHCS' name: 'M3x12 SHCS'
description: 'M3x12 socket head cap screw' description: 'M3x12 socket head cap screw'
@ -15,6 +17,7 @@
# Create some resistors # Create some resistors
- model: part.part - model: part.part
pk: 3
fields: fields:
name: 'R_2K2_0805' name: 'R_2K2_0805'
description: '2.2kOhm resistor in 0805 package' description: '2.2kOhm resistor in 0805 package'
@ -35,6 +38,7 @@
category: 3 category: 3
- model: part.part - model: part.part
pk: 25
fields: fields:
name: 'Widget' name: 'Widget'
description: 'A watchamacallit' description: 'A watchamacallit'

View File

@ -493,6 +493,7 @@ class Part(models.Model):
hash = hashlib.md5('bom seed'.encode()) hash = hashlib.md5('bom seed'.encode())
for item in self.bom_items.all(): 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.sub_part.full_name).encode())
hash.update(str(item.quantity).encode()) hash.update(str(item.quantity).encode())
hash.update(str(item.note).encode()) hash.update(str(item.note).encode())

View File

@ -111,11 +111,11 @@ class CategoryTest(TestCase):
def test_default_locations(self): def test_default_locations(self):
""" Test traversal for default locations """ """ 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 # Test that parts in this location return the same default location, too
for p in self.fasteners.children.all(): 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' # Any part under electronics should default to 'Home'
R1 = Part.objects.get(name='R_2K2_0805') R1 = Part.objects.get(name='R_2K2_0805')

View File

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

View File

@ -5,12 +5,14 @@
fields: fields:
name: 'Home' name: 'Home'
description: 'My house' description: 'My house'
- model: stock.stocklocation - model: stock.stocklocation
pk: 2 pk: 2
fields: fields:
name: 'Bathroom' name: 'Bathroom'
description: 'Where I keep my bath' description: 'Where I keep my bath'
parent: 1 parent: 1
- model: stock.stocklocation - model: stock.stocklocation
pk: 3 pk: 3
fields: fields:
@ -23,9 +25,24 @@
fields: fields:
name: 'Office' name: 'Office'
description: 'Place of work' description: 'Place of work'
- model: stock.stocklocation - model: stock.stocklocation
pk: 5 pk: 5
fields: fields:
name: 'Drawer' name: 'Drawer_1'
description: 'In my desk' 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 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 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 @property
def item_count(self): def item_count(self):
""" Simply returns the number of stock items in this location.
Required for tree view serializer.
"""
return self.stock_item_count return self.stock_item_count
@ -211,7 +213,7 @@ class StockItem(models.Model):
quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)], default=1) 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 # last time the stock was checked / counted
stocktake_date = models.DateField(blank=True, null=True) stocktake_date = models.DateField(blank=True, null=True)
@ -444,9 +446,6 @@ class StockItem(models.Model):
""" Remove items from stock """ Remove items from stock
""" """
if self.quantity == 0:
return False
quantity = int(quantity) quantity = int(quantity)
if quantity <= 0 or self.infinite: if quantity <= 0 or self.infinite:

View File

@ -1,4 +1,5 @@
from django.test import TestCase from django.test import TestCase
from django.db.models import Sum
from .models import StockLocation, StockItem, StockItemTracking from .models import StockLocation, StockItem, StockItemTracking
from part.models import Part from part.models import Part
@ -9,100 +10,123 @@ class StockTest(TestCase):
Tests to ensure that the stock location tree functions correcly Tests to ensure that the stock location tree functions correcly
""" """
fixtures = [
'category',
'part',
'location',
'stock',
]
def setUp(self): def setUp(self):
# Initialize some categories # Extract some shortcuts from the fixtures
self.loc1 = StockLocation.objects.create(name='L0', self.home = StockLocation.objects.get(name='Home')
description='Top level category', self.bathroom = StockLocation.objects.get(name='Bathroom')
parent=None) self.diningroom = StockLocation.objects.get(name='Dining Room')
self.loc2 = StockLocation.objects.create(name='L1.1', self.office = StockLocation.objects.get(name='Office')
description='Second level 1/2', self.drawer1 = StockLocation.objects.get(name='Drawer_1')
parent=self.loc1) self.drawer2 = StockLocation.objects.get(name='Drawer_2')
self.drawer3 = StockLocation.objects.get(name='Drawer_3')
self.loc3 = StockLocation.objects.create(name='L1.2', def test_loc_count(self):
description='Second level 2/2', self.assertEqual(StockLocation.objects.count(), 7)
parent=self.loc1)
self.loc4 = StockLocation.objects.create(name='L2.1', def test_url(self):
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) it = StockItem.objects.get(pk=2)
self.assertEqual(it.get_absolute_url(), '/stock/item/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): def test_strings(self):
it = StockItem.objects.get(pk=2) it = StockItem.objects.get(pk=1)
self.assertEqual(str(it), '250 x ACME Part @ L2.1') self.assertEqual(str(it), '4000 x M2x4 LPHS @ Dining Room')
def test_parent(self): def test_parent_locations(self):
self.assertEqual(StockLocation.objects.count(), 5)
self.assertEqual(self.loc1.parent, None) self.assertEqual(self.office.parent, None)
self.assertEqual(self.loc2.parent, self.loc1) self.assertEqual(self.drawer1.parent, self.office)
self.assertEqual(self.loc5.parent, self.loc3) 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): def test_children(self):
self.assertTrue(self.loc1.has_children) self.assertTrue(self.office.has_children)
self.assertFalse(self.loc5.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.drawer1.id, childs)
self.assertIn(self.loc4.id, childs) self.assertIn(self.drawer2.id, childs)
def test_paths(self): self.assertNotIn(self.bathroom.id, childs)
self.assertEqual(self.loc5.pathstring, 'L0/L1.2/L2.2')
def test_items(self): def test_items(self):
# Location 5 should have no items self.assertTrue(self.drawer1.has_items())
self.assertFalse(self.loc5.has_items()) self.assertTrue(self.drawer3.has_items())
self.assertFalse(self.loc3.has_items()) self.assertFalse(self.drawer2.has_items())
# Location 4 should have three stock items # Drawer 3 should have three stock items
self.assertEqual(self.loc4.stock_items.count(), 3) self.assertEqual(self.drawer3.stock_items.count(), 3)
self.assertEqual(self.drawer3.item_count, 3)
def test_stock_count(self): def test_stock_count(self):
part = Part.objects.get(pk=1) part = Part.objects.get(pk=1)
# There should be 1262 items in stock # There should be 5000 screws in stock
self.assertEqual(part.total_stock, 1262) 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): 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 # Delete location - parts should move to parent location
self.loc4.delete() self.drawer3.delete()
# There should still be 3 stock items # There should still be the same number of parts
self.assertEqual(StockItem.objects.count(), 3) self.assertEqual(StockItem.objects.count(), n_stock)
# Parent location should have moved up to loc2 # stock should have moved
for it in StockItem.objects.all(): for s_id in stock_ids:
self.assertEqual(it.location, self.loc2) s_item = StockItem.objects.get(id=s_id)
self.assertEqual(s_item.location, self.office)
def test_move(self): 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) it = StockItem.objects.get(pk=1)
self.assertNotEqual(it.location, self.loc5) self.assertNotEqual(it.location, self.bathroom)
self.assertTrue(it.move(self.loc5, 'Moved to another place', None)) self.assertTrue(it.move(self.bathroom, 'Moved to the bathroom', None))
self.assertEqual(it.location, self.loc5) 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 # Check that a tracking item was added
track = StockItemTracking.objects.filter(item=it).latest('id') track = StockItemTracking.objects.filter(item=it).latest('id')
self.assertEqual(track.item, it) self.assertEqual(track.item, it)
self.assertIn('Moved to', track.title) 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): def test_self_move(self):
# Try to move an item to its current location (should fail) # Try to move an item to its current location (should fail)
@ -114,10 +138,47 @@ class StockTest(TestCase):
# Ensure tracking info was not added # Ensure tracking info was not added
self.assertEqual(it.tracking_info.count(), n) 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): def test_stocktake(self):
# Perform stocktake # Perform stocktake
it = StockItem.objects.get(pk=2) it = StockItem.objects.get(pk=2)
self.assertEqual(it.quantity, 250) self.assertEqual(it.quantity, 5000)
it.stocktake(255, None, notes='Counted items!') it.stocktake(255, None, notes='Counted items!')
self.assertEqual(it.quantity, 255) self.assertEqual(it.quantity, 255)
@ -147,6 +208,8 @@ class StockTest(TestCase):
self.assertIn('Added', track.title) self.assertIn('Added', track.title)
self.assertIn('Added some items', track.notes) self.assertIn('Added some items', track.notes)
self.assertFalse(it.add_stock(-10, None))
def test_take_stock(self): def test_take_stock(self):
it = StockItem.objects.get(pk=2) it = StockItem.objects.get(pk=2)
n = it.quantity n = it.quantity
@ -160,3 +223,24 @@ class StockTest(TestCase):
self.assertIn('Removed', track.title) self.assertIn('Removed', track.title)
self.assertIn('Removed some items', track.notes) self.assertIn('Removed some items', track.notes)
self.assertTrue(it.has_tracking_info) 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)