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 %} -
- - -
+ {% include "hover_image.html" with image=item.sub_part.image %}
{{ 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 %} -
- - {% if variant.image %} - - {% endif %} -
+ {% 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 += "\n"; - - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - - html += ''; - - for (idx=0; idx'; - - if (item.location) { - html += ''; - } else { - html += ''; - } - - html += ''; - - html += ""; - - html += ''; - } - - html += '
ItemLocationQuantity' + options.action + '
' + item.location.name + 'No location set' + item.quantity + ' 0) { - html += "max='" + vMax + "' "; - } - - html += "type='number' id='q-update-" + item.pk + "'/>
'; - - html += "
"; - html += "

Note field must be filled

"; - - html += ` -
-
- -

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 += "
"; - - html += "
"; - - html += "

Note field must be filled

"; - - html += "
The following stock items will be moved:
"; - - html += ` - - - - - - - - `; - - 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 += ""; - - html += ""; - html += ""; - html += ""; - - html += ""; - - html += ""; - } - - html += "
PartLocationAvailableMoving
" + name + "" + item.location__path + "" + item.quantity + ""; - 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 @@
@@ -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 %} diff --git a/InvenTree/stock/templates/stock/stock_adjust.html b/InvenTree/stock/templates/stock/stock_adjust.html new file mode 100644 index 0000000000..9bd4203725 --- /dev/null +++ b/InvenTree/stock/templates/stock/stock_adjust.html @@ -0,0 +1,39 @@ +{% block pre_form_content %} + +{% endblock %} + +
+ {% csrf_token %} + {% load crispy_forms_tags %} + + + + + + + + + + + {% for item in stock_items %} + + + + + + + {% endfor %} +
Stock ItemLocation{{ stock_action_title }}
{% include "hover_image.html" with image=item.part.image %} + {{ item.part.full_name }} {{ item.part.description }}{{ item.location.pathstring }} + + {% if item.error %} +
{{ item.error }} + {% endif %} +
+ + {% crispy form %} + +
\ 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 %} + +
+ {% if image %} + + {% endif %} + + {% if image %} + + + {% endif %} +
\ 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 @@