mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
- Ref: https://github.com/inventree/InvenTree/pull/6335 - Fixes bug with regard to splitting stock items
This commit is contained in:
parent
3daf85c3d0
commit
c12e6dbf42
@ -57,7 +57,6 @@ class AddFieldOrSkip(migrations.AddField):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
super().database_forwards(app_label, schema_editor, from_state, to_state)
|
super().database_forwards(app_label, schema_editor, from_state, to_state)
|
||||||
print(f'Added field {self.name} to model {self.model_name}')
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
@ -40,6 +41,8 @@ from part import models as PartModels
|
|||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
class StockLocationType(MetadataMixin, models.Model):
|
class StockLocationType(MetadataMixin, models.Model):
|
||||||
"""A type of stock location like Warehouse, room, shelf, drawer.
|
"""A type of stock location like Warehouse, room, shelf, drawer.
|
||||||
@ -1660,9 +1663,12 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
# Nullify the PK so a new record is created
|
# Nullify the PK so a new record is created
|
||||||
new_stock = StockItem.objects.get(pk=self.pk)
|
new_stock = StockItem.objects.get(pk=self.pk)
|
||||||
new_stock.pk = None
|
new_stock.pk = None
|
||||||
new_stock.parent = self
|
|
||||||
new_stock.quantity = quantity
|
new_stock.quantity = quantity
|
||||||
|
|
||||||
|
# Update the new stock item to ensure the tree structure is observed
|
||||||
|
new_stock.parent = self
|
||||||
|
new_stock.level = self.level + 1
|
||||||
|
|
||||||
# Move to the new location if specified, otherwise use current location
|
# Move to the new location if specified, otherwise use current location
|
||||||
if location:
|
if location:
|
||||||
new_stock.location = location
|
new_stock.location = location
|
||||||
@ -1704,6 +1710,19 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
stockitem=new_stock,
|
stockitem=new_stock,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Rebuild the tree for this parent item
|
||||||
|
try:
|
||||||
|
StockItem.objects.partial_rebuild(tree_id=self.tree_id)
|
||||||
|
except Exception:
|
||||||
|
logger.warning('Rebuilding entire StockItem tree')
|
||||||
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
|
# Attempt to reload the new item from the database
|
||||||
|
try:
|
||||||
|
new_stock.refresh_from_db()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Return a copy of the "new" stock item
|
# Return a copy of the "new" stock item
|
||||||
return new_stock
|
return new_stock
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from .models import (StockItem, StockItemTestResult, StockItemTracking,
|
|||||||
|
|
||||||
|
|
||||||
class StockTestBase(InvenTreeTestCase):
|
class StockTestBase(InvenTreeTestCase):
|
||||||
"""Base class for running Stock tests"""
|
"""Base class for running Stock tests."""
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
'category',
|
'category',
|
||||||
@ -54,11 +54,11 @@ class StockTest(StockTestBase):
|
|||||||
"""Tests to ensure that the stock location tree functions correctly."""
|
"""Tests to ensure that the stock location tree functions correctly."""
|
||||||
|
|
||||||
def test_pathstring(self):
|
def test_pathstring(self):
|
||||||
"""Check that pathstring updates occur as expected"""
|
"""Check that pathstring updates occur as expected."""
|
||||||
a = StockLocation.objects.create(name="A")
|
a = StockLocation.objects.create(name='A')
|
||||||
b = StockLocation.objects.create(name="B", parent=a)
|
b = StockLocation.objects.create(name='B', parent=a)
|
||||||
c = StockLocation.objects.create(name="C", parent=b)
|
c = StockLocation.objects.create(name='C', parent=b)
|
||||||
d = StockLocation.objects.create(name="D", parent=c)
|
d = StockLocation.objects.create(name='D', parent=c)
|
||||||
|
|
||||||
def refresh():
|
def refresh():
|
||||||
a.refresh_from_db()
|
a.refresh_from_db()
|
||||||
@ -67,56 +67,56 @@ class StockTest(StockTestBase):
|
|||||||
d.refresh_from_db()
|
d.refresh_from_db()
|
||||||
|
|
||||||
# Initial checks
|
# Initial checks
|
||||||
self.assertEqual(a.pathstring, "A")
|
self.assertEqual(a.pathstring, 'A')
|
||||||
self.assertEqual(b.pathstring, "A/B")
|
self.assertEqual(b.pathstring, 'A/B')
|
||||||
self.assertEqual(c.pathstring, "A/B/C")
|
self.assertEqual(c.pathstring, 'A/B/C')
|
||||||
self.assertEqual(d.pathstring, "A/B/C/D")
|
self.assertEqual(d.pathstring, 'A/B/C/D')
|
||||||
|
|
||||||
c.name = "Cc"
|
c.name = 'Cc'
|
||||||
c.save()
|
c.save()
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
self.assertEqual(a.pathstring, "A")
|
self.assertEqual(a.pathstring, 'A')
|
||||||
self.assertEqual(b.pathstring, "A/B")
|
self.assertEqual(b.pathstring, 'A/B')
|
||||||
self.assertEqual(c.pathstring, "A/B/Cc")
|
self.assertEqual(c.pathstring, 'A/B/Cc')
|
||||||
self.assertEqual(d.pathstring, "A/B/Cc/D")
|
self.assertEqual(d.pathstring, 'A/B/Cc/D')
|
||||||
|
|
||||||
b.name = "Bb"
|
b.name = 'Bb'
|
||||||
b.save()
|
b.save()
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
self.assertEqual(a.pathstring, "A")
|
self.assertEqual(a.pathstring, 'A')
|
||||||
self.assertEqual(b.pathstring, "A/Bb")
|
self.assertEqual(b.pathstring, 'A/Bb')
|
||||||
self.assertEqual(c.pathstring, "A/Bb/Cc")
|
self.assertEqual(c.pathstring, 'A/Bb/Cc')
|
||||||
self.assertEqual(d.pathstring, "A/Bb/Cc/D")
|
self.assertEqual(d.pathstring, 'A/Bb/Cc/D')
|
||||||
|
|
||||||
a.name = "Aa"
|
a.name = 'Aa'
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
self.assertEqual(a.pathstring, "Aa")
|
self.assertEqual(a.pathstring, 'Aa')
|
||||||
self.assertEqual(b.pathstring, "Aa/Bb")
|
self.assertEqual(b.pathstring, 'Aa/Bb')
|
||||||
self.assertEqual(c.pathstring, "Aa/Bb/Cc")
|
self.assertEqual(c.pathstring, 'Aa/Bb/Cc')
|
||||||
self.assertEqual(d.pathstring, "Aa/Bb/Cc/D")
|
self.assertEqual(d.pathstring, 'Aa/Bb/Cc/D')
|
||||||
|
|
||||||
d.name = "Dd"
|
d.name = 'Dd'
|
||||||
d.save()
|
d.save()
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
self.assertEqual(a.pathstring, "Aa")
|
self.assertEqual(a.pathstring, 'Aa')
|
||||||
self.assertEqual(b.pathstring, "Aa/Bb")
|
self.assertEqual(b.pathstring, 'Aa/Bb')
|
||||||
self.assertEqual(c.pathstring, "Aa/Bb/Cc")
|
self.assertEqual(c.pathstring, 'Aa/Bb/Cc')
|
||||||
self.assertEqual(d.pathstring, "Aa/Bb/Cc/Dd")
|
self.assertEqual(d.pathstring, 'Aa/Bb/Cc/Dd')
|
||||||
|
|
||||||
# Test a really long name
|
# Test a really long name
|
||||||
# (it will be clipped to < 250 characters)
|
# (it will be clipped to < 250 characters)
|
||||||
a.name = "A" * 100
|
a.name = 'A' * 100
|
||||||
a.save()
|
a.save()
|
||||||
b.name = "B" * 100
|
b.name = 'B' * 100
|
||||||
b.save()
|
b.save()
|
||||||
c.name = "C" * 100
|
c.name = 'C' * 100
|
||||||
c.save()
|
c.save()
|
||||||
d.name = "D" * 100
|
d.name = 'D' * 100
|
||||||
d.save()
|
d.save()
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
@ -125,19 +125,15 @@ class StockTest(StockTestBase):
|
|||||||
self.assertEqual(len(c.pathstring), 249)
|
self.assertEqual(len(c.pathstring), 249)
|
||||||
self.assertEqual(len(d.pathstring), 249)
|
self.assertEqual(len(d.pathstring), 249)
|
||||||
|
|
||||||
self.assertTrue(d.pathstring.startswith("AAAAAAAA"))
|
self.assertTrue(d.pathstring.startswith('AAAAAAAA'))
|
||||||
self.assertTrue(d.pathstring.endswith("DDDDDDDD"))
|
self.assertTrue(d.pathstring.endswith('DDDDDDDD'))
|
||||||
|
|
||||||
def test_link(self):
|
def test_link(self):
|
||||||
"""Test the link URL field validation"""
|
"""Test the link URL field validation."""
|
||||||
item = StockItem.objects.get(pk=1)
|
item = StockItem.objects.get(pk=1)
|
||||||
|
|
||||||
# Check that invalid URLs fail
|
# Check that invalid URLs fail
|
||||||
for bad_url in [
|
for bad_url in ['test.com', 'httpx://abc.xyz', 'https:google.com']:
|
||||||
'test.com',
|
|
||||||
'httpx://abc.xyz',
|
|
||||||
'https:google.com',
|
|
||||||
]:
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
item.link = bad_url
|
item.link = bad_url
|
||||||
item.save()
|
item.save()
|
||||||
@ -168,52 +164,42 @@ class StockTest(StockTestBase):
|
|||||||
|
|
||||||
@override_settings(EXTRA_URL_SCHEMES=['ssh'])
|
@override_settings(EXTRA_URL_SCHEMES=['ssh'])
|
||||||
def test_exteneded_schema(self):
|
def test_exteneded_schema(self):
|
||||||
"""Test that extended URL schemes are allowed"""
|
"""Test that extended URL schemes are allowed."""
|
||||||
item = StockItem.objects.get(pk=1)
|
item = StockItem.objects.get(pk=1)
|
||||||
item.link = 'ssh://user:pwd@deb.org:223'
|
item.link = 'ssh://user:pwd@deb.org:223'
|
||||||
item.save()
|
item.save()
|
||||||
item.full_clean()
|
item.full_clean()
|
||||||
|
|
||||||
def test_serial_numbers(self):
|
def test_serial_numbers(self):
|
||||||
"""Test serial number uniqueness"""
|
"""Test serial number uniqueness."""
|
||||||
# Ensure that 'global uniqueness' setting is enabled
|
# Ensure that 'global uniqueness' setting is enabled
|
||||||
InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', True, self.user)
|
InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', True, self.user)
|
||||||
|
|
||||||
part_a = Part.objects.create(name='A', description='A part with a description', trackable=True)
|
part_a = Part.objects.create(
|
||||||
part_b = Part.objects.create(name='B', description='B part with a description', trackable=True)
|
name='A', description='A part with a description', trackable=True
|
||||||
|
)
|
||||||
|
part_b = Part.objects.create(
|
||||||
|
name='B', description='B part with a description', trackable=True
|
||||||
|
)
|
||||||
|
|
||||||
# Create a StockItem for part_a
|
# Create a StockItem for part_a
|
||||||
StockItem.objects.create(
|
StockItem.objects.create(part=part_a, quantity=1, serial='ABCDE')
|
||||||
part=part_a,
|
|
||||||
quantity=1,
|
|
||||||
serial='ABCDE',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a StockItem for part_a (but, will error due to identical serial)
|
# Create a StockItem for part_a (but, will error due to identical serial)
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
StockItem.objects.create(
|
StockItem.objects.create(part=part_b, quantity=1, serial='ABCDE')
|
||||||
part=part_b,
|
|
||||||
quantity=1,
|
|
||||||
serial='ABCDE',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Now, allow serial numbers to be duplicated between different parts
|
# Now, allow serial numbers to be duplicated between different parts
|
||||||
InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, self.user)
|
InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, self.user)
|
||||||
|
|
||||||
StockItem.objects.create(
|
StockItem.objects.create(part=part_b, quantity=1, serial='ABCDE')
|
||||||
part=part_b,
|
|
||||||
quantity=1,
|
|
||||||
serial='ABCDE',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_expiry(self):
|
def test_expiry(self):
|
||||||
"""Test expiry date functionality for StockItem model."""
|
"""Test expiry date functionality for StockItem model."""
|
||||||
today = datetime.datetime.now().date()
|
today = datetime.datetime.now().date()
|
||||||
|
|
||||||
item = StockItem.objects.create(
|
item = StockItem.objects.create(
|
||||||
location=self.office,
|
location=self.office, part=Part.objects.get(pk=1), quantity=10
|
||||||
part=Part.objects.get(pk=1),
|
|
||||||
quantity=10,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Without an expiry_date set, item should not be "expired"
|
# Without an expiry_date set, item should not be "expired"
|
||||||
@ -249,13 +235,14 @@ class StockTest(StockTestBase):
|
|||||||
# And there should be *no* items being build
|
# And there should be *no* items being build
|
||||||
self.assertEqual(part.quantity_being_built, 0)
|
self.assertEqual(part.quantity_being_built, 0)
|
||||||
|
|
||||||
build = Build.objects.create(reference='BO-4444', part=part, title='A test build', quantity=1)
|
build = Build.objects.create(
|
||||||
|
reference='BO-4444', part=part, title='A test build', quantity=1
|
||||||
|
)
|
||||||
|
|
||||||
# Add some stock items which are "building"
|
# Add some stock items which are "building"
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
StockItem.objects.create(
|
StockItem.objects.create(
|
||||||
part=part, build=build,
|
part=part, build=build, quantity=10, is_building=True
|
||||||
quantity=10, is_building=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# The "is_building" quantity should not be counted here
|
# The "is_building" quantity should not be counted here
|
||||||
@ -330,7 +317,10 @@ class StockTest(StockTestBase):
|
|||||||
|
|
||||||
# There should be 16 widgets "in stock"
|
# There should be 16 widgets "in stock"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 16
|
StockItem.objects.filter(part=25).aggregate(Sum('quantity'))[
|
||||||
|
'quantity__sum'
|
||||||
|
],
|
||||||
|
16,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_location(self):
|
def test_delete_location(self):
|
||||||
@ -339,7 +329,9 @@ class StockTest(StockTestBase):
|
|||||||
n_stock = StockItem.objects.count()
|
n_stock = StockItem.objects.count()
|
||||||
|
|
||||||
# What parts are in drawer 3?
|
# What parts are in drawer 3?
|
||||||
stock_ids = [part.id for part in StockItem.objects.filter(location=self.drawer3.id)]
|
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.drawer3.delete()
|
self.drawer3.delete()
|
||||||
@ -361,7 +353,9 @@ class StockTest(StockTestBase):
|
|||||||
self.assertEqual(it.location, self.bathroom)
|
self.assertEqual(it.location, self.bathroom)
|
||||||
|
|
||||||
# There now should be 2 lots of screws in the bathroom
|
# There now should be 2 lots of screws in the bathroom
|
||||||
self.assertEqual(StockItem.objects.filter(part=1, location=self.bathroom).count(), 2)
|
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')
|
||||||
@ -463,13 +457,15 @@ class StockTest(StockTestBase):
|
|||||||
self.assertFalse(it.add_stock(-10, None))
|
self.assertFalse(it.add_stock(-10, None))
|
||||||
|
|
||||||
def test_allocate_to_customer(self):
|
def test_allocate_to_customer(self):
|
||||||
"""Test allocating stock to a customer"""
|
"""Test allocating stock to a customer."""
|
||||||
it = StockItem.objects.get(pk=2)
|
it = StockItem.objects.get(pk=2)
|
||||||
n = it.quantity
|
n = it.quantity
|
||||||
an = n - 10
|
an = n - 10
|
||||||
customer = Company.objects.create(name="MyTestCompany")
|
customer = Company.objects.create(name='MyTestCompany')
|
||||||
order = SalesOrder.objects.create(description="Test order")
|
order = SalesOrder.objects.create(description='Test order')
|
||||||
ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock')
|
ait = it.allocateToCustomer(
|
||||||
|
customer, quantity=an, order=order, user=None, notes='Allocated some stock'
|
||||||
|
)
|
||||||
|
|
||||||
# Check if new stockitem is created
|
# Check if new stockitem is created
|
||||||
self.assertTrue(ait)
|
self.assertTrue(ait)
|
||||||
@ -485,29 +481,48 @@ class StockTest(StockTestBase):
|
|||||||
# Check that a tracking item was added
|
# Check that a tracking item was added
|
||||||
track = StockItemTracking.objects.filter(item=ait).latest('id')
|
track = StockItemTracking.objects.filter(item=ait).latest('id')
|
||||||
|
|
||||||
self.assertEqual(track.tracking_type, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER)
|
self.assertEqual(
|
||||||
|
track.tracking_type, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER
|
||||||
|
)
|
||||||
self.assertIn('Allocated some stock', track.notes)
|
self.assertIn('Allocated some stock', track.notes)
|
||||||
|
|
||||||
def test_return_from_customer(self):
|
def test_return_from_customer(self):
|
||||||
"""Test removing previous allocated stock from customer"""
|
"""Test removing previous allocated stock from customer."""
|
||||||
it = StockItem.objects.get(pk=2)
|
it = StockItem.objects.get(pk=2)
|
||||||
|
|
||||||
# First establish total stock for this part
|
# First establish total stock for this part
|
||||||
allstock_before = StockItem.objects.filter(part=it.part).aggregate(Sum("quantity"))["quantity__sum"]
|
allstock_before = StockItem.objects.filter(part=it.part).aggregate(
|
||||||
|
Sum('quantity')
|
||||||
|
)['quantity__sum']
|
||||||
|
|
||||||
n = it.quantity
|
n = it.quantity
|
||||||
an = n - 10
|
an = n - 10
|
||||||
customer = Company.objects.create(name="MyTestCompany")
|
customer = Company.objects.create(name='MyTestCompany')
|
||||||
order = SalesOrder.objects.create(description="Test order")
|
order = SalesOrder.objects.create(description='Test order')
|
||||||
|
|
||||||
ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock')
|
ait = it.allocateToCustomer(
|
||||||
ait.return_from_customer(it.location, None, notes="Stock removed from customer")
|
customer, quantity=an, order=order, user=None, notes='Allocated some stock'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(ait.quantity, an)
|
||||||
|
self.assertTrue(ait.parent, it)
|
||||||
|
|
||||||
|
# There should be only quantity 10x remaining
|
||||||
|
it.refresh_from_db()
|
||||||
|
self.assertEqual(it.quantity, 10)
|
||||||
|
|
||||||
|
ait.return_from_customer(it.location, None, notes='Stock removed from customer')
|
||||||
|
|
||||||
# When returned stock is returned to its original (parent) location, check that the parent has correct quantity
|
# When returned stock is returned to its original (parent) location, check that the parent has correct quantity
|
||||||
|
it.refresh_from_db()
|
||||||
self.assertEqual(it.quantity, n)
|
self.assertEqual(it.quantity, n)
|
||||||
|
|
||||||
ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock')
|
ait = it.allocateToCustomer(
|
||||||
ait.return_from_customer(self.drawer3, None, notes="Stock removed from customer")
|
customer, quantity=an, order=order, user=None, notes='Allocated some stock'
|
||||||
|
)
|
||||||
|
ait.return_from_customer(
|
||||||
|
self.drawer3, None, notes='Stock removed from customer'
|
||||||
|
)
|
||||||
|
|
||||||
# Check correct assignment of the new location
|
# Check correct assignment of the new location
|
||||||
self.assertEqual(ait.location, self.drawer3)
|
self.assertEqual(ait.location, self.drawer3)
|
||||||
@ -527,7 +542,9 @@ class StockTest(StockTestBase):
|
|||||||
self.assertIn('Stock removed from customer', track.notes)
|
self.assertIn('Stock removed from customer', track.notes)
|
||||||
|
|
||||||
# Establish total stock for the part after remove from customer to check that we still have the correct quantity in stock
|
# Establish total stock for the part after remove from customer to check that we still have the correct quantity in stock
|
||||||
allstock_after = StockItem.objects.filter(part=it.part).aggregate(Sum("quantity"))["quantity__sum"]
|
allstock_after = StockItem.objects.filter(part=it.part).aggregate(
|
||||||
|
Sum('quantity')
|
||||||
|
)['quantity__sum']
|
||||||
self.assertEqual(allstock_before, allstock_after)
|
self.assertEqual(allstock_before, allstock_after)
|
||||||
|
|
||||||
def test_take_stock(self):
|
def test_take_stock(self):
|
||||||
@ -578,10 +595,7 @@ class StockTest(StockTestBase):
|
|||||||
# Ensure we do not have unique serials enabled
|
# Ensure we do not have unique serials enabled
|
||||||
InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, None)
|
InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, None)
|
||||||
|
|
||||||
item = StockItem.objects.create(
|
item = StockItem.objects.create(part=p, quantity=1)
|
||||||
part=p,
|
|
||||||
quantity=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertFalse(item.serialized)
|
self.assertFalse(item.serialized)
|
||||||
|
|
||||||
@ -609,10 +623,7 @@ class StockTest(StockTestBase):
|
|||||||
trackable=True,
|
trackable=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
item = StockItem.objects.create(
|
item = StockItem.objects.create(part=p, quantity=1)
|
||||||
part=p,
|
|
||||||
quantity=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
for sn in [12345, '12345', ' 12345 ']:
|
for sn in [12345, '12345', ' 12345 ']:
|
||||||
item.serial = sn
|
item.serial = sn
|
||||||
@ -620,7 +631,7 @@ class StockTest(StockTestBase):
|
|||||||
|
|
||||||
self.assertEqual(item.serial_int, 12345)
|
self.assertEqual(item.serial_int, 12345)
|
||||||
|
|
||||||
item.serial = "-123"
|
item.serial = '-123'
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
# Negative number should map to positive value
|
# Negative number should map to positive value
|
||||||
@ -631,7 +642,7 @@ class StockTest(StockTestBase):
|
|||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
# The 'integer' portion has been clipped to a maximum value
|
# The 'integer' portion has been clipped to a maximum value
|
||||||
self.assertEqual(item.serial_int, 0x7fffffff)
|
self.assertEqual(item.serial_int, 0x7FFFFFFF)
|
||||||
|
|
||||||
# Non-numeric values should encode to zero
|
# Non-numeric values should encode to zero
|
||||||
for sn in ['apple', 'banana', 'carrot']:
|
for sn in ['apple', 'banana', 'carrot']:
|
||||||
@ -644,30 +655,18 @@ class StockTest(StockTestBase):
|
|||||||
item.serial = 100
|
item.serial = 100
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
item_next = StockItem.objects.create(
|
item_next = StockItem.objects.create(part=p, serial=150, quantity=1)
|
||||||
part=p,
|
|
||||||
serial=150,
|
|
||||||
quantity=1
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(item.get_next_serialized_item(), item_next)
|
self.assertEqual(item.get_next_serialized_item(), item_next)
|
||||||
|
|
||||||
item_prev = StockItem.objects.create(
|
item_prev = StockItem.objects.create(part=p, serial=' 57', quantity=1)
|
||||||
part=p,
|
|
||||||
serial=' 57',
|
|
||||||
quantity=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(item.get_next_serialized_item(reverse=True), item_prev)
|
self.assertEqual(item.get_next_serialized_item(reverse=True), item_prev)
|
||||||
|
|
||||||
# Create a number of serialized stock items around the current item
|
# Create a number of serialized stock items around the current item
|
||||||
for i in range(75, 125):
|
for i in range(75, 125):
|
||||||
try:
|
try:
|
||||||
StockItem.objects.create(
|
StockItem.objects.create(part=p, serial=i, quantity=1)
|
||||||
part=p,
|
|
||||||
serial=i,
|
|
||||||
quantity=1,
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -696,14 +695,14 @@ class StockTest(StockTestBase):
|
|||||||
|
|
||||||
# Try an invalid quantity
|
# Try an invalid quantity
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
item.serializeStock("k", [], self.user)
|
item.serializeStock('k', [], self.user)
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
item.serializeStock(-1, [], self.user)
|
item.serializeStock(-1, [], self.user)
|
||||||
|
|
||||||
# Not enough serial numbers for all stock items.
|
# Not enough serial numbers for all stock items.
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
item.serializeStock(3, "hello", self.user)
|
item.serializeStock(3, 'hello', self.user)
|
||||||
|
|
||||||
def test_serialize_stock_valid(self):
|
def test_serialize_stock_valid(self):
|
||||||
"""Perform valid stock serializations."""
|
"""Perform valid stock serializations."""
|
||||||
@ -755,55 +754,25 @@ class StockTest(StockTestBase):
|
|||||||
"""
|
"""
|
||||||
# First, we will create a stock location structure
|
# First, we will create a stock location structure
|
||||||
|
|
||||||
A = StockLocation.objects.create(
|
A = StockLocation.objects.create(name='A', description='Top level location')
|
||||||
name='A',
|
|
||||||
description='Top level location'
|
|
||||||
)
|
|
||||||
|
|
||||||
B1 = StockLocation.objects.create(
|
B1 = StockLocation.objects.create(name='B1', parent=A)
|
||||||
name='B1',
|
|
||||||
parent=A
|
|
||||||
)
|
|
||||||
|
|
||||||
B2 = StockLocation.objects.create(
|
B2 = StockLocation.objects.create(name='B2', parent=A)
|
||||||
name='B2',
|
|
||||||
parent=A
|
|
||||||
)
|
|
||||||
|
|
||||||
B3 = StockLocation.objects.create(
|
B3 = StockLocation.objects.create(name='B3', parent=A)
|
||||||
name='B3',
|
|
||||||
parent=A
|
|
||||||
)
|
|
||||||
|
|
||||||
C11 = StockLocation.objects.create(
|
C11 = StockLocation.objects.create(name='C11', parent=B1)
|
||||||
name='C11',
|
|
||||||
parent=B1,
|
|
||||||
)
|
|
||||||
|
|
||||||
C12 = StockLocation.objects.create(
|
C12 = StockLocation.objects.create(name='C12', parent=B1)
|
||||||
name='C12',
|
|
||||||
parent=B1,
|
|
||||||
)
|
|
||||||
|
|
||||||
C21 = StockLocation.objects.create(
|
C21 = StockLocation.objects.create(name='C21', parent=B2)
|
||||||
name='C21',
|
|
||||||
parent=B2,
|
|
||||||
)
|
|
||||||
|
|
||||||
C22 = StockLocation.objects.create(
|
C22 = StockLocation.objects.create(name='C22', parent=B2)
|
||||||
name='C22',
|
|
||||||
parent=B2,
|
|
||||||
)
|
|
||||||
|
|
||||||
C31 = StockLocation.objects.create(
|
C31 = StockLocation.objects.create(name='C31', parent=B3)
|
||||||
name='C31',
|
|
||||||
parent=B3,
|
|
||||||
)
|
|
||||||
|
|
||||||
C32 = StockLocation.objects.create(
|
C32 = StockLocation.objects.create(name='C32', parent=B3)
|
||||||
name='C32',
|
|
||||||
parent=B3
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check that the tree_id is correct for each sublocation
|
# Check that the tree_id is correct for each sublocation
|
||||||
for loc in [B1, B2, B3, C11, C12, C21, C22, C31, C32]:
|
for loc in [B1, B2, B3, C11, C12, C21, C22, C31, C32]:
|
||||||
@ -850,9 +819,7 @@ class StockTest(StockTestBase):
|
|||||||
# Add some stock items to B3
|
# Add some stock items to B3
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
StockItem.objects.create(
|
StockItem.objects.create(
|
||||||
part=Part.objects.get(pk=1),
|
part=Part.objects.get(pk=1), quantity=10, location=B3
|
||||||
quantity=10,
|
|
||||||
location=B3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(StockItem.objects.filter(location=B3).count(), 10)
|
self.assertEqual(StockItem.objects.filter(location=B3).count(), 10)
|
||||||
@ -930,10 +897,10 @@ class StockTest(StockTestBase):
|
|||||||
|
|
||||||
|
|
||||||
class StockBarcodeTest(StockTestBase):
|
class StockBarcodeTest(StockTestBase):
|
||||||
"""Run barcode tests for the stock app"""
|
"""Run barcode tests for the stock app."""
|
||||||
|
|
||||||
def test_stock_item_barcode_basics(self):
|
def test_stock_item_barcode_basics(self):
|
||||||
"""Simple tests for the StockItem barcode integration"""
|
"""Simple tests for the StockItem barcode integration."""
|
||||||
item = StockItem.objects.get(pk=1)
|
item = StockItem.objects.get(pk=1)
|
||||||
|
|
||||||
self.assertEqual(StockItem.barcode_model_type(), 'stockitem')
|
self.assertEqual(StockItem.barcode_model_type(), 'stockitem')
|
||||||
@ -949,7 +916,7 @@ class StockBarcodeTest(StockTestBase):
|
|||||||
self.assertEqual(barcode, '{"stockitem": 1}')
|
self.assertEqual(barcode, '{"stockitem": 1}')
|
||||||
|
|
||||||
def test_location_barcode_basics(self):
|
def test_location_barcode_basics(self):
|
||||||
"""Simple tests for the StockLocation barcode integration"""
|
"""Simple tests for the StockLocation barcode integration."""
|
||||||
self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation')
|
self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation')
|
||||||
|
|
||||||
loc = StockLocation.objects.get(pk=1)
|
loc = StockLocation.objects.get(pk=1)
|
||||||
@ -982,7 +949,10 @@ class VariantTest(StockTestBase):
|
|||||||
chair = Part.objects.get(pk=10000)
|
chair = Part.objects.get(pk=10000)
|
||||||
|
|
||||||
# Operations on the top-level object
|
# Operations on the top-level object
|
||||||
[self.assertFalse(chair.validate_serial_number(i)) for i in [1, 2, 3, 4, 5, 20, 21, 22]]
|
[
|
||||||
|
self.assertFalse(chair.validate_serial_number(i))
|
||||||
|
for i in [1, 2, 3, 4, 5, 20, 21, 22]
|
||||||
|
]
|
||||||
|
|
||||||
self.assertFalse(chair.validate_serial_number(20))
|
self.assertFalse(chair.validate_serial_number(20))
|
||||||
self.assertFalse(chair.validate_serial_number(21))
|
self.assertFalse(chair.validate_serial_number(21))
|
||||||
@ -1006,11 +976,7 @@ class VariantTest(StockTestBase):
|
|||||||
# Create a new serial number
|
# Create a new serial number
|
||||||
n = variant.get_latest_serial_number()
|
n = variant.get_latest_serial_number()
|
||||||
|
|
||||||
item = StockItem(
|
item = StockItem(part=variant, quantity=1, serial=n)
|
||||||
part=variant,
|
|
||||||
quantity=1,
|
|
||||||
serial=n
|
|
||||||
)
|
|
||||||
|
|
||||||
# This should fail
|
# This should fail
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@ -1031,6 +997,63 @@ class VariantTest(StockTestBase):
|
|||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
class StockTreeTest(StockTestBase):
|
||||||
|
"""Unit test for StockItem tree structure."""
|
||||||
|
|
||||||
|
def test_stock_split(self):
|
||||||
|
"""Test that stock splitting works correctly."""
|
||||||
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
|
part = Part.objects.create(name='My part', description='My part description')
|
||||||
|
location = StockLocation.objects.create(name='Test Location')
|
||||||
|
|
||||||
|
# Create an initial stock item
|
||||||
|
item = StockItem.objects.create(part=part, quantity=1000, location=location)
|
||||||
|
|
||||||
|
# Test that the initial MPTT values are correct
|
||||||
|
self.assertEqual(item.level, 0)
|
||||||
|
self.assertEqual(item.lft, 1)
|
||||||
|
self.assertEqual(item.rght, 2)
|
||||||
|
|
||||||
|
children = []
|
||||||
|
|
||||||
|
self.assertEqual(item.get_descendants(include_self=False).count(), 0)
|
||||||
|
self.assertEqual(item.get_descendants(include_self=True).count(), 1)
|
||||||
|
|
||||||
|
# Create child items by splitting stock
|
||||||
|
for idx in range(10):
|
||||||
|
child = item.splitStock(50, None, None)
|
||||||
|
children.append(child)
|
||||||
|
|
||||||
|
# Check that the child item has been correctly created
|
||||||
|
self.assertEqual(child.parent.pk, item.pk)
|
||||||
|
self.assertEqual(child.tree_id, item.tree_id)
|
||||||
|
self.assertEqual(child.level, 1)
|
||||||
|
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.get_children().count(), idx + 1)
|
||||||
|
self.assertEqual(item.get_descendants(include_self=True).count(), idx + 2)
|
||||||
|
|
||||||
|
item.refresh_from_db()
|
||||||
|
n = item.get_descendants(include_self=True).count()
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
# Create multiple sub-childs
|
||||||
|
for _idx in range(3):
|
||||||
|
sub_child = child.splitStock(10, None, None)
|
||||||
|
self.assertEqual(sub_child.parent.pk, child.pk)
|
||||||
|
self.assertEqual(sub_child.tree_id, child.tree_id)
|
||||||
|
self.assertEqual(sub_child.level, 2)
|
||||||
|
|
||||||
|
self.assertEqual(sub_child.get_ancestors(include_self=True).count(), 3)
|
||||||
|
|
||||||
|
child.refresh_from_db()
|
||||||
|
self.assertEqual(child.get_descendants(include_self=True).count(), 4)
|
||||||
|
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.get_descendants(include_self=True).count(), n + 30)
|
||||||
|
|
||||||
|
|
||||||
class TestResultTest(StockTestBase):
|
class TestResultTest(StockTestBase):
|
||||||
"""Tests for the StockItemTestResult model."""
|
"""Tests for the StockItemTestResult model."""
|
||||||
|
|
||||||
@ -1040,7 +1063,7 @@ class TestResultTest(StockTestBase):
|
|||||||
tests = item.test_results
|
tests = item.test_results
|
||||||
self.assertEqual(tests.count(), 4)
|
self.assertEqual(tests.count(), 4)
|
||||||
|
|
||||||
results = item.getTestResults(test="Temperature Test")
|
results = item.getTestResults(test='Temperature Test')
|
||||||
self.assertEqual(results.count(), 2)
|
self.assertEqual(results.count(), 2)
|
||||||
|
|
||||||
# Passing tests
|
# Passing tests
|
||||||
@ -1074,9 +1097,7 @@ class TestResultTest(StockTestBase):
|
|||||||
test.save()
|
test.save()
|
||||||
|
|
||||||
StockItemTestResult.objects.create(
|
StockItemTestResult.objects.create(
|
||||||
stock_item=item,
|
stock_item=item, test='sew cushion', result=True
|
||||||
test='sew cushion',
|
|
||||||
result=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Still should be failing at this point,
|
# Still should be failing at this point,
|
||||||
@ -1088,7 +1109,7 @@ class TestResultTest(StockTestBase):
|
|||||||
stock_item=item,
|
stock_item=item,
|
||||||
test='apply paint',
|
test='apply paint',
|
||||||
date=datetime.datetime(2022, 12, 12),
|
date=datetime.datetime(2022, 12, 12),
|
||||||
result=True
|
result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(item.passedAllRequiredTests())
|
self.assertTrue(item.passedAllRequiredTests())
|
||||||
@ -1096,6 +1117,9 @@ class TestResultTest(StockTestBase):
|
|||||||
def test_duplicate_item_tests(self):
|
def test_duplicate_item_tests(self):
|
||||||
"""Test duplicate item behaviour."""
|
"""Test duplicate item behaviour."""
|
||||||
# Create an example stock item by copying one from the database (because we are lazy)
|
# Create an example stock item by copying one from the database (because we are lazy)
|
||||||
|
|
||||||
|
StockItem.objects.rebuild()
|
||||||
|
|
||||||
item = StockItem.objects.get(pk=522)
|
item = StockItem.objects.get(pk=522)
|
||||||
|
|
||||||
item.pk = None
|
item.pk = None
|
||||||
@ -1103,32 +1127,25 @@ class TestResultTest(StockTestBase):
|
|||||||
item.quantity = 50
|
item.quantity = 50
|
||||||
|
|
||||||
# Try with an invalid batch code (according to sample validatoin plugin)
|
# Try with an invalid batch code (according to sample validatoin plugin)
|
||||||
item.batch = "X234"
|
item.batch = 'X234'
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
item.batch = "B123"
|
item.batch = 'B123'
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
# Do some tests!
|
# Do some tests!
|
||||||
StockItemTestResult.objects.create(
|
StockItemTestResult.objects.create(
|
||||||
stock_item=item,
|
stock_item=item, test='Firmware', result=True
|
||||||
test="Firmware",
|
|
||||||
result=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
StockItemTestResult.objects.create(
|
StockItemTestResult.objects.create(
|
||||||
stock_item=item,
|
stock_item=item, test='Paint Color', result=True, value='Red'
|
||||||
test="Paint Color",
|
|
||||||
result=True,
|
|
||||||
value="Red"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
StockItemTestResult.objects.create(
|
StockItemTestResult.objects.create(
|
||||||
stock_item=item,
|
stock_item=item, test='Applied Sticker', result=False
|
||||||
test="Applied Sticker",
|
|
||||||
result=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(item.test_results.count(), 3)
|
self.assertEqual(item.test_results.count(), 3)
|
||||||
@ -1142,10 +1159,7 @@ class TestResultTest(StockTestBase):
|
|||||||
self.assertEqual(item.test_results.count(), 3)
|
self.assertEqual(item.test_results.count(), 3)
|
||||||
self.assertEqual(item2.test_results.count(), 3)
|
self.assertEqual(item2.test_results.count(), 3)
|
||||||
|
|
||||||
StockItemTestResult.objects.create(
|
StockItemTestResult.objects.create(stock_item=item2, test='A new test')
|
||||||
stock_item=item2,
|
|
||||||
test='A new test'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(item.test_results.count(), 3)
|
self.assertEqual(item.test_results.count(), 3)
|
||||||
self.assertEqual(item2.test_results.count(), 4)
|
self.assertEqual(item2.test_results.count(), 4)
|
||||||
@ -1154,10 +1168,7 @@ class TestResultTest(StockTestBase):
|
|||||||
item2.serializeStock(1, [100], self.user)
|
item2.serializeStock(1, [100], self.user)
|
||||||
|
|
||||||
# Add a test result to the parent *after* serialization
|
# Add a test result to the parent *after* serialization
|
||||||
StockItemTestResult.objects.create(
|
StockItemTestResult.objects.create(stock_item=item2, test='abcde')
|
||||||
stock_item=item2,
|
|
||||||
test='abcde'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(item2.test_results.count(), 5)
|
self.assertEqual(item2.test_results.count(), 5)
|
||||||
|
|
||||||
@ -1182,10 +1193,7 @@ class TestResultTest(StockTestBase):
|
|||||||
|
|
||||||
# Create a stock item which is installed *inside* the master item
|
# Create a stock item which is installed *inside* the master item
|
||||||
sub_item = StockItem.objects.create(
|
sub_item = StockItem.objects.create(
|
||||||
part=item.part,
|
part=item.part, quantity=1, belongs_to=item, location=None
|
||||||
quantity=1,
|
|
||||||
belongs_to=item,
|
|
||||||
location=None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Now, create some test results against the sub item
|
# Now, create some test results against the sub item
|
||||||
@ -1195,7 +1203,7 @@ class TestResultTest(StockTestBase):
|
|||||||
stock_item=sub_item,
|
stock_item=sub_item,
|
||||||
test='firmware version',
|
test='firmware version',
|
||||||
date=datetime.datetime.now().date(),
|
date=datetime.datetime.now().date(),
|
||||||
result=True
|
result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should return the same number of tests as before
|
# Should return the same number of tests as before
|
||||||
|
Loading…
x
Reference in New Issue
Block a user