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:
Oliver 2018-05-08 22:06:28 +10:00
parent ca2d3a1a7b
commit 25e0de1ce7
5 changed files with 252 additions and 110 deletions

View File

@ -125,3 +125,7 @@
.float-right { .float-right {
float: right; float: right;
} }
.warning-msg {
color: #e00;
}

View File

@ -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);
} }

View File

@ -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'
});
} }

View File

@ -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'})

View File

@ -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(