mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
c140ecf14b
@ -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'
|
||||
|
@ -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())
|
||||
|
@ -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')
|
||||
|
@ -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>";
|
||||
|
||||
|
@ -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
|
47
InvenTree/stock/fixtures/stock.yaml
Normal file
47
InvenTree/stock/fixtures/stock.yaml
Normal 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
|
18
InvenTree/stock/migrations/0016_auto_20190512_2119.py
Normal file
18
InvenTree/stock/migrations/0016_auto_20190512_2119.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user