mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add support for recursively delete the stock locations (#3926)
This commit is contained in:
parent
60e44700a9
commit
8ceb1af3c3
@ -27,7 +27,8 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||
from InvenTree.filters import InvenTreeOrderingFilter
|
||||
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
|
||||
str2bool, str2int)
|
||||
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
|
||||
from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI,
|
||||
ListAPI, ListCreateAPI, RetrieveAPI,
|
||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
|
||||
from order.serializers import PurchaseOrderSerializer
|
||||
@ -1357,7 +1358,7 @@ class LocationMetadata(RetrieveUpdateAPI):
|
||||
queryset = StockLocation.objects.all()
|
||||
|
||||
|
||||
class LocationDetail(RetrieveUpdateDestroyAPI):
|
||||
class LocationDetail(CustomRetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for detail view of StockLocation object.
|
||||
|
||||
- GET: Return a single StockLocation object
|
||||
@ -1375,6 +1376,16 @@ class LocationDetail(RetrieveUpdateDestroyAPI):
|
||||
queryset = StockSerializers.LocationSerializer.annotate_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Delete a Stock location instance via the API"""
|
||||
delete_stock_items = 'delete_stock_items' in request.data and request.data['delete_stock_items'] == '1'
|
||||
delete_sub_locations = 'delete_sub_locations' in request.data and request.data['delete_sub_locations'] == '1'
|
||||
return super().destroy(request,
|
||||
*args,
|
||||
**dict(kwargs,
|
||||
delete_sub_locations=delete_sub_locations,
|
||||
delete_stock_items=delete_stock_items))
|
||||
|
||||
|
||||
stock_api_urls = [
|
||||
re_path(r'^location/', include([
|
||||
|
@ -43,9 +43,36 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
|
||||
"""Organization tree for StockItem objects.
|
||||
|
||||
A "StockLocation" can be considered a warehouse, or storage location
|
||||
Stock locations can be heirarchical as required
|
||||
Stock locations can be hierarchical as required
|
||||
"""
|
||||
|
||||
def delete_recursive(self, *args, **kwargs):
|
||||
"""This function handles the recursive deletion of sub-locations depending on kwargs contents"""
|
||||
delete_stock_items = kwargs.get('delete_stock_items', False)
|
||||
parent_location = kwargs.get('parent_location', None)
|
||||
|
||||
if parent_location is None:
|
||||
# First iteration, (no parent_location kwargs passed)
|
||||
parent_location = self.parent
|
||||
|
||||
for child_item in self.get_stock_items(False):
|
||||
if delete_stock_items:
|
||||
child_item.delete()
|
||||
else:
|
||||
child_item.location = parent_location
|
||||
child_item.save()
|
||||
|
||||
for child_location in self.children.all():
|
||||
if kwargs.get('delete_sub_locations', False):
|
||||
child_location.delete_recursive(**dict(delete_sub_locations=True,
|
||||
delete_stock_items=delete_stock_items,
|
||||
parent_location=parent_location))
|
||||
else:
|
||||
child_location.parent = parent_location
|
||||
child_location.save()
|
||||
|
||||
super().delete(*args, **dict())
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Custom model deletion routine, which updates any child locations or items.
|
||||
|
||||
@ -53,24 +80,13 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
|
||||
"""
|
||||
with transaction.atomic():
|
||||
|
||||
parent = self.parent
|
||||
tree_id = self.tree_id
|
||||
self.delete_recursive(**dict(delete_stock_items=kwargs.get('delete_stock_items', False),
|
||||
delete_sub_locations=kwargs.get('delete_sub_locations', False),
|
||||
parent_category=self.parent))
|
||||
|
||||
# Update each stock item in the stock location
|
||||
for item in self.stock_items.all():
|
||||
item.location = self.parent
|
||||
item.save()
|
||||
|
||||
# Update each child category
|
||||
for child in self.children.all():
|
||||
child.parent = self.parent
|
||||
child.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
if parent is not None:
|
||||
if self.parent is not None:
|
||||
# Partially rebuild the tree (cheaper than a complete rebuild)
|
||||
StockLocation.objects.partial_rebuild(tree_id)
|
||||
StockLocation.objects.partial_rebuild(self.tree_id)
|
||||
else:
|
||||
StockLocation.objects.rebuild()
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
import io
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from enum import IntEnum
|
||||
|
||||
import django.http
|
||||
from django.urls import reverse
|
||||
@ -15,6 +16,7 @@ import part.models
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
from InvenTree.status_codes import StockStatus
|
||||
from part.models import Part
|
||||
from stock.models import StockItem, StockItemTestResult, StockLocation
|
||||
|
||||
|
||||
@ -37,6 +39,7 @@ class StockAPITestCase(InvenTreeAPITestCase):
|
||||
'stock.add',
|
||||
'stock_location.change',
|
||||
'stock_location.add',
|
||||
'stock_location.delete',
|
||||
'stock.delete',
|
||||
]
|
||||
|
||||
@ -107,6 +110,121 @@ class StockLocationTest(StockAPITestCase):
|
||||
response = self.client.post(self.list_url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_stock_location_delete(self):
|
||||
"""Test stock location deletion with different parameters"""
|
||||
|
||||
class Target(IntEnum):
|
||||
move_sub_locations_to_parent_move_stockitems_to_parent = 0,
|
||||
move_sub_locations_to_parent_delete_stockitems = 1,
|
||||
delete_sub_locations_move_stockitems_to_parent = 2,
|
||||
delete_sub_locations_delete_stockitems = 3,
|
||||
|
||||
# First, construct a set of template / variant parts
|
||||
part = Part.objects.create(
|
||||
name='Part for stock item creation', description='Part for stock item creation',
|
||||
category=None,
|
||||
is_template=False,
|
||||
)
|
||||
|
||||
for i in range(4):
|
||||
delete_sub_locations: bool = False
|
||||
delete_stock_items: bool = False
|
||||
|
||||
if i == Target.move_sub_locations_to_parent_delete_stockitems \
|
||||
or i == Target.delete_sub_locations_delete_stockitems:
|
||||
delete_stock_items = True
|
||||
if i == Target.delete_sub_locations_move_stockitems_to_parent \
|
||||
or i == Target.delete_sub_locations_delete_stockitems:
|
||||
delete_sub_locations = True
|
||||
|
||||
# Create a parent stock location
|
||||
parent_stock_location = StockLocation.objects.create(
|
||||
name='Parent stock location',
|
||||
description='This is the parent stock location where the sub categories and stock items are moved to',
|
||||
parent=None
|
||||
)
|
||||
|
||||
stocklocation_count_before = StockLocation.objects.count()
|
||||
stock_location_count_before = StockItem.objects.count()
|
||||
|
||||
# Create a stock location to be deleted
|
||||
stock_location_to_delete = StockLocation.objects.create(
|
||||
name='Stock location to delete',
|
||||
description='This is the stock location to be deleted',
|
||||
parent=parent_stock_location
|
||||
)
|
||||
|
||||
url = reverse('api-location-detail', kwargs={'pk': stock_location_to_delete.id})
|
||||
|
||||
stock_items = []
|
||||
# Create stock items in the location to be deleted
|
||||
for jj in range(3):
|
||||
stock_items.append(StockItem.objects.create(
|
||||
batch=f"Stock Item xyz {jj}",
|
||||
location=stock_location_to_delete,
|
||||
part=part
|
||||
))
|
||||
|
||||
child_stock_locations = []
|
||||
child_stock_locations_items = []
|
||||
# Create sub location under the stock location to be deleted
|
||||
for ii in range(3):
|
||||
child = StockLocation.objects.create(
|
||||
name=f"Sub-location {ii}",
|
||||
description="A sub-location of the deleted stock location",
|
||||
parent=stock_location_to_delete
|
||||
)
|
||||
child_stock_locations.append(child)
|
||||
|
||||
# Create stock items in the sub locations
|
||||
for jj in range(3):
|
||||
child_stock_locations_items.append(StockItem.objects.create(
|
||||
batch=f"Stock item in sub location xyz {jj}",
|
||||
part=part,
|
||||
location=child
|
||||
))
|
||||
|
||||
# Delete the created stock location
|
||||
params = {}
|
||||
if delete_stock_items:
|
||||
params['delete_stock_items'] = '1'
|
||||
if delete_sub_locations:
|
||||
params['delete_sub_locations'] = '1'
|
||||
response = self.delete(
|
||||
url,
|
||||
params,
|
||||
expected_code=204,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
if delete_stock_items:
|
||||
if i == Target.delete_sub_locations_delete_stockitems:
|
||||
# Check if all sub-categories deleted
|
||||
self.assertEqual(StockItem.objects.count(), stock_location_count_before)
|
||||
elif i == Target.move_sub_locations_to_parent_delete_stockitems:
|
||||
# Check if all stock locations deleted
|
||||
self.assertEqual(StockItem.objects.count(), stock_location_count_before + len(child_stock_locations_items))
|
||||
else:
|
||||
# Stock locations moved to the parent location
|
||||
for stock_item in stock_items:
|
||||
stock_item.refresh_from_db()
|
||||
self.assertEqual(stock_item.location, parent_stock_location)
|
||||
|
||||
if delete_sub_locations:
|
||||
for child_stock_location_item in child_stock_locations_items:
|
||||
child_stock_location_item.refresh_from_db()
|
||||
self.assertEqual(child_stock_location_item.location, parent_stock_location)
|
||||
|
||||
if delete_sub_locations:
|
||||
# Check if all sub-locations are deleted
|
||||
self.assertEqual(StockLocation.objects.count(), stocklocation_count_before)
|
||||
else:
|
||||
# Check if all sub-locations moved to the parent
|
||||
for child in child_stock_locations:
|
||||
child.refresh_from_db()
|
||||
self.assertEqual(child.parent, parent_stock_location)
|
||||
|
||||
|
||||
class StockItemListTest(StockAPITestCase):
|
||||
"""Tests for the StockItem API LIST endpoint."""
|
||||
|
@ -171,16 +171,35 @@ function deleteStockLocation(pk, options={}) {
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you want to delete this stock location?" %}
|
||||
<ul>
|
||||
<li>{% trans "Any child locations will be moved to the parent of this location" %}</li>
|
||||
<li>{% trans "Any stock items in this location will be moved to the parent of this location" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
var subChoices = [
|
||||
{
|
||||
value: 0,
|
||||
display_name: '{% trans "Move to parent stock location" %}',
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
display_name: '{% trans "Delete" %}',
|
||||
}
|
||||
];
|
||||
|
||||
constructForm(url, {
|
||||
title: '{% trans "Delete Stock Location" %}',
|
||||
method: 'DELETE',
|
||||
fields: {
|
||||
'delete_stock_items': {
|
||||
label: '{% trans "Action for stock items in this stock location" %}',
|
||||
choices: subChoices,
|
||||
type: 'choice'
|
||||
},
|
||||
'delete_sub_locations': {
|
||||
label: '{% trans "Action for sub-locations" %}',
|
||||
choices: subChoices,
|
||||
type: 'choice'
|
||||
},
|
||||
},
|
||||
preFormContent: html,
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
|
Loading…
Reference in New Issue
Block a user