Merge remote-tracking branch 'inventree/master'

# Conflicts:
#	InvenTree/static/script/inventree/stock.js
#	InvenTree/stock/forms.py
#	InvenTree/stock/urls.py
#	InvenTree/stock/views.py
This commit is contained in:
Oliver Walters 2019-06-02 12:51:56 +10:00
commit 9577e4e505
16 changed files with 393 additions and 569 deletions

View File

@ -6,10 +6,7 @@
{% block collapse_panel_setup %}class='panel part-allocation' id='allocation-panel-{{ item.sub_part.id }}'{% endblock %}
{% block collapse_title %}
<div class='hover-icon media-left' style='float: left;'>
<img class='hover-img-thumb' src="{% if item.sub_part.image %}{{ item.sub_part.image.url }}{% endif %}">
<img class='hover-img-large' src="{% if item.sub_part.image %}{{ item.sub_part.image.url }}{% endif %}">
</div>
{% include "hover_image.html" with image=item.sub_part.image %}
<div>
{{ item.sub_part.full_name }}
<small><i>{{ item.sub_part.description }}</i></small>

View File

@ -21,10 +21,7 @@ Automatically allocate stock to this build?
{% for item in allocations %}
<tr>
<td>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{% if item.stock_item.part.image %}{{ item.stock_item.part.image.url }}{% endif %}'>
<img class='hover-img-large' src='{% if item.stock_item.part.image %}{{ item.stock_item.part.image.url }}{% endif %}'>
</a>
{% include "hover_image.html" with image=item.stock_item.part.image %}
</td>
<td>
{{ item.stock_item.part.full_name }}<br>

View File

@ -30,7 +30,7 @@ InvenTree | Build - {{ build }}
</tr>
<tr>
<td>Part</td>
<td>{{ build.part.full_name }}</td>
<td><a href="{% url 'part-detail' build.part.id %}">{{ build.part.full_name }}</a></td>
</tr>
<tr>
<td>Quantity</td>

View File

@ -18,10 +18,7 @@ The following items will be removed from stock:
{% for item in taking %}
<tr>
<td>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{{ item.stock_item.part.image.url }}'>
<img class='hover-img-large' src='{{ item.stock_item.part.image.url }}'>
</a>
{% include "hover_image.html" with image=item.stock_item.part.image %}
</td>
<td>
{{ item.stock_item.part.full_name }}<br>
@ -38,10 +35,7 @@ No parts have been allocated to this build.
<hr>
The following items will be created:
<div class='panel panel-default'>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{{ build.part.image.url }}'>
<img class='hover-img-large' src='{{ build.part.image.url }}'>
</a>
{% include "hover_image.html" with image=build.part.image %}
{{ build.quantity }} x {{ build.part.full_name }}
</div>

View File

@ -19,10 +19,7 @@
{% for item in build.required_parts %}
<tr>
<td>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{% if item.part.image %}{{ item.part.image.url }}{% endif %}'>
<img class='hover-img-large' src='{% if item.part.image %}{{ item.part.image.url }}{% endif %}'>
</a>
{% include "hover_image.html" with image=item.part.image %}
<a class='hover-icon'a href="{% url 'part-detail' item.part.id %}">{{ item.part.full_name }}</a>
</td>
<td>{{ item.part.total_stock }}</td>

View File

@ -33,12 +33,7 @@
{% for variant in part.variants.all %}
<tr>
<td>
<div class='hover-icon media-left' style='float: left;'>
<img class='hover-img-thumb' src="{% if variant.image %}{{ variant.image.url }}{% else %}{% static 'img/blank_image.png' %}{% endif %}">
{% if variant.image %}
<img class='hover-img-large' src="{{ variant.image.url }}">
{% endif %}
</div>
{% include "hover_image.html" with image=variant.image %}
<a href="{% url 'part-detail' variant.id %}">{{ variant.full_name }}</a>
</td>
<td>{{ variant.description }}</td>

View File

@ -192,7 +192,7 @@
}
.modal-dialog {
width: 45%;
width: 60%;
}
.modal-secondary .modal-dialog {
@ -225,6 +225,7 @@
/* Force a control-label div to be 100% width */
.modal .control-label {
width: 100%;
margin-top: 5px;
}
.modal .control-label .btn {
@ -281,6 +282,13 @@
margin-right: 2px;
}
.btn-remove {
padding: 3px;
padding-left: 5px;
padding-right: 5px;
color: #A11;
}
.button-toolbar {
padding-left: 0px;
}

View File

@ -14,377 +14,34 @@ 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
/* Functions for interacting with stock management forms
*/
function updateStock(items, options={}) {
if (!options.action) {
alert('No action supplied to stock update');
return false;
}
function removeStockRow(e) {
// Remove a selected row from a stock modal form
var modal = options.modal || '#modal-form';
e = e || window.event;
var src = e.target || e.srcElement;
if (items.length == 0) {
alert('No items selected');
return;
}
var row = $(src).attr('row');
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 += '<th>' + options.action + '</th>';
html += '</thead><tbody>';
for (idx=0; idx<items.length; idx++) {
var item = items[idx];
var vMin = 0;
var vMax = 0;
var vCur = item.quantity;
if (options.action == 'remove') {
vCur = 0;
vMax = item.quantity;
}
else if (options.action == 'add') {
vCur = 0;
vMax = 0;
}
html += '<tr>';
html += '<td>' + item.part.full_name + '</td>';
if (item.location) {
html += '<td>' + item.location.name + '</td>';
} else {
html += '<td><i>No location set</i></td>';
}
html += '<td>' + item.quantity + '</td>';
html += "<td><input class='form-control' ";
html += "value='" + vCur + "' ";
html += "min='" + vMin + "' ";
if (vMax > 0) {
html += "max='" + vMax + "' ";
}
html += "type='number' id='q-update-" + item.pk + "'/></td>";
html += '</tr>';
}
html += '</tbody></table>';
html += "<hr><input type='text' id='stocktake-notes' placeholder='Notes'/>";
html += "<p class='help-inline' id='note-warning'><strong>Note field must be filled</strong></p>";
html += `
<hr>
<div class='control-group'>
<label class='checkbox'>
<input type='checkbox' id='stocktake-confirm' placeholder='Confirm'/>
Confirm Stocktake
</label>
<p class='help-inline' id='confirm-warning'><strong>Confirm stock count</strong></p>
</div>`;
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).find('#note-warning').hide();
$(modal).find('#confirm-warning').hide();
modalEnable(modal, true);
modalSubmit(modal, function() {
var stocktake = [];
var notes = $(modal).find('#stocktake-notes').val();
var confirm = $(modal).find('#stocktake-confirm').is(':checked');
var valid = true;
if (!notes) {
$(modal).find('#note-warning').show();
valid = false;
}
if (!confirm) {
$(modal).find('#confirm-warning').show();
valid = false;
}
if (!valid) {
return false;
}
// Form stocktake data
for (idx = 0; idx < items.length; idx++) {
var item = items[idx];
var q = $(modal).find("#q-update-" + item.pk).val();
stocktake.push({
pk: item.pk,
quantity: q
});
};
if (!valid) {
alert('Invalid data');
return false;
}
inventreePut("/api/stock/stocktake/",
{
'action': options.action,
'items[]': stocktake,
'notes': $(modal).find('#stocktake-notes').val()
},
{
method: 'post',
}).then(function(response) {
closeModal(modal);
afterForm(response, options);
}).fail(function(xhr, status, error) {
alert(error);
});
});
}
function selectStockItems(options) {
/* Return list of selections from stock table
* If options.table not provided, assumed to be '#stock-table'
*/
var table_name = options.table || '#stock-table';
// Return list of selected items from the bootstrap table
return $(table_name).bootstrapTable('getSelections');
}
function adjustStock(options) {
if (options.items) {
updateStock(options.items, options);
}
else {
// Lookup of individual item
if (options.query.pk) {
getStockDetail(options.query.pk).then(function(response) {
updateStock([response], options);
});
}
else {
getStockList(options.query).then(function(response) {
updateStock(response, options);
});
}
}
}
function updateStockItems(options) {
/* Update one or more stock items selected from a stock-table
* Options available:
* 'action' - Action to perform - 'add' / 'remove' / 'stocktake'
* 'table' - ID of the stock table (default = '#stock-table'
*/
var table = options.table || '#stock-table';
var items = selectStockItems({
table: table,
});
// Pass items through
options.items = items;
options.table = table;
// On success, reload the table
options.success = function() {
$(table).bootstrapTable('refresh');
};
adjustStock(options);
}
function moveStockItems(items, options) {
var modal = options.modal || '#modal-form';
if (items.length == 0) {
alert('No stock items selected');
return;
}
function doMove(location, parts, notes) {
inventreePut("/api/stock/move/",
{
location: location,
'stock': parts,
'notes': notes,
},
{
method: 'post',
}).then(function(response) {
closeModal(modal);
afterForm(response, options);
}).fail(function(xhr, status, error) {
alert(error);
});
}
getStockLocations({},
{
success: function(response) {
// Extact part row info
var parts = [];
var html = "Select new location:<br>\n";
html += "<select class='select' id='stock-location'>";
for (i = 0; i < response.length; i++) {
var loc = response[i];
html += makeOption(loc.pk, loc.pathstring + ' - <i>' + loc.description + '</i>');
}
html += "</select><br>";
html += "<hr><input type='text' id='notes' placeholder='Notes'/>";
html += "<p class='warning-msg' id='note-warning'><i>Note field must be filled</i></p>";
html += "<hr>The following stock items will be moved:<hr>";
html += `
<table class='table table-striped table-condensed'>
<tr>
<th>Part</th>
<th>Location</th>
<th>Available</th>
<th>Moving</th>
</tr>
`;
for (i = 0; i < items.length; i++) {
parts.push({
pk: items[i].pk,
quantity: items[i].quantity,
});
var item = items[i];
var name = item.part__IPN;
if (name) {
name += ' | ';
}
name += item.part__name;
html += "<tr>";
html += "<td>" + name + "</td>";
html += "<td>" + item.location__path + "</td>";
html += "<td>" + item.quantity + "</td>";
html += "<td>";
html += "<input class='form-control' min='0' max='" + item.quantity + "'";
html += " value='" + item.quantity + "'";
html += "type='number' id='q-move-" + item.pk + "'/></td>";
html += "</tr>";
}
html += "</table>";
openModal({
modal: modal,
title: "Move " + items.length + " stock items",
submit_text: "Move",
content: html
});
//modalSetContent(modal, html);
attachSelect(modal);
modalEnable(modal, true);
$(modal).find('#note-warning').hide();
modalSubmit(modal, function() {
var locId = $(modal).find("#stock-location").val();
var notes = $(modal).find('#notes').val();
if (!notes) {
$(modal).find('#note-warning').show();
return false;
}
// Update the quantity for each item
for (var ii = 0; ii < parts.length; ii++) {
var pk = parts[ii].pk;
var q = $(modal).find('#q-move-' + pk).val();
parts[ii].quantity = q;
}
doMove(locId, parts, notes);
});
},
error: function(error) {
alert('Error getting stock locations:\n' + error.error);
}
});
$('#' + row).remove();
}
function loadStockTable(table, options) {
/* Load data into a stock table with adjustable options.
* Fetches data (via AJAX) and loads into a bootstrap table.
* Also links in default button callbacks.
*
* Options:
* url - URL for the stock query
* params - query params for augmenting stock data request
* groupByField - Column for grouping stock items
* buttons - Which buttons to link to stock selection callbacks
*/
var params = options.params || {};
// Aggregate stock items
//params.aggregate = true;
table.bootstrapTable({
sortable: true,
search: true,
@ -526,31 +183,8 @@ function loadStockTable(table, options) {
linkButtonsToSelection(table, options.buttons);
}
// Automatically link button callbacks
$('#multi-item-stocktake').click(function() {
updateStockItems({
action: 'stocktake',
});
return false;
});
$('#multi-item-remove').click(function() {
updateStockItems({
action: 'remove',
});
return false;
});
$('#multi-item-add').click(function() {
updateStockItems({
action: 'add',
});
return false;
});
$("#multi-item-move").click(function() {
var items = $('#stock-table').bootstrapTable('getSelections');
function stockAdjustment(action) {
var items = $("#stock-table").bootstrapTable("getSelections");
var stock = [];
@ -558,27 +192,34 @@ function loadStockTable(table, options) {
stock.push(item.pk);
});
launchModalForm("/stock/move/",
launchModalForm("/stock/adjust/",
{
data: {
action: action,
stock: stock,
},
success: function() {
$("#stock-table").bootstrapTable('refresh');
},
}
);
}
/*
// Automatically link button callbacks
$('#multi-item-stocktake').click(function() {
stockAdjustment('count');
});
var items = $("#stock-table").bootstrapTable('getSelections');
$('#multi-item-remove').click(function() {
stockAdjustment('take');
});
moveStockItems(items,
{
success: function() {
$("#stock-table").bootstrapTable('refresh');
}
});
$('#multi-item-add').click(function() {
stockAdjustment('add');
});
return false;
*/
$("#multi-item-move").click(function() {
stockAdjustment('move');
});
}

View File

@ -470,9 +470,9 @@ stock_api_urls = [
url(r'location/(?P<pk>\d+)/', include(location_endpoints)),
url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'),
url(r'move/?', StockMove.as_view(), name='api-stock-move'),
# These JSON endpoints have been replaced (for now) with server-side form rendering - 02/06/2019
# url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'),
# url(r'move/?', StockMove.as_view(), name='api-stock-move'),
url(r'track/?', StockTrackingList.as_view(), name='api-stock-track'),

View File

@ -42,8 +42,16 @@ class CreateStockItemForm(HelperForm):
]
class MoveStockItemForm(forms.ModelForm):
""" Form for moving a StockItem to a new location """
class AdjustStockForm(forms.ModelForm):
""" Form for performing simple stock adjustments.
- Add stock
- Remove stock
- Count stock
- Move stock
This form is used for managing stock adjuments for single or multiple stock items.
"""
def get_location_choices(self):
locs = StockLocation.objects.all()
@ -55,37 +63,27 @@ class MoveStockItemForm(forms.ModelForm):
return choices
location = forms.ChoiceField(label='Destination', required=True, help_text='Destination stock location')
destination = forms.ChoiceField(label='Destination', required=True, help_text='Destination stock location')
note = forms.CharField(label='Notes', required=True, help_text='Add note (required)')
transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts')
# transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts')
confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items')
def __init__(self, *args, **kwargs):
super(MoveStockItemForm, self).__init__(*args, **kwargs)
self.fields['location'].choices = self.get_location_choices()
super().__init__(*args, **kwargs)
self.fields['destination'].choices = self.get_location_choices()
class Meta:
model = StockItem
fields = [
'location',
'destination',
'note',
'transaction',
# 'transaction',
'confirm',
]
class StocktakeForm(forms.ModelForm):
class Meta:
model = StockItem
fields = [
'quantity',
]
class EditStockItemForm(HelperForm):
""" Form for editing a StockItem object.
Note that not all fields can be edited here (even if they can be specified during creation.

View File

@ -22,11 +22,13 @@
<ul class="dropdown-menu">
{% if item.in_stock %}
<li><a href="#" id='stock-edit' title='Edit stock item'>Edit stock item</a></li>
<li><a href="#" id='stock-move' title='Move stock item'>Move stock item</a></li>
<hr>
<li><a href='#' id='stock-add' title='Add stock'>Add to stock</a></li>
<li><a href='#' id='stock-remove' title='Remove stock'>Take from stock</a></li>
<li><a href='#' id='stock-remove' title='Take stock'>Take from stock</a></li>
<li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li>
<li><a href="#" id='stock-move' title='Move stock'>Move stock item</a></li>
{% endif %}
<hr>
<li><a href="#" id='stock-delete' title='Delete stock item'>Delete stock item</a></li>
</div>
</div>
@ -155,40 +157,33 @@
});
{% if item.in_stock %}
$("#stock-move").click(function() {
launchModalForm(
"{% url 'stock-item-move' item.id %}",
{
reload: true,
submit_text: "Move"
});
});
function itemAdjust(action) {
adjustStock({
query: {
pk: {{ item.id }},
},
action: action,
success: function() {
location.reload();
launchModalForm("/stock/adjust/",
{
data: {
action: action,
item: {{ item.id }},
},
reload: true,
}
});
);
}
$("#stock-move").click(function() {
itemAdjust("move");
});
$("#stock-stocktake").click(function() {
itemAdjust('stocktake');
return false;
itemAdjust('count');
});
$('#stock-remove').click(function() {
itemAdjust('remove');
return false;
itemAdjust('take');
});
$('#stock-add').click(function() {
itemAdjust('add');
return false;
});
{% endif %}

View File

@ -0,0 +1,39 @@
{% block pre_form_content %}
{% endblock %}
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
{% csrf_token %}
{% load crispy_forms_tags %}
<input type='hidden' name='stock_action' value='{{ stock_action }}'/>
<table class='table table-condensed table-striped' id='stock-table'>
<tr>
<th>Stock Item</th>
<th>Location</th>
<th>{{ stock_action_title }}</th>
<th></th>
</tr>
{% for item in stock_items %}
<tr id='stock-row-{{ item.id }}' class='error'>
<td>{% include "hover_image.html" with image=item.part.image %}
{{ item.part.full_name }} <small><i>{{ item.part.description }}</i></small></td>
<td>{{ item.location.pathstring }}</td>
<td>
<input class='numberinput'
min='0'
{% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
value='{{ item.new_quantity }}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
{% if item.error %}
<br><span class='help-inline'>{{ item.error }}</span>
{% endif %}
</td>
<td><button class='btn btn-default btn-remove' id='del-{{ item.id }}' title='Remove item' type='button'><span row='stock-row-{{ item.id }}' onclick='removeStockRow()' class='glyphicon glyphicon-small glyphicon-remove'></span></button></td>
</tr>
{% endfor %}
</table>
{% crispy form %}
</form>

View File

@ -19,8 +19,6 @@ stock_location_detail_urls = [
stock_item_detail_urls = [
url(r'^edit/?', views.StockItemEdit.as_view(), name='stock-item-edit'),
url(r'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'),
url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'),
url(r'^qr_code/?', views.StockItemQRCode.as_view(), name='stock-item-qr'),
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
@ -36,7 +34,7 @@ stock_urls = [
url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'),
url(r'^move/?', views.StockItemMoveMultiple.as_view(), name='stock-item-move-multiple'),
url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
# Individual stock items
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),

View File

@ -10,19 +10,21 @@ from django.views.generic import DetailView, ListView
from django.forms.models import model_to_dict
from django.forms import HiddenInput
from django.utils.translation import ugettext as _
from InvenTree.views import AjaxView
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
from InvenTree.helpers import str2bool
from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking
from .forms import EditStockLocationForm
from .forms import CreateStockItemForm
from .forms import EditStockItemForm
from .forms import MoveStockItemForm
from .forms import StocktakeForm
from .forms import MoveStockItemForm
from .forms import AdjustStockForm
class StockIndex(ListView):
@ -125,53 +127,274 @@ class StockItemQRCode(QRCodeView):
return None
class StockItemMoveMultiple(AjaxView, FormMixin):
""" Move multiple stock items """
class StockAdjust(AjaxView, FormMixin):
""" View for enacting simple stock adjustments:
- Take items from stock
- Add items to stock
- Count items
- Move stock
"""
ajax_template_name = 'stock/stock_move.html'
ajax_form_title = 'Move Stock'
form_class = MoveStockItemForm
ajax_template_name = 'stock/stock_adjust.html'
ajax_form_title = 'Adjust Stock'
form_class = AdjustStockForm
stock_items = []
def get_items(self, item_list):
""" Return list of stock items. """
def get_GET_items(self):
""" Return list of stock items initally requested using GET """
items = []
# Start with all 'in stock' items
items = StockItem.objects.filter(customer=None, belongs_to=None)
for pk in item_list:
try:
items.append(StockItem.objects.get(pk=pk))
except StockItem.DoesNotExist:
pass
# Client provides a list of individual stock items
if 'stock[]' in self.request.GET:
items = items.filter(id__in=self.request.GET.getlist('stock[]'))
# Client provides a PART reference
elif 'part' in self.request.GET:
items = items.filter(part=self.request.GET.get('part'))
# Client provides a LOCATION reference
elif 'location' in self.request.GET:
items = items.filter(location=self.request.GET.get('location'))
# Client provides a single StockItem lookup
elif 'item' in self.request.GET:
items = [StockItem.objects.get(id=self.request.GET.get('item'))]
# Unsupported query
else:
items = None
for item in items:
# Initialize quantity to zero for addition/removal
if self.stock_action in ['take', 'add']:
item.new_quantity = 0
# Initialize quantity at full amount for counting or moving
else:
item.new_quantity = item.quantity
return items
def get_POST_items(self):
""" Return list of stock items sent back by client on a POST request """
items = []
for item in self.request.POST:
if item.startswith('stock-id-'):
pk = item.replace('stock-id-', '')
q = self.request.POST[item]
try:
stock_item = StockItem.objects.get(pk=pk)
except StockItem.DoesNotExist:
continue
stock_item.new_quantity = q
items.append(stock_item)
return items
def get_context_data(self):
context = super().get_context_data()
context['stock_items'] = self.stock_items
context['stock_action'] = self.stock_action
context['stock_action_title'] = self.stock_action.capitalize()
return context
def get_form(self):
form = super().get_form()
if not self.stock_action == 'move':
form.fields.pop('destination')
return form
def get(self, request, *args, **kwargs):
item_list = request.GET.getlist('stock[]')
items = self.get_items(item_list)
self.request = request
print(items)
# Action
self.stock_action = request.GET.get('action', '').lower()
return self.renderJsonResponse(request, self.form_class())
# Pick a default action...
if self.stock_action not in ['move', 'count', 'take', 'add']:
self.stock_action = 'count'
# Choose the form title based on the action
titles = {
'move': 'Move Stock',
'count': 'Count Stock',
'take': 'Remove Stock',
'add': 'Add Stock'
}
self.ajax_form_title = titles[self.stock_action]
# Save list of items!
self.stock_items = self.get_GET_items()
return self.renderJsonResponse(request, self.get_form())
def post(self, request, *args, **kwargs):
self.request = request
self.stock_action = request.POST.get('stock_action').lower()
# Update list of stock items
self.stock_items = self.get_POST_items()
form = self.get_form()
valid = form.is_valid()
for item in self.stock_items:
try:
item.new_quantity = int(item.new_quantity)
except ValueError:
item.error = _('Must enter integer value')
valid = False
continue
print("Valid:", valid)
if item.new_quantity < 0:
item.error = _('Quantity must be positive')
valid = False
continue
if self.stock_action in ['move', 'take']:
if item.new_quantity > item.quantity:
item.error = _('Quantity must not exceed {x}'.format(x=item.quantity))
valid = False
continue
confirmed = str2bool(request.POST.get('confirm'))
if not confirmed:
valid = False
form.errors['confirm'] = [_('Confirm stock adjustment')]
data = {
'form_valid': False,
'form_valid': valid,
}
#form.errors['note'] = ['hello world']
if valid:
data['success'] = self.do_action()
return self.renderJsonResponse(request, form, data=data)
def do_action(self):
""" Perform stock adjustment action """
if self.stock_action == 'move':
destination = None
try:
destination = StockLocation.objects.get(id=self.request.POST.get('destination'))
except StockLocation.DoesNotExist:
pass
except ValueError:
pass
return self.do_move(destination)
elif self.stock_action == 'add':
return self.do_add()
elif self.stock_action == 'take':
return self.do_take()
elif self.stock_action == 'count':
return self.do_count()
else:
return 'No action performed'
def do_add(self):
count = 0
note = self.request.POST['note']
for item in self.stock_items:
if item.new_quantity <= 0:
continue
item.add_stock(item.new_quantity, self.request.user, notes=note)
count += 1
return _("Added stock to {n} items".format(n=count))
def do_take(self):
count = 0
note = self.request.POST['note']
for item in self.stock_items:
if item.new_quantity <= 0:
continue
item.take_stock(item.new_quantity, self.request.user, notes=note)
count += 1
return _("Removed stock from {n} items".format(n=count))
def do_count(self):
count = 0
note = self.request.POST['note']
for item in self.stock_items:
item.stocktake(item.new_quantity, self.request.user, notes=note)
count += 1
return _("Counted stock for {n} items".format(n=count))
def do_move(self, destination):
""" Perform actual stock movement """
count = 0
note = self.request.POST['note']
for item in self.stock_items:
# Avoid moving zero quantity
if item.new_quantity <= 0:
continue
# Do not move to the same location
if destination == item.location:
continue
item.move(destination, note, self.request.user, quantity=int(item.new_quantity))
count += 1
if count == 0:
return _('No items were moved')
else:
return _('Moved {n} items to {dest}'.format(
n=count,
dest=destination.pathstring))
class StockItemEdit(AjaxUpdateView):
"""
@ -357,76 +580,6 @@ class StockItemDelete(AjaxDeleteView):
ajax_form_title = 'Delete Stock Item'
class StockItemMove(AjaxUpdateView):
"""
View to move a StockItem from one location to another
Performs some data validation to prevent illogical stock moves
"""
model = StockItem
ajax_template_name = 'modal_form.html'
context_object_name = 'item'
ajax_form_title = 'Move Stock Item'
form_class = MoveStockItemForm
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST, instance=self.get_object())
if form.is_valid():
obj = self.get_object()
try:
loc_id = form['location'].value()
if loc_id:
loc = StockLocation.objects.get(pk=form['location'].value())
if str(loc.pk) == str(obj.pk):
form.errors['location'] = ['Item is already in this location']
else:
obj.move(loc, form['note'].value(), request.user)
else:
form.errors['location'] = ['Cannot move to an empty location']
except StockLocation.DoesNotExist:
form.errors['location'] = ['Location does not exist']
data = {
'form_valid': form.is_valid() and len(form.errors) == 0,
}
return self.renderJsonResponse(request, form, data)
class StockItemStocktake(AjaxUpdateView):
"""
View to perform stocktake on a single StockItem
Updates the quantity, which will also create a new StockItemTracking item
"""
model = StockItem
template_name = 'modal_form.html'
context_object_name = 'item'
ajax_form_title = 'Item stocktake'
form_class = StocktakeForm
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST, instance=self.get_object())
if form.is_valid():
obj = self.get_object()
obj.stocktake(form.data['quantity'], request.user)
data = {
'form_valid': form.is_valid()
}
return self.renderJsonResponse(request, form, data)
class StockTrackingIndex(ListView):
"""
StockTrackingIndex provides a page to display StockItemTracking objects

View File

@ -0,0 +1,12 @@
{% load static %}
<div class='hover-icon media-left' style='float: left;'>
{% if image %}
<a class='hover-icon'>
{% endif %}
<img class='hover-img-thumb' {% if image %}src='{{ image.url }}'{% else %}src='{% static "img/blank_image.png" %}'{% endif %}>
{% if image %}
<img class='hover-img-large' src='{{ image.url }}'>
</a>
{% endif %}
</div>

View File

@ -8,8 +8,8 @@
<ul class="dropdown-menu">
<li><a href="#" id='multi-item-add' title='Add to selected stock items'>Add stock</a></li>
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move</a></li>
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Count stock</a></li>
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move stock</a></li>
</ul>
</div>
</div>