mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-18 04:33:48 +00:00
[bug] Quantity fixes (#12375)
- Ensure valid quantity values on stock operations - Regression testing
This commit is contained in:
@@ -1376,9 +1376,16 @@ class StockItem(
|
|||||||
user: User that performed the action
|
user: User that performed the action
|
||||||
notes: Notes field
|
notes: Notes field
|
||||||
"""
|
"""
|
||||||
if quantity is None:
|
if quantity is None or self.serialized:
|
||||||
quantity = self.quantity
|
quantity = self.quantity
|
||||||
|
|
||||||
|
# Cannot allocate more than the available quantity
|
||||||
|
# (also ensures the recorded history matches the actual allocation)
|
||||||
|
quantity = min(quantity, self.quantity)
|
||||||
|
|
||||||
|
if quantity <= 0:
|
||||||
|
raise ValidationError({'quantity': _('Quantity must be greater than zero')})
|
||||||
|
|
||||||
if quantity >= self.quantity:
|
if quantity >= self.quantity:
|
||||||
item = self
|
item = self
|
||||||
else:
|
else:
|
||||||
@@ -1708,6 +1715,18 @@ class StockItem(
|
|||||||
notes: Any notes associated with the operation
|
notes: Any notes associated with the operation
|
||||||
build: The BuildOrder to associate with the operation (optional)
|
build: The BuildOrder to associate with the operation (optional)
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
quantity = Decimal(quantity)
|
||||||
|
except (InvalidOperation, TypeError):
|
||||||
|
raise ValidationError({'quantity': _('Invalid quantity value')})
|
||||||
|
|
||||||
|
if quantity <= 0:
|
||||||
|
raise ValidationError({'quantity': _('Quantity must be greater than zero')})
|
||||||
|
|
||||||
|
# Cannot install more than the available quantity
|
||||||
|
# (also ensures the recorded history matches the actual installation)
|
||||||
|
quantity = min(quantity, other_item.quantity)
|
||||||
|
|
||||||
# If the quantity is less than the stock item, split the stock!
|
# If the quantity is less than the stock item, split the stock!
|
||||||
stock_item = other_item.splitStock(quantity, None, user)
|
stock_item = other_item.splitStock(quantity, None, user)
|
||||||
|
|
||||||
@@ -3065,6 +3084,10 @@ class StockItem(
|
|||||||
except InvalidOperation:
|
except InvalidOperation:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Cannot remove more than the available quantity
|
||||||
|
# (also ensures the recorded history matches the actual removal)
|
||||||
|
quantity = min(quantity, self.quantity)
|
||||||
|
|
||||||
if quantity <= 0:
|
if quantity <= 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -351,6 +351,92 @@ class StockTest(StockTestBase):
|
|||||||
stock.splitStock(stock.quantity, None, self.user)
|
stock.splitStock(stock.quantity, None, self.user)
|
||||||
self.assertEqual(StockItem.objects.filter(part=3).count(), n + 1)
|
self.assertEqual(StockItem.objects.filter(part=3).count(), n + 1)
|
||||||
|
|
||||||
|
def test_over_adjustment_quantities(self):
|
||||||
|
"""Stock adjustments are clamped to the available stock quantity.
|
||||||
|
|
||||||
|
Regression test: take_stock / allocateToCustomer / installStockItem
|
||||||
|
previously recorded the *requested* quantity in the stock history,
|
||||||
|
even when less stock was actually removed / allocated / installed.
|
||||||
|
"""
|
||||||
|
part = Part.objects.create(
|
||||||
|
name='Clamp part',
|
||||||
|
description='A part for quantity clamping tests',
|
||||||
|
salable=True,
|
||||||
|
component=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- take_stock: remove more than available ---
|
||||||
|
item = StockItem.objects.create(part=part, quantity=10, delete_on_deplete=False)
|
||||||
|
|
||||||
|
self.assertTrue(item.take_stock(25, self.user, notes='over-remove'))
|
||||||
|
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.quantity, 0)
|
||||||
|
|
||||||
|
# History records the quantity which was *actually* removed
|
||||||
|
entry = item.tracking_info.latest('pk')
|
||||||
|
self.assertEqual(entry.deltas['removed'], 10.0)
|
||||||
|
self.assertEqual(entry.deltas['quantity'], 0.0)
|
||||||
|
|
||||||
|
# Removing stock from an empty item fails, and adds no history
|
||||||
|
n = item.tracking_info.count()
|
||||||
|
self.assertFalse(item.take_stock(5, self.user))
|
||||||
|
self.assertEqual(item.tracking_info.count(), n)
|
||||||
|
|
||||||
|
# --- allocateToCustomer: allocate more than available ---
|
||||||
|
customer = Company.objects.create(name='Clamp customer', is_customer=True)
|
||||||
|
|
||||||
|
item = StockItem.objects.create(part=part, quantity=10)
|
||||||
|
|
||||||
|
allocated = item.allocateToCustomer(customer, quantity=25, user=self.user)
|
||||||
|
|
||||||
|
# The whole item is allocated (no split occurs)
|
||||||
|
self.assertEqual(allocated.pk, item.pk)
|
||||||
|
self.assertEqual(allocated.customer, customer)
|
||||||
|
|
||||||
|
# History records the quantity which was *actually* allocated
|
||||||
|
entry = allocated.tracking_info.latest('pk')
|
||||||
|
self.assertEqual(entry.deltas['quantity'], 10.0)
|
||||||
|
|
||||||
|
# A zero quantity is rejected outright
|
||||||
|
item = StockItem.objects.create(part=part, quantity=10)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.allocateToCustomer(customer, quantity=0, user=self.user)
|
||||||
|
|
||||||
|
# --- installStockItem: install more than available ---
|
||||||
|
assembly = Part.objects.create(
|
||||||
|
name='Clamp assembly',
|
||||||
|
description='An assembly for quantity clamping tests',
|
||||||
|
assembly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
parent_item = StockItem.objects.create(part=assembly, quantity=1)
|
||||||
|
component = StockItem.objects.create(part=part, quantity=10)
|
||||||
|
|
||||||
|
parent_item.installStockItem(component, 25, self.user, 'over-install')
|
||||||
|
|
||||||
|
component.refresh_from_db()
|
||||||
|
self.assertEqual(component.belongs_to, parent_item)
|
||||||
|
self.assertEqual(component.quantity, 10)
|
||||||
|
|
||||||
|
# Both history entries record the quantity which was *actually* installed
|
||||||
|
entry = component.tracking_info.filter(
|
||||||
|
tracking_type=StockHistoryCode.INSTALLED_INTO_ASSEMBLY
|
||||||
|
).latest('pk')
|
||||||
|
self.assertEqual(entry.deltas['quantity'], 10.0)
|
||||||
|
|
||||||
|
entry = parent_item.tracking_info.filter(
|
||||||
|
tracking_type=StockHistoryCode.INSTALLED_CHILD_ITEM
|
||||||
|
).latest('pk')
|
||||||
|
self.assertEqual(entry.deltas['quantity'], 10.0)
|
||||||
|
|
||||||
|
# A zero quantity is rejected outright
|
||||||
|
other = StockItem.objects.create(part=part, quantity=5)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
parent_item.installStockItem(other, 0, self.user, 'bad install')
|
||||||
|
|
||||||
def test_uninstall_into_structural_location(self):
|
def test_uninstall_into_structural_location(self):
|
||||||
"""Test that an item cannot be uninstalled into a structural location."""
|
"""Test that an item cannot be uninstalled into a structural location."""
|
||||||
parent = StockItem.objects.get(pk=1)
|
parent = StockItem.objects.get(pk=1)
|
||||||
|
|||||||
Reference in New Issue
Block a user