mirror of
https://github.com/inventree/InvenTree.git
synced 2026-02-06 13:25:53 +00:00
Stock Tracking - Add Old Status to Deltas (#11179)
* match custom status tracking entry in edit * add old_status to stockitemtracking * test old_status tracking * use vars for readability * split custom status test * move custom status from fixture to setup * add old status to tracking table * fallback to logical status if custom removed * avoid shared deltas reference in loop * track old status in stock add/remove/count/transfer --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -795,12 +795,28 @@ class StockItem(
|
||||
|
||||
try:
|
||||
old = StockItem.objects.get(pk=self.pk)
|
||||
old_custom_status = old.get_custom_status()
|
||||
custom_status = self.get_custom_status()
|
||||
|
||||
deltas = {}
|
||||
|
||||
# Status changed?
|
||||
if old.status != self.status:
|
||||
deltas['status'] = self.status
|
||||
# Custom status changed?
|
||||
# Matches custom status tracking behavior of StockChangeStatusSerializer
|
||||
if old_custom_status != custom_status:
|
||||
deltas['status'] = custom_status
|
||||
deltas['status_logical'] = self.status
|
||||
else:
|
||||
deltas['status'] = self.status
|
||||
deltas['status_logical'] = self.status
|
||||
|
||||
if old_custom_status:
|
||||
deltas['old_status'] = old_custom_status
|
||||
deltas['old_status_logical'] = old.status
|
||||
else:
|
||||
deltas['old_status'] = old.status
|
||||
deltas['old_status_logical'] = old.status
|
||||
|
||||
if add_note and len(deltas) > 0:
|
||||
self.add_tracking_entry(
|
||||
@@ -1446,8 +1462,17 @@ class StockItem(
|
||||
|
||||
if status := kwargs.pop('status', None):
|
||||
if not item.compare_status(status):
|
||||
old_custom_status = item.get_custom_status()
|
||||
old_status_logical = item.status
|
||||
item.set_status(status)
|
||||
tracking_info['status'] = status
|
||||
tracking_info['status'] = status # may be a custom value
|
||||
tracking_info['status_logicial'] = (
|
||||
item.status
|
||||
) # always the logical value
|
||||
tracking_info['old_status'] = (
|
||||
old_custom_status if old_custom_status else old_status_logical
|
||||
)
|
||||
tracking_info['old_status_logical'] = old_status_logical
|
||||
|
||||
item.save()
|
||||
|
||||
@@ -2268,8 +2293,26 @@ class StockItem(
|
||||
# Optional fields which can be supplied in a 'move' call
|
||||
for field in StockItem.optional_transfer_fields():
|
||||
if field in kwargs:
|
||||
setattr(new_stock, field, kwargs[field])
|
||||
deltas[field] = kwargs[field]
|
||||
# handle specific case for status deltas
|
||||
if field == 'status':
|
||||
status = kwargs[field]
|
||||
if not new_stock.compare_status(status):
|
||||
old_custom_status = new_stock.get_custom_status()
|
||||
old_status_logical = new_stock.status
|
||||
new_stock.set_status(status)
|
||||
deltas['status'] = status # may be a custom value
|
||||
deltas['status_logicial'] = (
|
||||
new_stock.status
|
||||
) # always the logical value
|
||||
deltas['old_status'] = (
|
||||
old_custom_status
|
||||
if old_custom_status
|
||||
else old_status_logical
|
||||
)
|
||||
deltas['old_status_logical'] = old_status_logical
|
||||
else:
|
||||
setattr(new_stock, field, kwargs[field])
|
||||
deltas[field] = kwargs[field]
|
||||
|
||||
new_stock.save(add_note=False)
|
||||
|
||||
@@ -2385,8 +2428,15 @@ class StockItem(
|
||||
status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None)
|
||||
|
||||
if status and not self.compare_status(status):
|
||||
old_custom_status = self.get_custom_status()
|
||||
old_status_logical = self.status
|
||||
self.set_status(status)
|
||||
tracking_info['status'] = status
|
||||
tracking_info['status'] = status # may be a custom value
|
||||
tracking_info['status_logicial'] = self.status # always the logical value
|
||||
tracking_info['old_status'] = (
|
||||
old_custom_status if old_custom_status else old_status_logical
|
||||
)
|
||||
tracking_info['old_status_logical'] = old_status_logical
|
||||
|
||||
# Optional fields which can be supplied in a 'move' call
|
||||
for field in StockItem.optional_transfer_fields():
|
||||
@@ -2437,7 +2487,7 @@ class StockItem(
|
||||
|
||||
return False
|
||||
|
||||
self.save()
|
||||
self.save(add_note=False)
|
||||
|
||||
trigger_event(
|
||||
StockEvents.ITEM_QUANTITY_UPDATED, id=self.id, quantity=float(self.quantity)
|
||||
@@ -2470,8 +2520,15 @@ class StockItem(
|
||||
status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None)
|
||||
|
||||
if status and not self.compare_status(status):
|
||||
old_custom_status = self.get_custom_status()
|
||||
old_status_logical = self.status
|
||||
self.set_status(status)
|
||||
tracking_info['status'] = status
|
||||
tracking_info['status'] = status # may be a custom value
|
||||
tracking_info['status_logicial'] = self.status # always the logical value
|
||||
tracking_info['old_status'] = (
|
||||
old_custom_status if old_custom_status else old_status_logical
|
||||
)
|
||||
tracking_info['old_status_logical'] = old_status_logical
|
||||
|
||||
if self.updateQuantity(count):
|
||||
tracking_info['quantity'] = float(count)
|
||||
@@ -2533,8 +2590,15 @@ class StockItem(
|
||||
status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None)
|
||||
|
||||
if status and not self.compare_status(status):
|
||||
old_custom_status = self.get_custom_status()
|
||||
old_status_logical = self.status
|
||||
self.set_status(status)
|
||||
tracking_info['status'] = status
|
||||
tracking_info['status'] = status # may be a custom value
|
||||
tracking_info['status_logicial'] = self.status # always the logical value
|
||||
tracking_info['old_status'] = (
|
||||
old_custom_status if old_custom_status else old_status_logical
|
||||
)
|
||||
tracking_info['old_status_logical'] = old_status_logical
|
||||
|
||||
if self.updateQuantity(self.quantity + quantity):
|
||||
tracking_info['added'] = float(quantity)
|
||||
@@ -2587,8 +2651,15 @@ class StockItem(
|
||||
status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None)
|
||||
|
||||
if status and not self.compare_status(status):
|
||||
old_custom_status = self.get_custom_status()
|
||||
old_status_logical = self.status
|
||||
self.set_status(status)
|
||||
deltas['status'] = status
|
||||
deltas['status'] = status # may be a custom value
|
||||
deltas['status_logicial'] = self.status # always the logical value
|
||||
deltas['old_status'] = (
|
||||
old_custom_status if old_custom_status else old_status_logical
|
||||
)
|
||||
deltas['old_status_logical'] = old_status_logical
|
||||
|
||||
if self.updateQuantity(self.quantity - quantity):
|
||||
deltas['removed'] = float(quantity)
|
||||
|
||||
@@ -463,12 +463,16 @@ class StockItemSerializer(
|
||||
status_custom_key = validated_data.pop('status_custom_key', None)
|
||||
status = validated_data.pop('status', None)
|
||||
|
||||
instance = super().update(instance, validated_data=validated_data)
|
||||
|
||||
if status_code := status_custom_key or status:
|
||||
if not instance.compare_status(status_code):
|
||||
instance.set_status(status_code)
|
||||
instance.save()
|
||||
# avoid a second .save() call and perform both status updates at once (to support `old_status` in tracking event)
|
||||
# by setting the values in validated_data as computed by set_status()
|
||||
instance.set_status(status_code)
|
||||
validated_data['status'] = instance.status
|
||||
validated_data['status_custom_key'] = (
|
||||
status_code # for compatibility with custom "leader/follower" concept in super().update()
|
||||
)
|
||||
|
||||
instance = super().update(instance, validated_data=validated_data)
|
||||
|
||||
return instance
|
||||
|
||||
@@ -1053,8 +1057,6 @@ class StockChangeStatusSerializer(serializers.Serializer):
|
||||
|
||||
transaction_notes = []
|
||||
|
||||
deltas = {'status': status}
|
||||
|
||||
now = InvenTree.helpers.current_time()
|
||||
|
||||
# Instead of performing database updates for each item,
|
||||
@@ -1072,9 +1074,22 @@ class StockChangeStatusSerializer(serializers.Serializer):
|
||||
if status == custom_status or custom_status is None:
|
||||
continue
|
||||
|
||||
deltas = {'status': status}
|
||||
|
||||
# before save, track old status logical
|
||||
deltas['old_status_logical'] = item.status
|
||||
|
||||
if item.get_custom_status():
|
||||
deltas['old_status'] = item.get_custom_status()
|
||||
else:
|
||||
deltas['old_status'] = item.status
|
||||
|
||||
item.set_status(status, custom_values=custom_status_codes)
|
||||
item.save(add_note=False)
|
||||
|
||||
# after save, can track new status_logical
|
||||
deltas['status_logical'] = item.status
|
||||
|
||||
# Create a new transaction note for each item
|
||||
transaction_notes.append(
|
||||
StockItemTracking(
|
||||
|
||||
@@ -1315,6 +1315,17 @@ class StockItemTest(StockAPITestCase):
|
||||
StockLocation.objects.create(name='B', description='location b', parent=top)
|
||||
StockLocation.objects.create(name='C', description='location c', parent=top)
|
||||
|
||||
# Create a custom status
|
||||
self.inspect_custom_status = InvenTreeCustomUserStateModel.objects.create(
|
||||
key=150,
|
||||
name='INSPECT',
|
||||
label='Incoming goods inspection',
|
||||
color='warning',
|
||||
logical_key=50,
|
||||
model=ContentType.objects.get(model='stockitem'),
|
||||
reference_status='StockStatus',
|
||||
)
|
||||
|
||||
def test_create_default_location(self):
|
||||
"""Test the default location functionality, if a 'location' is not specified in the creation request."""
|
||||
# The part 'R_4K7_0603' (pk=4) has a default location specified
|
||||
@@ -1820,6 +1831,15 @@ class StockItemTest(StockAPITestCase):
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(item.status, StockStatus.DAMAGED.value)
|
||||
self.assertEqual(item.tracking_info.count(), 2)
|
||||
tracking = item.tracking_info.last()
|
||||
self.assertEqual(tracking.deltas['old_status'], StockStatus.OK.value)
|
||||
self.assertEqual(
|
||||
tracking.deltas['old_status_logical'], StockStatus.OK.value
|
||||
)
|
||||
self.assertEqual(tracking.deltas['status'], StockStatus.DAMAGED.value)
|
||||
self.assertEqual(
|
||||
tracking.deltas['status_logical'], StockStatus.DAMAGED.value
|
||||
)
|
||||
|
||||
# Same test, but with one item unchanged
|
||||
items[0].set_status(StockStatus.ATTENTION.value)
|
||||
@@ -1837,6 +1857,68 @@ class StockItemTest(StockAPITestCase):
|
||||
tracking = item.tracking_info.last()
|
||||
self.assertEqual(tracking.tracking_type, StockHistoryCode.EDITED.value)
|
||||
|
||||
def test_set_custom_status(self):
|
||||
"""Test API endpoint for setting StockItem custom status."""
|
||||
url = reverse('api-stock-change-status')
|
||||
|
||||
prt = Part.objects.first()
|
||||
|
||||
# Number of items to create
|
||||
N_ITEMS = 10
|
||||
|
||||
# Create a bunch of items
|
||||
items = [
|
||||
StockItem.objects.create(part=prt, quantity=10) for _ in range(N_ITEMS)
|
||||
]
|
||||
|
||||
for item in items:
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(item.status, StockStatus.OK.value)
|
||||
self.assertEqual(item.tracking_info.count(), 1)
|
||||
|
||||
# Test tracking with custom status
|
||||
# *from* standard *to* custom
|
||||
data = {
|
||||
'items': [item.pk for item in items],
|
||||
'status': self.inspect_custom_status.key,
|
||||
}
|
||||
|
||||
self.post(url, data, expected_code=201)
|
||||
|
||||
for item in items:
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(item.status, self.inspect_custom_status.logical_key)
|
||||
self.assertEqual(item.get_custom_status(), self.inspect_custom_status.key)
|
||||
tracking = item.tracking_info.last()
|
||||
self.assertEqual(tracking.deltas['old_status'], StockStatus.OK.value)
|
||||
self.assertEqual(
|
||||
tracking.deltas['old_status_logical'], StockStatus.OK.value
|
||||
)
|
||||
self.assertEqual(tracking.deltas['status'], self.inspect_custom_status.key)
|
||||
self.assertEqual(
|
||||
tracking.deltas['status_logical'],
|
||||
self.inspect_custom_status.logical_key,
|
||||
)
|
||||
|
||||
# reverse case
|
||||
# *from* custom *to* standard
|
||||
data['status'] = StockStatus.OK.value
|
||||
self.post(url, data, expected_code=201)
|
||||
for item in items:
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(item.status, StockStatus.OK.value)
|
||||
self.assertIsNone(item.get_custom_status())
|
||||
tracking = item.tracking_info.last()
|
||||
self.assertEqual(
|
||||
tracking.deltas['old_status'], self.inspect_custom_status.key
|
||||
)
|
||||
self.assertEqual(
|
||||
tracking.deltas['old_status_logical'],
|
||||
self.inspect_custom_status.logical_key,
|
||||
)
|
||||
self.assertEqual(tracking.deltas['status'], StockStatus.OK.value)
|
||||
self.assertEqual(tracking.deltas['status_logical'], StockStatus.OK.value)
|
||||
|
||||
|
||||
class StocktakeTest(StockAPITestCase):
|
||||
"""Series of tests for the Stocktake API."""
|
||||
|
||||
Reference in New Issue
Block a user