mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-10 15:36:17 +00:00
[API] Refactor stock adjustment endpoints (#12488)
This commit is contained in:
@@ -76,6 +76,14 @@ class DiffMixin:
|
||||
if field.name == 'id':
|
||||
continue
|
||||
|
||||
if field.is_relation:
|
||||
# Compare the raw FK id first (no query) - only dereference to the
|
||||
# full related object (one query each side) if it actually differs.
|
||||
# In the common case (unchanged FK) this avoids two full-object
|
||||
# fetches per relational field for every single save() call.
|
||||
if getattr(self, field.attname) == getattr(db_instance, field.attname):
|
||||
continue
|
||||
|
||||
if getattr(self, field.name) != getattr(db_instance, field.name):
|
||||
deltas[field.name] = {
|
||||
'old': getattr(db_instance, field.name),
|
||||
|
||||
@@ -282,7 +282,11 @@ class StatusCodeMixin:
|
||||
|
||||
- Ensure custom status code values are correctly updated
|
||||
"""
|
||||
if self.status_class:
|
||||
# Only need to verify the custom key against the DB if one is actually set -
|
||||
# the 'key' column on InvenTreeCustomUserStateModel is non-nullable, so a
|
||||
# query for key=None (i.e. no custom status) can never match and would only
|
||||
# ever result in a no-op (custom_key is already None in that case).
|
||||
if self.status_class and self.get_custom_status() is not None:
|
||||
# Check that the current 'logical key' actually matches the current status code
|
||||
custom_values = self.status_class.custom_queryset().filter(
|
||||
logical_key=self.get_status(), key=self.get_custom_status()
|
||||
|
||||
@@ -1035,11 +1035,19 @@ class StockItem(
|
||||
if type(self.batch) is str:
|
||||
self.batch = self.batch.strip()
|
||||
|
||||
if not get_global_setting('STOCK_ALLOW_EDIT_SERIAL'):
|
||||
deltas = self.get_field_deltas()
|
||||
|
||||
if not get_global_setting('STOCK_ALLOW_EDIT_SERIAL') and self.pk:
|
||||
# Prevent editing of serial numbers if the item already has a serial number assigned
|
||||
if 'serial' in deltas and deltas['serial']['old'] not in [None, '']:
|
||||
# Note: A targeted single-column lookup is used here (rather than get_field_deltas(),
|
||||
# which fetches and diffs *every* field, dereferencing every non-null FK in the
|
||||
# process) since only the previous 'serial' value is actually needed.
|
||||
old_serial = (
|
||||
StockItem.objects
|
||||
.filter(pk=self.pk)
|
||||
.values_list('serial', flat=True)
|
||||
.first()
|
||||
)
|
||||
|
||||
if old_serial not in [None, ''] and old_serial != self.serial:
|
||||
raise ValidationError({
|
||||
'serial': _(
|
||||
'Editing of serial numbers is not allowed - this item has already been assigned a serial number'
|
||||
@@ -3246,19 +3254,36 @@ class StockItem(
|
||||
|
||||
self._apply_model_reference_fields(kwargs, tracking_info)
|
||||
|
||||
quantity_updated = self.serialized or self.updateQuantity(count)
|
||||
# Will updateQuantity() below actually change (and therefore save) the row?
|
||||
# Mirrors updateQuantity()'s own change check - used to decide whether the
|
||||
# stocktake stamp can ride along on that save, avoiding a second write.
|
||||
quantity_will_change = not self.serialized and count != self.quantity
|
||||
|
||||
# Stamp the stocktake metadata *before* updating the quantity, so that when
|
||||
# updateQuantity() performs its own save (below), this stamp - and the status/
|
||||
# location/reference field changes already applied above - are written in that
|
||||
# single query, rather than needing a second, otherwise-redundant save() after.
|
||||
if fields_updated or self.serialized or quantity_will_change:
|
||||
self.stocktake_date = InvenTree.helpers.current_date()
|
||||
self.stocktake_user = user
|
||||
|
||||
raw_update_result = None if self.serialized else self.updateQuantity(count)
|
||||
quantity_updated = self.serialized or raw_update_result
|
||||
|
||||
# True only if updateQuantity() actually performed the save above
|
||||
# (as opposed to: serialized item, no change, or item deleted)
|
||||
update_persisted = raw_update_result is True
|
||||
|
||||
# Record the resulting quantity, whether or not this item survived the stocktake
|
||||
# (self.quantity is updated by updateQuantity() even if the item was deleted)
|
||||
tracking_info['quantity'] = 1 if self.serialized else float(self.quantity)
|
||||
|
||||
# Save if the quantity or any other field was changed.
|
||||
# Save if the quantity or any other field was changed, and updateQuantity()
|
||||
# didn't already do so above.
|
||||
# Note that updateQuantity() may have *deleted* the item (depleted to zero),
|
||||
# in which case there is nothing left to save.
|
||||
if self.pk and (quantity_updated or fields_updated):
|
||||
self.stocktake_date = InvenTree.helpers.current_date()
|
||||
self.stocktake_user = user
|
||||
|
||||
if not update_persisted:
|
||||
self.save(add_note=False)
|
||||
|
||||
trigger_event(
|
||||
@@ -3312,6 +3337,14 @@ class StockItem(
|
||||
self._apply_status_change(status, tracking_info)
|
||||
self._apply_model_reference_fields(kwargs, tracking_info)
|
||||
|
||||
# Determine up-front whether any optional fields will actually change,
|
||||
# so we can skip the second save() below when updateQuantity() (which
|
||||
# already writes the full row, including the status/reference field
|
||||
# changes applied above) has already persisted everything that matters.
|
||||
optional_fields_changed = any(
|
||||
field in kwargs for field in StockItem.optional_transfer_fields()
|
||||
)
|
||||
|
||||
if self.updateQuantity(self.quantity + quantity):
|
||||
tracking_info['added'] = float(quantity)
|
||||
tracking_info['quantity'] = float(self.quantity)
|
||||
@@ -3319,6 +3352,7 @@ class StockItem(
|
||||
# Optional fields which can be supplied in a 'stocktake' call
|
||||
self._apply_optional_transfer_fields(kwargs, tracking_info)
|
||||
|
||||
if optional_fields_changed:
|
||||
self.save(add_note=False)
|
||||
|
||||
self.add_tracking_entry(
|
||||
@@ -3372,6 +3406,14 @@ class StockItem(
|
||||
self._apply_status_change(status, deltas)
|
||||
self._apply_model_reference_fields(kwargs, deltas)
|
||||
|
||||
# Determine up-front whether any optional fields will actually change,
|
||||
# so we can skip the second save() below when updateQuantity() (which
|
||||
# already writes the full row, including the status/reference field
|
||||
# changes applied above) has already persisted everything that matters.
|
||||
optional_fields_changed = any(
|
||||
field in kwargs for field in StockItem.optional_transfer_fields()
|
||||
)
|
||||
|
||||
quantity_updated = self.updateQuantity(self.quantity - quantity)
|
||||
|
||||
# Record the resulting quantity, whether or not this item survived the removal
|
||||
@@ -3383,6 +3425,7 @@ class StockItem(
|
||||
# Optional fields which can be supplied in a 'stocktake' call
|
||||
self._apply_optional_transfer_fields(kwargs, deltas)
|
||||
|
||||
if optional_fields_changed:
|
||||
self.save(add_note=False)
|
||||
|
||||
# Always record a tracking entry, even if the item was deleted as a result
|
||||
|
||||
@@ -1979,7 +1979,10 @@ class StockAdjustmentSerializer(serializers.Serializer):
|
||||
|
||||
if pks:
|
||||
self.context['_stockitems'] = {
|
||||
obj.pk: obj for obj in StockItem.objects.filter(pk__in=pks)
|
||||
obj.pk: obj
|
||||
for obj in StockItem.objects.filter(pk__in=pks).select_related(
|
||||
'part', 'location'
|
||||
)
|
||||
}
|
||||
|
||||
return super().to_internal_value(data)
|
||||
|
||||
@@ -3092,9 +3092,8 @@ class StocktakeTest(StockAPITestCase):
|
||||
with self.settings(
|
||||
PLUGIN_TESTING_EVENTS=True, PLUGIN_TESTING_EVENTS_ASYNC=True
|
||||
):
|
||||
# TODO: 2026-07-12 : Refactor this API call
|
||||
response = self.post(
|
||||
url, data, max_query_count=2250, benchmark=True, format='json'
|
||||
url, data, max_query_count=950, benchmark=True, format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
@@ -3123,9 +3122,8 @@ class StocktakeTest(StockAPITestCase):
|
||||
with self.settings(
|
||||
PLUGIN_TESTING_EVENTS=True, PLUGIN_TESTING_EVENTS_ASYNC=True
|
||||
):
|
||||
# TODO: 2026-07-12 : Refactor this API call
|
||||
response = self.post(
|
||||
url, data, max_query_count=2500, benchmark=True, format='json'
|
||||
url, data, max_query_count=950, benchmark=True, format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
@@ -3154,9 +3152,8 @@ class StocktakeTest(StockAPITestCase):
|
||||
with self.settings(
|
||||
PLUGIN_TESTING_EVENTS=True, PLUGIN_TESTING_EVENTS_ASYNC=True
|
||||
):
|
||||
# TODO: 2026-07-12 : Refactor this API call
|
||||
response = self.post(
|
||||
url, data, max_query_count=2250, benchmark=True, format='json'
|
||||
url, data, max_query_count=950, benchmark=True, format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
@@ -3191,9 +3188,8 @@ class StocktakeTest(StockAPITestCase):
|
||||
with self.settings(
|
||||
PLUGIN_TESTING_EVENTS=True, PLUGIN_TESTING_EVENTS_ASYNC=True
|
||||
):
|
||||
# TODO: 2026-07-12 : Refactor this API call
|
||||
response = self.post(
|
||||
url, data, max_query_count=1250, benchmark=True, format='json'
|
||||
url, data, max_query_count=850, benchmark=True, format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
Reference in New Issue
Block a user