mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Stock status change API (#5064)
* Add API endpoint for changing stock item status - Change status for multiple items simultaneously - Reduce number of database queries required * Perform bulk update in serializer * Update 'updated' field * Add front-end code * Bump API version * Bug fix and unit test
This commit is contained in:
parent
f6420f98c2
commit
2e8fb2a14a
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 124
|
INVENTREE_API_VERSION = 125
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v125 -> 2023-06-17 : https://github.com/inventree/InvenTree/pull/5064
|
||||||
|
- Adds API endpoint for setting the "status" field for multiple stock items simultaneously
|
||||||
|
|
||||||
v124 -> 2023-06-17 : https://github.com/inventree/InvenTree/pull/5057
|
v124 -> 2023-06-17 : https://github.com/inventree/InvenTree/pull/5057
|
||||||
- Add "created_before" and "created_after" filters to the Part API
|
- Add "created_before" and "created_after" filters to the Part API
|
||||||
|
|
||||||
|
@ -158,6 +158,12 @@ class StockAdjustView(CreateAPI):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class StockChangeStatus(StockAdjustView):
|
||||||
|
"""API endpoint to change the status code of multiple StockItem objects."""
|
||||||
|
|
||||||
|
serializer_class = StockSerializers.StockChangeStatusSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockCount(StockAdjustView):
|
class StockCount(StockAdjustView):
|
||||||
"""Endpoint for counting stock (performing a stocktake)."""
|
"""Endpoint for counting stock (performing a stocktake)."""
|
||||||
|
|
||||||
@ -1371,6 +1377,7 @@ stock_api_urls = [
|
|||||||
re_path(r'^transfer/', StockTransfer.as_view(), name='api-stock-transfer'),
|
re_path(r'^transfer/', StockTransfer.as_view(), name='api-stock-transfer'),
|
||||||
re_path(r'^assign/', StockAssign.as_view(), name='api-stock-assign'),
|
re_path(r'^assign/', StockAssign.as_view(), name='api-stock-assign'),
|
||||||
re_path(r'^merge/', StockMerge.as_view(), name='api-stock-merge'),
|
re_path(r'^merge/', StockMerge.as_view(), name='api-stock-merge'),
|
||||||
|
re_path(r'^change_status/', StockChangeStatus.as_view(), name='api-stock-change-status'),
|
||||||
|
|
||||||
# StockItemAttachment API endpoints
|
# StockItemAttachment API endpoints
|
||||||
re_path(r'^attachment/', include([
|
re_path(r'^attachment/', include([
|
||||||
|
@ -18,6 +18,7 @@ import common.models
|
|||||||
import company.models
|
import company.models
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
|
import InvenTree.status_codes
|
||||||
import part.models as part_models
|
import part.models as part_models
|
||||||
import stock.filters
|
import stock.filters
|
||||||
from company.serializers import SupplierPartSerializer
|
from company.serializers import SupplierPartSerializer
|
||||||
@ -481,6 +482,7 @@ class InstallStockItemSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
note = serializers.CharField(
|
note = serializers.CharField(
|
||||||
label=_('Note'),
|
label=_('Note'),
|
||||||
|
help_text=_('Add transaction note (optional)'),
|
||||||
required=False,
|
required=False,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
)
|
)
|
||||||
@ -641,6 +643,100 @@ class ReturnStockItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StockChangeStatusSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for changing status of multiple StockItem objects"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Metaclass options"""
|
||||||
|
fields = [
|
||||||
|
'items',
|
||||||
|
'status',
|
||||||
|
'note',
|
||||||
|
]
|
||||||
|
|
||||||
|
items = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=StockItem.objects.all(),
|
||||||
|
many=True,
|
||||||
|
required=True,
|
||||||
|
allow_null=False,
|
||||||
|
label=_('Stock Items'),
|
||||||
|
help_text=_('Select stock items to change status'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_items(self, items):
|
||||||
|
"""Validate the selected stock items"""
|
||||||
|
|
||||||
|
if len(items) == 0:
|
||||||
|
raise ValidationError(_("No stock items selected"))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
status = serializers.ChoiceField(
|
||||||
|
choices=InvenTree.status_codes.StockStatus.items(),
|
||||||
|
default=InvenTree.status_codes.StockStatus.OK.value,
|
||||||
|
label=_('Status'),
|
||||||
|
)
|
||||||
|
|
||||||
|
note = serializers.CharField(
|
||||||
|
label=_('Notes'),
|
||||||
|
help_text=_('Add transaction note (optional)'),
|
||||||
|
required=False, allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self):
|
||||||
|
"""Save the serializer to change the status of the selected stock items"""
|
||||||
|
|
||||||
|
data = self.validated_data
|
||||||
|
|
||||||
|
items = data['items']
|
||||||
|
status = data['status']
|
||||||
|
|
||||||
|
request = self.context['request']
|
||||||
|
user = getattr(request, 'user', None)
|
||||||
|
|
||||||
|
note = data.get('note', '')
|
||||||
|
|
||||||
|
items_to_update = []
|
||||||
|
transaction_notes = []
|
||||||
|
|
||||||
|
deltas = {
|
||||||
|
'status': status,
|
||||||
|
}
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Instead of performing database updates for each item,
|
||||||
|
# perform bulk database updates (much more efficient)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
# Ignore items which are already in the desired status
|
||||||
|
if item.status == status:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item.updated = now
|
||||||
|
item.status = status
|
||||||
|
items_to_update.append(item)
|
||||||
|
|
||||||
|
# Create a new transaction note for each item
|
||||||
|
transaction_notes.append(
|
||||||
|
StockItemTracking(
|
||||||
|
item=item,
|
||||||
|
tracking_type=InvenTree.status_codes.StockHistoryCode.EDITED.value,
|
||||||
|
date=now,
|
||||||
|
deltas=deltas,
|
||||||
|
user=user,
|
||||||
|
notes=note,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
StockItem.objects.bulk_update(items_to_update, ['status', 'updated'])
|
||||||
|
|
||||||
|
# Create entries
|
||||||
|
StockItemTracking.objects.bulk_create(transaction_notes)
|
||||||
|
|
||||||
|
|
||||||
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||||
"""Serializer for a simple tree view."""
|
"""Serializer for a simple tree view."""
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from rest_framework import status
|
|||||||
import company.models
|
import company.models
|
||||||
import part.models
|
import part.models
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.status_codes import StockStatus
|
from InvenTree.status_codes import StockHistoryCode, StockStatus
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from stock.models import StockItem, StockItemTestResult, StockLocation
|
from stock.models import StockItem, StockItemTestResult, StockLocation
|
||||||
@ -1153,6 +1153,51 @@ class StockItemTest(StockAPITestCase):
|
|||||||
stock_item.refresh_from_db()
|
stock_item.refresh_from_db()
|
||||||
self.assertEqual(stock_item.part, variant)
|
self.assertEqual(stock_item.part, variant)
|
||||||
|
|
||||||
|
def test_set_status(self):
|
||||||
|
"""Test API endpoint for setting StockItem status"""
|
||||||
|
|
||||||
|
url = reverse('api-stock-change-status')
|
||||||
|
|
||||||
|
prt = Part.objects.first()
|
||||||
|
|
||||||
|
# Create a bunch of items
|
||||||
|
items = [
|
||||||
|
StockItem.objects.create(part=prt, quantity=10) for _ in range(10)
|
||||||
|
]
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.status, StockStatus.OK.value)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'items': [item.pk for item in items],
|
||||||
|
'status': StockStatus.DAMAGED.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.post(url, data, expected_code=201)
|
||||||
|
|
||||||
|
# Check that the item has been updated correctly
|
||||||
|
for item in items:
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.status, StockStatus.DAMAGED.value)
|
||||||
|
self.assertEqual(item.tracking_info.count(), 1)
|
||||||
|
|
||||||
|
# Same test, but with one item unchanged
|
||||||
|
items[0].status = StockStatus.ATTENTION.value
|
||||||
|
items[0].save()
|
||||||
|
|
||||||
|
data['status'] = StockStatus.ATTENTION.value
|
||||||
|
|
||||||
|
self.post(url, data, expected_code=201)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.status, StockStatus.ATTENTION.value)
|
||||||
|
self.assertEqual(item.tracking_info.count(), 2)
|
||||||
|
|
||||||
|
tracking = item.tracking_info.last()
|
||||||
|
self.assertEqual(tracking.tracking_type, StockHistoryCode.EDITED.value)
|
||||||
|
|
||||||
|
|
||||||
class StocktakeTest(StockAPITestCase):
|
class StocktakeTest(StockAPITestCase):
|
||||||
"""Series of tests for the Stocktake API."""
|
"""Series of tests for the Stocktake API."""
|
||||||
|
@ -1138,7 +1138,7 @@ function adjustStock(action, items, options={}) {
|
|||||||
if (itemCount == 0) {
|
if (itemCount == 0) {
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
'{% trans "Select Stock Items" %}',
|
'{% trans "Select Stock Items" %}',
|
||||||
'{% trans "You must select at least one available stock item" %}',
|
'{% trans "Select at least one available stock item" %}',
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -2297,22 +2297,27 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback for 'stocktake' button
|
||||||
$('#multi-item-stocktake').click(function() {
|
$('#multi-item-stocktake').click(function() {
|
||||||
stockAdjustment('count');
|
stockAdjustment('count');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'remove stock' button
|
||||||
$('#multi-item-remove').click(function() {
|
$('#multi-item-remove').click(function() {
|
||||||
stockAdjustment('take');
|
stockAdjustment('take');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'add stock' button
|
||||||
$('#multi-item-add').click(function() {
|
$('#multi-item-add').click(function() {
|
||||||
stockAdjustment('add');
|
stockAdjustment('add');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'move stock' button
|
||||||
$('#multi-item-move').click(function() {
|
$('#multi-item-move').click(function() {
|
||||||
stockAdjustment('move');
|
stockAdjustment('move');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'merge stock' button
|
||||||
$('#multi-item-merge').click(function() {
|
$('#multi-item-merge').click(function() {
|
||||||
var items = getTableData(table);
|
var items = getTableData(table);
|
||||||
|
|
||||||
@ -2327,6 +2332,7 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'assign stock' button
|
||||||
$('#multi-item-assign').click(function() {
|
$('#multi-item-assign').click(function() {
|
||||||
|
|
||||||
var items = getTableData(table);
|
var items = getTableData(table);
|
||||||
@ -2338,6 +2344,7 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'un-assign stock' button
|
||||||
$('#multi-item-order').click(function() {
|
$('#multi-item-order').click(function() {
|
||||||
|
|
||||||
var selections = getTableData(table);
|
var selections = getTableData(table);
|
||||||
@ -2355,6 +2362,7 @@ function loadStockTable(table, options) {
|
|||||||
orderParts(parts, {});
|
orderParts(parts, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'delete stock' button
|
||||||
$('#multi-item-delete').click(function() {
|
$('#multi-item-delete').click(function() {
|
||||||
var selections = getTableData(table);
|
var selections = getTableData(table);
|
||||||
|
|
||||||
@ -2366,6 +2374,46 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
stockAdjustment('delete');
|
stockAdjustment('delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for 'change status' button
|
||||||
|
$('#multi-item-status').click(function() {
|
||||||
|
let selections = getTableData(table);
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
items.push(item.pk);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (items.length == 0) {
|
||||||
|
showAlertDialog(
|
||||||
|
'{% trans "Select Stock Items" %}',
|
||||||
|
'{% trans "Select one or more stock items" %}'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class='alert alert-info alert-block>
|
||||||
|
{% trans "Selected stock items" %}: ${items.length}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
constructForm('{% url "api-stock-change-status" %}', {
|
||||||
|
title: '{% trans "Change Stock Status" %}',
|
||||||
|
method: 'POST',
|
||||||
|
preFormContent: html,
|
||||||
|
fields: {
|
||||||
|
status: {},
|
||||||
|
note: {},
|
||||||
|
},
|
||||||
|
processBeforeUpload: function(data) {
|
||||||
|
data.items = items;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: function() {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
<li><a class='dropdown-item' href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
<li><a class='dropdown-item' href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Count stock" %}</a></li>
|
<li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Count stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
||||||
|
<li><a class='dropdown-item' href='#' id='multi-item-status' title='{% trans "Change stock status" %}'><span class='fas fa-info-circle icon-blue'></span> {% trans "Change stock status" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
|
||||||
<li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user