mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-14 14:11:55 +00:00
Prevent deletion of line items which have received stock (#13004)
* Prevent deletion of line items which have received stock * Revert docstring changes
This commit is contained in:
1 parent
99893f3453
commit
29eb9632ac
2 files changed
+86
-2
No files matched your search
@@ -36,6 +36,7 @@ from generic.states.api import FSMTransitionMixin, StatusView
|
||||
from InvenTree.api import (
|
||||
BulkDeleteMixin,
|
||||
BulkDeleteViewsetMixin,
|
||||
BulkRequestSerializer,
|
||||
BulkUpdateMixin,
|
||||
ListCreateDestroyAPIView,
|
||||
ParameterListMixin,
|
||||
@@ -686,6 +687,55 @@ class PurchaseOrderLineItemViewSet(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
def check_received(self, queryset) -> None:
|
||||
"""Prevent deletion of line items against which stock has been received.
|
||||
|
||||
Deleting a received line item would erase the record of what was received
|
||||
against the order. The line item rows are locked (in pk order, matching the
|
||||
lock order used when receiving stock) so that a concurrent receipt cannot
|
||||
succeed between this check and the deletion.
|
||||
|
||||
Raises:
|
||||
ValidationError: If any line item has a received quantity
|
||||
"""
|
||||
received = [
|
||||
pk
|
||||
for pk, qty in queryset
|
||||
.select_for_update()
|
||||
.order_by('pk')
|
||||
.values_list('pk', 'received')
|
||||
if qty > 0
|
||||
]
|
||||
|
||||
if received:
|
||||
raise rest_framework.serializers.ValidationError(
|
||||
_('Cannot delete line items which have received stock')
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Prevent deletion of a single line item which has received stock."""
|
||||
with transaction.atomic():
|
||||
self.check_received(
|
||||
models.PurchaseOrderLineItem.objects.filter(pk=instance.pk)
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
def validate_delete(self, queryset, request) -> None:
|
||||
"""Prevent bulk deletion of line items which have received stock."""
|
||||
self.check_received(queryset)
|
||||
|
||||
@extend_schema(request=BulkRequestSerializer)
|
||||
def bulk_delete(self, request, *args, **kwargs):
|
||||
"""Perform a bulk delete operation.
|
||||
|
||||
Provide either a list of ids (via `items`) or a filter (via `filters`) to select the items to be deleted.
|
||||
|
||||
This action is performed attomically, so either all items will be deleted, or none will be deleted.
|
||||
|
||||
"""
|
||||
with transaction.atomic():
|
||||
return super().bulk_delete(request, *args, **kwargs)
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
ordering_field_aliases = {
|
||||
|
||||
@@ -1123,16 +1123,50 @@ class PurchaseOrderLineItemTest(OrderTest):
|
||||
url = reverse('api-po-line-list')
|
||||
|
||||
# Deletion should fail without the correct role
|
||||
self.delete(url, {'items': [1, 2]}, expected_code=403)
|
||||
self.delete(url, {'items': [1, 3]}, expected_code=403)
|
||||
|
||||
self.assignRole('purchase_order.delete')
|
||||
|
||||
# Try to delete a set of line items via their IDs
|
||||
self.delete(url, {'items': [1, 2]}, expected_code=200)
|
||||
self.delete(url, {'items': [1, 3]}, expected_code=200)
|
||||
|
||||
# We should have 2 less PurchaseOrderLineItems after deleting them
|
||||
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 2)
|
||||
|
||||
def test_po_line_delete_received(self):
|
||||
"""Line items against which stock has been received cannot be deleted."""
|
||||
self.assignRole('purchase_order.delete')
|
||||
|
||||
n = models.PurchaseOrderLineItem.objects.count()
|
||||
|
||||
# Line item 2 is partially received (fixture data)
|
||||
self.assertGreater(models.PurchaseOrderLineItem.objects.get(pk=2).received, 0)
|
||||
|
||||
# Single delete
|
||||
response = self.delete(
|
||||
reverse('api-po-line-detail', kwargs={'pk': 2}), expected_code=400
|
||||
)
|
||||
self.assertIn(
|
||||
'Cannot delete line items which have received stock', str(response.data)
|
||||
)
|
||||
|
||||
# Bulk delete - received line item is mixed in with unreceived line items
|
||||
url = reverse('api-po-line-list')
|
||||
self.delete(url, {'items': [1, 2, 3]}, expected_code=400)
|
||||
|
||||
# Bulk delete by filter
|
||||
self.delete(url, {'filters': {'order': 1}}, expected_code=400)
|
||||
|
||||
# Nothing has been deleted
|
||||
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n)
|
||||
|
||||
# Line items without received stock can still be deleted
|
||||
self.delete(url, {'items': [1, 3]}, expected_code=200)
|
||||
self.delete(reverse('api-po-line-detail', kwargs={'pk': 23}), expected_code=204)
|
||||
|
||||
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 3)
|
||||
self.assertTrue(models.PurchaseOrderLineItem.objects.filter(pk=2).exists())
|
||||
|
||||
def test_po_extra_line_bulk_delete(self):
|
||||
"""Test that we can bulk delete multiple PurchaseOrderExtraLine items via the API."""
|
||||
po = models.PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
Reference in new issue
Block a user