diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index f6fdbc4d46..338e664252 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -9,7 +9,7 @@ as JSON objects and passing them to modal forms (using jQuery / bootstrap).
from __future__ import unicode_literals
from django.template.loader import render_to_string
-from django.http import JsonResponse
+from django.http import JsonResponse, HttpResponseRedirect
from django.views import View
from django.views.generic import UpdateView, CreateView
@@ -132,6 +132,9 @@ class AjaxMixin(object):
JSON response object
"""
+ if not request.is_ajax():
+ return HttpResponseRedirect('/')
+
if context is None:
try:
context = self.get_context_data()
diff --git a/InvenTree/build/templates/build/allocation_item.html b/InvenTree/build/templates/build/allocation_item.html
index 920012e953..ebe05ce4d4 100644
--- a/InvenTree/build/templates/build/allocation_item.html
+++ b/InvenTree/build/templates/build/allocation_item.html
@@ -6,10 +6,7 @@
{% block collapse_panel_setup %}class='panel part-allocation' id='allocation-panel-{{ item.sub_part.id }}'{% endblock %}
{% block collapse_title %}
-
{{ item.sub_part.full_name }}
{{ item.sub_part.description }}
diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html
index dc2160a006..72d328df9b 100644
--- a/InvenTree/build/templates/build/auto_allocate.html
+++ b/InvenTree/build/templates/build/auto_allocate.html
@@ -21,10 +21,7 @@ Automatically allocate stock to this build?
{% for item in allocations %}
-
-
-
-
+ {% include "hover_image.html" with image=item.stock_item.part.image %}
{{ item.stock_item.part.full_name }}
diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index b2102ed74b..7cb5a2d659 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -30,7 +30,7 @@ InvenTree | Build - {{ build }}
Part
- {{ build.part.full_name }}
+ {{ build.part.full_name }}
Quantity
diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html
index 9f41bee54e..0a8f094614 100644
--- a/InvenTree/build/templates/build/complete.html
+++ b/InvenTree/build/templates/build/complete.html
@@ -18,10 +18,7 @@ The following items will be removed from stock:
{% for item in taking %}
-
-
-
-
+ {% include "hover_image.html" with image=item.stock_item.part.image %}
{{ item.stock_item.part.full_name }}
@@ -38,10 +35,7 @@ No parts have been allocated to this build.
The following items will be created:
-
-
-
-
+ {% include "hover_image.html" with image=build.part.image %}
{{ build.quantity }} x {{ build.part.full_name }}
diff --git a/InvenTree/build/templates/build/required.html b/InvenTree/build/templates/build/required.html
index 6ab16032ab..66aea7d48a 100644
--- a/InvenTree/build/templates/build/required.html
+++ b/InvenTree/build/templates/build/required.html
@@ -19,10 +19,7 @@
{% for item in build.required_parts %}
-
-
-
-
+ {% include "hover_image.html" with image=item.part.image %}
{{ item.part.full_name }}
{{ item.part.total_stock }}
diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html
index 98ac08a8d3..8061e32e7a 100644
--- a/InvenTree/part/templates/part/variants.html
+++ b/InvenTree/part/templates/part/variants.html
@@ -33,12 +33,7 @@
{% for variant in part.variants.all %}
-
+ {% include "hover_image.html" with image=variant.image %}
{{ variant.full_name }}
{{ variant.description }}
diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css
index 0769380fe1..da7d38d250 100644
--- a/InvenTree/static/css/inventree.css
+++ b/InvenTree/static/css/inventree.css
@@ -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;
}
diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js
index 800b23bbb6..65b0c55da5 100644
--- a/InvenTree/static/script/inventree/stock.js
+++ b/InvenTree/static/script/inventree/stock.js
@@ -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 += "';
-
- html += " ";
- html += "Note field must be filled
";
-
- html += `
-
-
-
-
- Confirm Stocktake
-
-
Confirm stock count
-
`;
-
-
- 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: \n";
-
- html += "";
-
- for (i = 0; i < response.length; i++) {
- var loc = response[i];
-
- html += makeOption(loc.pk, loc.pathstring + ' - ' + loc.description + ' ');
- }
-
- html += " ";
-
- html += " ";
-
- html += "Note field must be filled
";
-
- html += " The following stock items will be moved: ";
-
- html += `
- ";
-
- 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,40 +183,43 @@ function loadStockTable(table, options) {
linkButtonsToSelection(table, options.buttons);
}
+ function stockAdjustment(action) {
+ var items = $("#stock-table").bootstrapTable("getSelections");
+
+ var stock = [];
+
+ items.forEach(function(item) {
+ stock.push(item.pk);
+ });
+
+ launchModalForm("/stock/adjust/",
+ {
+ data: {
+ action: action,
+ stock: stock,
+ },
+ success: function() {
+ $("#stock-table").bootstrapTable('refresh');
+ },
+ }
+ );
+ }
+
// Automatically link button callbacks
$('#multi-item-stocktake').click(function() {
- updateStockItems({
- action: 'stocktake',
- });
- return false;
+ stockAdjustment('count');
});
$('#multi-item-remove').click(function() {
- updateStockItems({
- action: 'remove',
- });
- return false;
+ stockAdjustment('take');
});
$('#multi-item-add').click(function() {
- updateStockItems({
- action: 'add',
- });
- return false;
+ stockAdjustment('add');
});
$("#multi-item-move").click(function() {
-
- var items = $("#stock-table").bootstrapTable('getSelections');
-
- moveStockItems(items,
- {
- success: function() {
- $("#stock-table").bootstrapTable('refresh');
- }
- });
-
- return false;
+ stockAdjustment('move');
});
}
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 7fa88a646f..c1c2b8e1cf 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -470,9 +470,9 @@ stock_api_urls = [
url(r'location/(?P\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'),
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index a05c8caef0..7187cada4e 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -42,26 +42,45 @@ 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()
+
+ choices = [(None, '---------')]
+
+ for loc in locs:
+ choices.append((loc.pk, loc.pathstring + ' - ' + loc.description))
+
+ return choices
+
+ 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')
+ confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.fields['destination'].choices = self.get_location_choices()
class Meta:
model = StockItem
fields = [
- 'location',
- ]
-
-
-class StocktakeForm(forms.ModelForm):
-
- class Meta:
- model = StockItem
-
- fields = [
- 'quantity',
+ 'destination',
+ 'note',
+ # 'transaction',
+ 'confirm',
]
diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html
index cb54ac6f5e..40d78eb7e1 100644
--- a/InvenTree/stock/templates/stock/item.html
+++ b/InvenTree/stock/templates/stock/item.html
@@ -22,11 +22,13 @@
\ No newline at end of file
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index 37e54750de..4666eb05dc 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -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,6 +34,8 @@ stock_urls = [
url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'),
+ url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
+
# Individual stock items
url(r'^item/(?P\d+)/', include(stock_item_detail_urls)),
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 42ca16d3ba..3ede00697e 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -5,21 +5,26 @@ Django views for interacting with Stock app
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+from django.views.generic.edit import FormMixin
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 AdjustStockForm
class StockIndex(ListView):
@@ -120,7 +125,276 @@ class StockItemQRCode(QRCodeView):
return item.format_barcode()
except StockItem.DoesNotExist:
return None
+
+
+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_adjust.html'
+ ajax_form_title = 'Adjust Stock'
+ form_class = AdjustStockForm
+ stock_items = []
+
+ def get_GET_items(self):
+ """ Return list of stock items initally requested using GET """
+
+ # Start with all 'in stock' items
+ items = StockItem.objects.filter(customer=None, belongs_to=None)
+
+ # 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):
+
+ self.request = request
+
+ # Action
+ self.stock_action = request.GET.get('action', '').lower()
+
+ # 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
+
+ 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': valid,
+ }
+
+ 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):
"""
@@ -306,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
diff --git a/InvenTree/templates/hover_image.html b/InvenTree/templates/hover_image.html
new file mode 100644
index 0000000000..0853c4fc12
--- /dev/null
+++ b/InvenTree/templates/hover_image.html
@@ -0,0 +1,12 @@
+{% load static %}
+
+
\ No newline at end of file
diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html
index fae7b04214..acdd371d08 100644
--- a/InvenTree/templates/stock_table.html
+++ b/InvenTree/templates/stock_table.html
@@ -8,8 +8,8 @@