mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-18 12:36:11 +00:00
Fix StockItem parent update on delete (#12377)
* Fix StockItem parent update on delete * Check for null parents
This commit is contained in:
@@ -550,6 +550,9 @@ class StockItem(
|
|||||||
def delete(self, ignore_serial_check: bool = False, **kwargs):
|
def delete(self, ignore_serial_check: bool = False, **kwargs):
|
||||||
"""Custom delete method for StockItem model.
|
"""Custom delete method for StockItem model.
|
||||||
|
|
||||||
|
Any child items are re-linked to the parent of this item,
|
||||||
|
to preserve the stock item genealogy chain.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
ignore_serial_check: If True, allow deletion of serialized stock items regardless of global setting
|
ignore_serial_check: If True, allow deletion of serialized stock items regardless of global setting
|
||||||
"""
|
"""
|
||||||
@@ -559,6 +562,13 @@ class StockItem(
|
|||||||
if self.serialized:
|
if self.serialized:
|
||||||
raise ValidationError(_('Serialized stock items cannot be deleted'))
|
raise ValidationError(_('Serialized stock items cannot be deleted'))
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# Re-link any child items to the parent of this item,
|
||||||
|
# so the genealogy chain survives deletion of an intermediate item
|
||||||
|
|
||||||
|
parent = StockItem.objects.filter(pk=self.parent_id).first()
|
||||||
|
StockItem.objects.filter(parent=self).update(parent=parent)
|
||||||
|
|
||||||
super().delete(**kwargs)
|
super().delete(**kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1036,7 +1046,7 @@ class StockItem(
|
|||||||
"""Returns part name."""
|
"""Returns part name."""
|
||||||
return self.part.full_name
|
return self.part.full_name
|
||||||
|
|
||||||
# Note: When a StockItem is deleted, a pre_delete signal handles the parent/child relationship
|
# Note: When a StockItem is deleted, child items are re-linked to its parent (see delete())
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
'stock.StockItem',
|
'stock.StockItem',
|
||||||
verbose_name=_('Parent Stock Item'),
|
verbose_name=_('Parent Stock Item'),
|
||||||
|
|||||||
@@ -351,6 +351,71 @@ 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_delete_reparents_children(self):
|
||||||
|
"""Test that deleting an intermediate item re-links children to the grandparent."""
|
||||||
|
grandparent = StockItem.objects.get(id=1234)
|
||||||
|
|
||||||
|
parent = grandparent.splitStock(200, None, self.user)
|
||||||
|
child_a = parent.splitStock(50, None, self.user)
|
||||||
|
child_b = parent.splitStock(50, None, self.user)
|
||||||
|
|
||||||
|
self.assertEqual(parent.parent, grandparent)
|
||||||
|
self.assertEqual(child_a.parent, parent)
|
||||||
|
self.assertEqual(child_b.parent, parent)
|
||||||
|
|
||||||
|
# Deleting the intermediate item grafts its children onto the grandparent
|
||||||
|
parent.delete()
|
||||||
|
|
||||||
|
child_a.refresh_from_db()
|
||||||
|
child_b.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(child_a.parent, grandparent)
|
||||||
|
self.assertEqual(child_b.parent, grandparent)
|
||||||
|
|
||||||
|
# Deleting a top-level item leaves its children with no parent
|
||||||
|
grandparent.delete()
|
||||||
|
|
||||||
|
child_a.refresh_from_db()
|
||||||
|
child_b.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsNone(child_a.parent)
|
||||||
|
self.assertIsNone(child_b.parent)
|
||||||
|
|
||||||
|
def test_implicit_delete_reparents_children(self):
|
||||||
|
"""Test child re-linking when items are deleted by depletion or merging."""
|
||||||
|
grandparent = StockItem.objects.get(id=1234)
|
||||||
|
|
||||||
|
# An item depleted to zero with delete_on_deplete set is deleted
|
||||||
|
parent = grandparent.splitStock(200, None, self.user)
|
||||||
|
child = parent.splitStock(50, None, self.user)
|
||||||
|
|
||||||
|
parent.delete_on_deplete = True
|
||||||
|
parent.save()
|
||||||
|
|
||||||
|
self.assertTrue(parent.take_stock(150, self.user, notes='Deplete'))
|
||||||
|
self.assertFalse(StockItem.objects.filter(pk=parent.pk).exists())
|
||||||
|
|
||||||
|
child.refresh_from_db()
|
||||||
|
self.assertEqual(child.parent, grandparent)
|
||||||
|
|
||||||
|
# An item absorbed by a merge is also deleted
|
||||||
|
source = grandparent.splitStock(100, None, self.user)
|
||||||
|
kid = source.splitStock(25, None, self.user)
|
||||||
|
|
||||||
|
target = StockItem.objects.create(
|
||||||
|
part=grandparent.part,
|
||||||
|
supplier_part=grandparent.supplier_part,
|
||||||
|
quantity=10,
|
||||||
|
location=grandparent.location,
|
||||||
|
)
|
||||||
|
|
||||||
|
target.merge_stock_items([source], raise_error=True, user=self.user)
|
||||||
|
|
||||||
|
self.assertFalse(StockItem.objects.filter(pk=source.pk).exists())
|
||||||
|
|
||||||
|
kid.refresh_from_db()
|
||||||
|
self.assertEqual(kid.parent, grandparent)
|
||||||
|
|
||||||
def test_over_adjustment_quantities(self):
|
def test_over_adjustment_quantities(self):
|
||||||
"""Stock adjustments are clamped to the available stock quantity.
|
"""Stock adjustments are clamped to the available stock quantity.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user