mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-14 22:20:59 +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 (
|
from InvenTree.api import (
|
||||||
BulkDeleteMixin,
|
BulkDeleteMixin,
|
||||||
BulkDeleteViewsetMixin,
|
BulkDeleteViewsetMixin,
|
||||||
|
BulkRequestSerializer,
|
||||||
BulkUpdateMixin,
|
BulkUpdateMixin,
|
||||||
ListCreateDestroyAPIView,
|
ListCreateDestroyAPIView,
|
||||||
ParameterListMixin,
|
ParameterListMixin,
|
||||||
@@ -686,6 +687,55 @@ class PurchaseOrderLineItemViewSet(
|
|||||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
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
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
|
||||||
ordering_field_aliases = {
|
ordering_field_aliases = {
|
||||||
|
|||||||
@@ -1123,16 +1123,50 @@ class PurchaseOrderLineItemTest(OrderTest):
|
|||||||
url = reverse('api-po-line-list')
|
url = reverse('api-po-line-list')
|
||||||
|
|
||||||
# Deletion should fail without the correct role
|
# 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')
|
self.assignRole('purchase_order.delete')
|
||||||
|
|
||||||
# Try to delete a set of line items via their IDs
|
# 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
|
# We should have 2 less PurchaseOrderLineItems after deleting them
|
||||||
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 2)
|
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):
|
def test_po_extra_line_bulk_delete(self):
|
||||||
"""Test that we can bulk delete multiple PurchaseOrderExtraLine items via the API."""
|
"""Test that we can bulk delete multiple PurchaseOrderExtraLine items via the API."""
|
||||||
po = models.PurchaseOrder.objects.get(pk=1)
|
po = models.PurchaseOrder.objects.get(pk=1)
|
||||||
|
|||||||
Reference in new issue
Block a user