From 9f01acaa66ff86723d966ad277425ed2c1ec44ae Mon Sep 17 00:00:00 2001 From: Neil Beukes Date: Tue, 26 May 2026 17:30:17 +0000 Subject: [PATCH] fix history when merging --- src/backend/InvenTree/stock/models.py | 17 ++++++++++------- src/backend/InvenTree/stock/serializers.py | 5 ++--- src/backend/InvenTree/stock/test_api.py | 20 +++++++++++++------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index d3274e6a47..42eff73c01 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -2185,7 +2185,8 @@ class StockItem( return None candidates = list( - StockItem.objects.filter(part=self.part, location=location) + StockItem.objects + .filter(part=self.part, location=location) .exclude(pk=self.pk) .order_by('pk') ) @@ -2214,8 +2215,11 @@ class StockItem( *This* stock item subsumes the other, which is essentially deleted: - The quantity of this StockItem is increased - - Tracking history for the *other* item is copied to this item + - Tracking history for the *other* item is copied to this item (unless copy_history=False) - Any allocations (build order, sales order) are moved to this StockItem + + kwargs: + copy_history: If True (default), copy tracking from merged items. Set False for merge-on-transfer. """ if isinstance(other_items, StockItem): other_items = [other_items] @@ -2229,6 +2233,7 @@ class StockItem( user = kwargs.get('user') location = kwargs.get('location', self.location) notes = kwargs.get('notes') or '' + copy_history = kwargs.pop('copy_history', True) parent_id = self.parent.pk if self.parent else None @@ -2270,17 +2275,15 @@ class StockItem( self.parent = None self.save() - self.copyHistoryFrom(other) + if copy_history: + self.copyHistoryFrom(other) if other.location: location_note = _('Transferred from %(location)s') % { 'location': other.location.pathstring } - if notes: - notes = f'{notes}\n{location_note}' - else: - notes = location_note + notes = f'{notes}\n{location_note}' if notes else location_note other.delete() diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 6b857ac403..fe43a8047c 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -1883,6 +1883,7 @@ class StockTransferSerializer(StockAdjustmentSerializer): 'location': location, 'notes': notes, 'user': request.user, + 'copy_history': False, **kwargs, } @@ -1897,9 +1898,7 @@ class StockTransferSerializer(StockAdjustmentSerializer): ) target.merge_stock_items([piece], **merge_kwargs) else: - target.merge_stock_items( - [stock_item], **merge_kwargs - ) + target.merge_stock_items([stock_item], **merge_kwargs) continue diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index a4961d829d..67eb591c12 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -2166,6 +2166,9 @@ class StockTransferMergeTest(StockAPITestCase): self.source_loc = StockLocation.objects.get(pk=5) self.url = reverse('api-stock-transfer') + # Remove fixture stock at the destination so merge targets are deterministic + StockItem.objects.filter(part=self.part, location=self.dest).delete() + def test_transfer_without_merge_creates_separate_lot(self): """Transfer without merge leaves multiple stock rows at destination.""" existing = StockItem.objects.create( @@ -2219,8 +2222,8 @@ class StockTransferMergeTest(StockAPITestCase): self.assertEqual(existing.quantity, 150) self.assertFalse(StockItem.objects.filter(pk=incoming.pk).exists()) - def test_transfer_merge_preserves_tracking(self): - """Transfer merge copies tracking history onto the surviving stock item.""" + def test_transfer_merge_does_not_copy_source_tracking(self): + """Transfer merge keeps destination history and adds a single merge entry.""" existing = StockItem.objects.create( part=self.part, location=self.dest, quantity=100 ) @@ -2229,9 +2232,7 @@ class StockTransferMergeTest(StockAPITestCase): ) incoming.add_tracking_entry( - StockHistoryCode.STOCK_UPDATE, - self.user, - notes='Source tracking entry', + StockHistoryCode.STOCK_UPDATE, self.user, notes='Source tracking entry' ) tracking_count = existing.tracking_info.count() @@ -2248,10 +2249,15 @@ class StockTransferMergeTest(StockAPITestCase): existing.refresh_from_db() - self.assertTrue( + self.assertFalse( existing.tracking_info.filter(notes='Source tracking entry').exists() ) - self.assertGreater(existing.tracking_info.count(), tracking_count) + self.assertEqual(existing.tracking_info.count(), tracking_count + 1) + self.assertTrue( + existing.tracking_info.filter( + tracking_type=StockHistoryCode.MERGED_STOCK_ITEMS + ).exists() + ) class StockItemDeletionTest(StockAPITestCase):