mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Vast improvements to stocktake API endpoint
- Also acts to ADD and REMOVE stock - Send 'action' field to specify which one to perform - Fixed add_stock and remove_stock funcs for StockItem model - Autoatically add transaction notes for all actions
This commit is contained in:
parent
ca2d3a1a7b
commit
25e0de1ce7
@ -125,3 +125,7 @@
|
|||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning-msg {
|
||||||
|
color: #e00;
|
||||||
|
}
|
@ -95,14 +95,6 @@ function getPartCategories(filters={}, options={}) {
|
|||||||
return inventreeGet('/api/part/category/', filters, options);
|
return inventreeGet('/api/part/category/', filters, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStock(filters={}, options={}) {
|
|
||||||
return inventreeGet('/api/stock/', filters, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStockLocations(filters={}, options={}) {
|
|
||||||
return inventreeGet('/api/stock/location/', filters, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCompanies(filters={}, options={}) {
|
function getCompanies(filters={}, options={}) {
|
||||||
return inventreeGet('/api/company/', filters, options);
|
return inventreeGet('/api/company/', filters, options);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,180 @@
|
|||||||
|
/* Stock API functions
|
||||||
|
* Requires api.js to be loaded first
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getStockList(filters={}, options={}) {
|
||||||
|
return inventreeGet('/api/stock/', filters, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStockDetail(pk, options={}) {
|
||||||
|
return inventreeGet('/api/stock/' + pk + '/', {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStockLocations(filters={}, options={}) {
|
||||||
|
return inventreeGet('/api/stock/location/', filters, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Present user with a dialog to update multiple stock items
|
||||||
|
* Possible actions:
|
||||||
|
* - Stocktake
|
||||||
|
* - Take stock
|
||||||
|
* - Add stock
|
||||||
|
*/
|
||||||
|
function updateStock(items, options={}) {
|
||||||
|
|
||||||
|
if (!options.action) {
|
||||||
|
alert('No action supplied to stock update');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modal = options.modal || '#modal-form';
|
||||||
|
|
||||||
|
if (items.length == 0) {
|
||||||
|
alert('No items selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
html += "<table class='table table-striped table-condensed' id='stocktake-table'>\n";
|
||||||
|
|
||||||
|
html += '<thead><tr>';
|
||||||
|
html += '<th>Item</th>';
|
||||||
|
html += '<th>Location</th>';
|
||||||
|
html += '<th>Quantity</th>';
|
||||||
|
|
||||||
|
html += '</thead><tbody>';
|
||||||
|
|
||||||
|
for (idx=0; idx<items.length; idx++) {
|
||||||
|
var item = items[idx];
|
||||||
|
|
||||||
|
var vMin = 0;
|
||||||
|
var vMax = item.quantity;
|
||||||
|
var vCur = item.quantity;
|
||||||
|
|
||||||
|
if (options.action == 'remove') {
|
||||||
|
vCur = 0;
|
||||||
|
}
|
||||||
|
else if (options.action == 'add') {
|
||||||
|
vCur = 0;
|
||||||
|
vMax = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
|
||||||
|
html += '<td>' + item.part.name + '</td>';
|
||||||
|
html += '<td>' + item.location.name + '</td>';
|
||||||
|
html += "<td><input class='form-control' ";
|
||||||
|
html += "value='" + vCur + "' ";
|
||||||
|
html += "min='" + vMin + "' ";
|
||||||
|
|
||||||
|
if (vMax > 0) {
|
||||||
|
html += "max='" + vMax + "' ";
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "type='number' id='q-" + item.pk + "'/></td>";
|
||||||
|
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</tbody></table>';
|
||||||
|
|
||||||
|
html += "<hr><input type='text' id='stocktake-notes' placeholder='Notes'/>";
|
||||||
|
|
||||||
|
html += "<p class='warning-msg'>Note field must be filled</p>";
|
||||||
|
|
||||||
|
var title = '';
|
||||||
|
|
||||||
|
if (options.action == 'stocktake') {
|
||||||
|
title = 'Stocktake';
|
||||||
|
}
|
||||||
|
else if (options.action == 'remove') {
|
||||||
|
title = 'Remove stock items';
|
||||||
|
}
|
||||||
|
else if (options.action == 'add') {
|
||||||
|
title = 'Add stock items';
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal({
|
||||||
|
modal: modal,
|
||||||
|
title: title,
|
||||||
|
content: html
|
||||||
|
});
|
||||||
|
|
||||||
|
$(modal).on('click', '#modal-form-submit', function() {
|
||||||
|
|
||||||
|
var stocktake = [];
|
||||||
|
|
||||||
|
var valid = true;
|
||||||
|
|
||||||
|
// Form stocktake data
|
||||||
|
for (idx = 0; idx < items.length; idx++) {
|
||||||
|
var item = items[idx];
|
||||||
|
|
||||||
|
var q = $(modal).find("#q-" + item.pk).val();
|
||||||
|
|
||||||
|
stocktake.push({
|
||||||
|
pk: item.pk,
|
||||||
|
quantity: q
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
alert('Invalid data');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inventreeUpdate("/api/stock/stocktake/",
|
||||||
|
{
|
||||||
|
'action': options.action,
|
||||||
|
'items[]': stocktake,
|
||||||
|
'notes': $(modal).find('#stocktake-notes').val()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
closeModal(modal);
|
||||||
|
if (options.success) {
|
||||||
|
options.success();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(error) {
|
||||||
|
alert(error);
|
||||||
|
},
|
||||||
|
method: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustStock(options) {
|
||||||
|
if (options.items) {
|
||||||
|
updateStock(options.items, options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Lookup of individual item
|
||||||
|
if (options.query.pk) {
|
||||||
|
getStockDetail(options.query.pk,
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
updateStock([response], options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
getStockList(options.query,
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
updateStock(response, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function moveStockItems(items, options) {
|
function moveStockItems(items, options) {
|
||||||
|
|
||||||
var modal = '#modal-form';
|
var modal = options.modal || '#modal-form';
|
||||||
|
|
||||||
if ('modal' in options) {
|
|
||||||
modal = options.modal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length == 0) {
|
if (items.length == 0) {
|
||||||
alert('No stock items selected');
|
alert('No stock items selected');
|
||||||
@ -35,9 +204,11 @@ function moveStockItems(items, options) {
|
|||||||
getStockLocations({},
|
getStockLocations({},
|
||||||
{
|
{
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
openModal(modal);
|
openModal({
|
||||||
modalSetTitle(modal, "Move " + items.length + " stock items");
|
modal: modal,
|
||||||
modalSetButtonText(modal, "Move");
|
title: "Move " + items.length + " stock items",
|
||||||
|
buttonText: "Move"
|
||||||
|
});
|
||||||
|
|
||||||
// Extact part row info
|
// Extact part row info
|
||||||
var parts = [];
|
var parts = [];
|
||||||
@ -85,83 +256,6 @@ function moveStockItems(items, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function countStockItems(items, options) {
|
|
||||||
var modal = '#modal-form';
|
|
||||||
|
|
||||||
if ('modal' in options) {
|
|
||||||
modal = options.modal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length == 0) {
|
|
||||||
alert('No stock items selected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tbl = "<table class='table table-striped table-condensed' id='stocktake-table'></table>";
|
|
||||||
|
|
||||||
openModal(modal);
|
|
||||||
modalSetTitle(modal, 'Stocktake ' + items.length + ' items');
|
|
||||||
|
|
||||||
$(modal).find('.modal-form-content').html(tbl);
|
|
||||||
|
|
||||||
$(modal).find('#stocktake-table').bootstrapTable({
|
|
||||||
data: items,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
checkbox: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'part.name',
|
|
||||||
title: 'Part',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'location.name',
|
|
||||||
title: 'Location',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'quantity',
|
|
||||||
title: 'Quantity',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
$(modal).find('#stocktake-table').bootstrapTable('checkAll');
|
|
||||||
|
|
||||||
$(modal).on('click', '#modal-form-submit', function() {
|
|
||||||
var selections = $(modal).find('#stocktake-table').bootstrapTable('getSelections');
|
|
||||||
|
|
||||||
var stocktake = [];
|
|
||||||
|
|
||||||
console.log('Performing stocktake on ' + selections.length + ' items');
|
|
||||||
|
|
||||||
for (i = 0; i<selections.length; i++) {
|
|
||||||
var item = {
|
|
||||||
pk: selections[i].pk,
|
|
||||||
quantity: selections[i].quantity,
|
|
||||||
};
|
|
||||||
|
|
||||||
stocktake.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
inventreeUpdate("/api/stock/stocktake/",
|
|
||||||
{
|
|
||||||
'items[]': stocktake,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
closeModal(modal);
|
|
||||||
if (options.success) {
|
|
||||||
options.success();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(error) {
|
|
||||||
alert(error);
|
|
||||||
},
|
|
||||||
method: 'post',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteStockItems(items, options) {
|
function deleteStockItems(items, options) {
|
||||||
|
|
||||||
var modal = '#modal-delete';
|
var modal = '#modal-delete';
|
||||||
@ -179,6 +273,8 @@ function deleteStockItems(items, options) {
|
|||||||
//TODO
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
openModal(modal);
|
openModal({
|
||||||
modalSetTitle(modal, 'Delete ' + items.length + ' stock items');
|
modal: modal,
|
||||||
|
title: 'Delete ' + items.length + ' stock items'
|
||||||
|
});
|
||||||
}
|
}
|
@ -52,6 +52,13 @@ class StockFilter(FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class StockStocktake(APIView):
|
class StockStocktake(APIView):
|
||||||
|
"""
|
||||||
|
Stocktake API endpoint provides stock update of multiple items simultaneously
|
||||||
|
The 'action' field tells the type of stock action to perform:
|
||||||
|
* 'stocktake' - Count the stock item(s)
|
||||||
|
* 'remove' - Remove the quantity provided from stock
|
||||||
|
* 'add' - Add the quantity provided from stock
|
||||||
|
"""
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.IsAuthenticatedOrReadOnly,
|
permissions.IsAuthenticatedOrReadOnly,
|
||||||
@ -59,6 +66,16 @@ class StockStocktake(APIView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
if not 'action' in request.data:
|
||||||
|
raise ValidationError({'action': 'Stocktake action must be provided'})
|
||||||
|
|
||||||
|
action = request.data['action']
|
||||||
|
|
||||||
|
ACTIONS = ['stocktake', 'remove', 'add']
|
||||||
|
|
||||||
|
if not action in ACTIONS:
|
||||||
|
raise ValidationError({'action': 'Action must be one of ' + ','.join(ACTIONS)})
|
||||||
|
|
||||||
if not 'items[]' in request.data:
|
if not 'items[]' in request.data:
|
||||||
raise ValidationError({'items[]:' 'Request must contain list of items'})
|
raise ValidationError({'items[]:' 'Request must contain list of items'})
|
||||||
|
|
||||||
@ -86,8 +103,22 @@ class StockStocktake(APIView):
|
|||||||
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
|
# Stocktake notes
|
||||||
|
notes = ''
|
||||||
|
|
||||||
|
if 'notes' in request.data:
|
||||||
|
notes = request.data['notes']
|
||||||
|
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
item['item'].stocktake(item['quantity'], request.user)
|
quantity = int(item['quantity'])
|
||||||
|
|
||||||
|
if action == u'stocktake':
|
||||||
|
item['item'].stocktake(quantity, request.user, notes=notes)
|
||||||
|
elif action == u'remove':
|
||||||
|
item['item'].take_stock(quantity, request.user, notes=notes)
|
||||||
|
elif action == u'add':
|
||||||
|
item['item'].add_stock(quantity, request.user, notes=notes)
|
||||||
|
|
||||||
return Response({'success': 'success'})
|
return Response({'success': 'success'})
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ class StockItem(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def stocktake(self, count, user):
|
def stocktake(self, count, user, notes=''):
|
||||||
""" Perform item stocktake.
|
""" Perform item stocktake.
|
||||||
When the quantity of an item is counted,
|
When the quantity of an item is counted,
|
||||||
record the date of stocktake
|
record the date of stocktake
|
||||||
@ -252,35 +252,54 @@ class StockItem(models.Model):
|
|||||||
self.stocktake_user = user
|
self.stocktake_user = user
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
self.add_transaction_note('Stocktake',
|
self.add_transaction_note('Stocktake - counted {n} items'.format(n=count),
|
||||||
user,
|
user,
|
||||||
notes='Counted {n} items'.format(n=count),
|
notes=notes,
|
||||||
system=True)
|
system=True)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def add_stock(self, amount):
|
def add_stock(self, quantity, user, notes=''):
|
||||||
""" Add items to stock
|
""" Add items to stock
|
||||||
This function can be called by initiating a ProjectRun,
|
This function can be called by initiating a ProjectRun,
|
||||||
or by manually adding the items to the stock location
|
or by manually adding the items to the stock location
|
||||||
"""
|
"""
|
||||||
|
|
||||||
amount = int(amount)
|
quantity = int(quantity)
|
||||||
|
|
||||||
if self.infinite or amount == 0:
|
# Ignore amounts that do not make sense
|
||||||
|
if quantity <= 0 or self.infinite:
|
||||||
return
|
return
|
||||||
|
|
||||||
amount = int(amount)
|
self.quantity += quantity
|
||||||
|
|
||||||
q = self.quantity + amount
|
|
||||||
if q < 0:
|
|
||||||
q = 0
|
|
||||||
|
|
||||||
self.quantity = q
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
self.add_transaction_note('Added {n} items to stock'.format(n=quantity),
|
||||||
|
user,
|
||||||
|
notes=notes,
|
||||||
|
system=True)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def take_stock(self, amount):
|
def take_stock(self, quantity, user, notes=''):
|
||||||
self.add_stock(-amount)
|
""" Remove items from stock
|
||||||
|
"""
|
||||||
|
|
||||||
|
quantity = int(quantity)
|
||||||
|
|
||||||
|
if quantity <= 0 or self.infinite:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.quantity -= quantity
|
||||||
|
|
||||||
|
if self.quantity < 0:
|
||||||
|
self.quantity = 0
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
self.add_transaction_note('Removed {n} items from stock'.format(n=quantity),
|
||||||
|
user,
|
||||||
|
notes=notes,
|
||||||
|
system=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
s = '{n} x {part}'.format(
|
s = '{n} x {part}'.format(
|
||||||
|
Loading…
Reference in New Issue
Block a user