mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-17 20:23:50 +00:00
Prevent duplication completion of ReturnOrder (#12386)
This commit is contained in:
@@ -3189,6 +3189,12 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
def _action_complete(self, *args, **kwargs):
|
def _action_complete(self, *args, **kwargs):
|
||||||
"""Complete this ReturnOrder (if not already completed)."""
|
"""Complete this ReturnOrder (if not already completed)."""
|
||||||
|
# Lock this order against concurrent completion, and re-read the status
|
||||||
|
# from the database. Without this, two simultaneous completion requests
|
||||||
|
# can both observe status=IN_PROGRESS, and each would run the completion
|
||||||
|
# side effects (duplicate events and notifications).
|
||||||
|
self.status = ReturnOrder.objects.select_for_update().get(pk=self.pk).status
|
||||||
|
|
||||||
if self.status == ReturnOrderStatus.IN_PROGRESS.value:
|
if self.status == ReturnOrderStatus.IN_PROGRESS.value:
|
||||||
self.status = ReturnOrderStatus.COMPLETE.value
|
self.status = ReturnOrderStatus.COMPLETE.value
|
||||||
self.complete_date = InvenTree.helpers.current_date()
|
self.complete_date = InvenTree.helpers.current_date()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -3244,6 +3245,41 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
|||||||
stock_item.refresh_from_db()
|
stock_item.refresh_from_db()
|
||||||
self.assertEqual(stock_item.quantity, 6)
|
self.assertEqual(stock_item.quantity, 6)
|
||||||
|
|
||||||
|
def test_complete_stale_instance_is_noop(self):
|
||||||
|
"""A second completion attempt with a stale instance must be a no-op.
|
||||||
|
|
||||||
|
Regression test: _action_complete() checked 'status' on the caller's
|
||||||
|
(potentially stale) instance, so two concurrent completion requests
|
||||||
|
could both run the completion side effects (duplicate COMPLETED events).
|
||||||
|
The status is now re-read (under lock) from the database before the check.
|
||||||
|
"""
|
||||||
|
company = Company.objects.get(pk=4)
|
||||||
|
|
||||||
|
rma = models.ReturnOrder.objects.create(
|
||||||
|
customer=company, description='A return order'
|
||||||
|
)
|
||||||
|
rma.issue_order()
|
||||||
|
|
||||||
|
# Two "concurrent" requests each hold their own instance of the order
|
||||||
|
order_a = models.ReturnOrder.objects.get(pk=rma.pk)
|
||||||
|
order_b = models.ReturnOrder.objects.get(pk=rma.pk)
|
||||||
|
|
||||||
|
order_a.complete_order()
|
||||||
|
|
||||||
|
rma.refresh_from_db()
|
||||||
|
self.assertEqual(rma.status, ReturnOrderStatus.COMPLETE.value)
|
||||||
|
|
||||||
|
# The second (stale) instance still believes the order is IN_PROGRESS -
|
||||||
|
# completion must be skipped based on the database state
|
||||||
|
self.assertEqual(order_b.status, ReturnOrderStatus.IN_PROGRESS.value)
|
||||||
|
|
||||||
|
with mock.patch('order.models.trigger_event') as trigger:
|
||||||
|
order_b.complete_order()
|
||||||
|
trigger.assert_not_called()
|
||||||
|
|
||||||
|
rma.refresh_from_db()
|
||||||
|
self.assertEqual(rma.status, ReturnOrderStatus.COMPLETE.value)
|
||||||
|
|
||||||
def test_ro_calendar(self):
|
def test_ro_calendar(self):
|
||||||
"""Test the calendar export endpoint."""
|
"""Test the calendar export endpoint."""
|
||||||
# Full test is in test_po_calendar. Since these use the same backend, test only
|
# Full test is in test_po_calendar. Since these use the same backend, test only
|
||||||
|
|||||||
Reference in New Issue
Block a user