From 2b4cb4d3e0c69e089b495cd6fc7555a724b6bc10 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 22:05:13 +1000 Subject: [PATCH 01/25] Beginning to move the stocktake forms server side --- InvenTree/stock/forms.py | 21 ++++++++++++ .../stock/templates/stock/stock_move.html | 1 + InvenTree/stock/urls.py | 2 ++ InvenTree/stock/views.py | 33 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 InvenTree/stock/templates/stock/stock_move.html diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index a05c8caef0..6614d67d72 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -45,13 +45,34 @@ class CreateStockItemForm(HelperForm): class MoveStockItemForm(forms.ModelForm): """ Form for moving a StockItem to a new location """ + 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 + + location = 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(MoveStockItemForm, self).__init__(*args, **kwargs) + + self.fields['location'].choices = self.get_location_choices() class Meta: model = StockItem fields = [ 'location', + 'note', + 'transaction', + 'confirm', ] diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html new file mode 100644 index 0000000000..c7de8c74b2 --- /dev/null +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -0,0 +1 @@ +{% extends "modal_form.html" %} \ No newline at end of file diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 37e54750de..513fff6cb0 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -36,6 +36,8 @@ 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'), + # 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..4d0a2c8af2 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -5,10 +5,12 @@ 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 InvenTree.views import AjaxView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView @@ -20,6 +22,7 @@ from .forms import CreateStockItemForm from .forms import EditStockItemForm from .forms import MoveStockItemForm from .forms import StocktakeForm +from .forms import MoveStockItemForm class StockIndex(ListView): @@ -120,6 +123,36 @@ class StockItemQRCode(QRCodeView): return item.format_barcode() except StockItem.DoesNotExist: return None + + +class StockItemMoveMultiple(AjaxView, FormMixin): + """ Move multiple stock items """ + + ajax_template_name = 'stock/stock_move.html' + ajax_form_title = 'Move Stock' + form_class = MoveStockItemForm + + + def get(self, request, *args, **kwargs): + + return self.renderJsonResponse(request, self.form_class()) + + def post(self, request, *args, **kwargs): + + form = self.get_form() + + valid = form.is_valid() + + print("Valid:", valid) + + data = { + 'form_valid': False, + } + + #form.errors['note'] = ['hello world'] + + return self.renderJsonResponse(request, form, data=data) + class StockItemEdit(AjaxUpdateView): From 0ce6c5f7d5b148b9bdd5573cea95eaa4f80d0d1e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 22:11:03 +1000 Subject: [PATCH 02/25] Redirect non-ajax forms to / --- InvenTree/InvenTree/views.py | 5 ++++- InvenTree/stock/urls.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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/stock/urls.py b/InvenTree/stock/urls.py index 513fff6cb0..237e2281f0 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -36,7 +36,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'^move/?', views.StockItemMoveMultiple.as_view(), name='stock-item-move-multiple'), # Individual stock items url(r'^item/(?P\d+)/', include(stock_item_detail_urls)), From 0e3f74ef3108e3765498eebdbbfae5ee4fb07c68 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 22:23:45 +1000 Subject: [PATCH 03/25] Pass list of selected stock items to the view --- InvenTree/static/script/inventree/stock.js | 19 +++++++++++++++++++ InvenTree/stock/views.py | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 800b23bbb6..4788f2c1df 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -550,6 +550,24 @@ function loadStockTable(table, options) { $("#multi-item-move").click(function() { + var items = $('#stock-table').bootstrapTable('getSelections'); + + var stock = []; + + items.forEach(function(item) { + stock.push(item.pk); + }); + + launchModalForm("/stock/move/", + { + data: { + stock: stock, + }, + } + ); + + /* + var items = $("#stock-table").bootstrapTable('getSelections'); moveStockItems(items, @@ -560,6 +578,7 @@ function loadStockTable(table, options) { }); return false; + */ }); } diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 4d0a2c8af2..990287202d 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -132,9 +132,27 @@ class StockItemMoveMultiple(AjaxView, FormMixin): ajax_form_title = 'Move Stock' form_class = MoveStockItemForm + def get_items(self, item_list): + """ Return list of stock items. """ + + items = [] + + for pk in item_list: + try: + items.append(StockItem.objects.get(pk=pk)) + except StockItem.DoesNotExist: + pass + + return items def get(self, request, *args, **kwargs): + item_list = request.GET.getlist('stock[]') + + items = self.get_items(item_list) + + print(items) + return self.renderJsonResponse(request, self.form_class()) def post(self, request, *args, **kwargs): From d3219470265f69b9a74363facfe0ab4b23a4b530 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 22:38:42 +1000 Subject: [PATCH 04/25] Add stock item fields for each item passed to the form --- InvenTree/stock/forms.py | 5 +++++ InvenTree/stock/views.py | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 6614d67d72..5f22602a39 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -61,10 +61,15 @@ class MoveStockItemForm(forms.ModelForm): confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items') def __init__(self, *args, **kwargs): + stock_items = kwargs.pop('stock_items', []) + super(MoveStockItemForm, self).__init__(*args, **kwargs) self.fields['location'].choices = self.get_location_choices() + for item in stock_items: + self.fields['stock_item_{id}'.format(id=item.id)] = forms.IntegerField(required=True, initial=item.quantity, help_text='Quantity for ' + str(item)) + class Meta: model = StockItem diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 990287202d..44593f3c42 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -131,6 +131,7 @@ class StockItemMoveMultiple(AjaxView, FormMixin): ajax_template_name = 'stock/stock_move.html' ajax_form_title = 'Move Stock' form_class = MoveStockItemForm + items = [] def get_items(self, item_list): """ Return list of stock items. """ @@ -145,20 +146,27 @@ class StockItemMoveMultiple(AjaxView, FormMixin): return items + def get_form_kwargs(self): + + args = super().get_form_kwargs() + + args['stock_items'] = self.get_items(self.items) + + return args + def get(self, request, *args, **kwargs): - item_list = request.GET.getlist('stock[]') - - items = self.get_items(item_list) + # Save list of items! + self.items = request.GET.getlist('stock[]') - print(items) - - return self.renderJsonResponse(request, self.form_class()) + return self.renderJsonResponse(request, self.get_form()) def post(self, request, *args, **kwargs): form = self.get_form() + print(request.POST) + valid = form.is_valid() print("Valid:", valid) From 56821abd09fd266aa796b5b0042263d4038ab5a8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 23:13:26 +1000 Subject: [PATCH 05/25] CSS tweaks for modals --- InvenTree/static/css/inventree.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 0769380fe1..0554138c24 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 { From 3869bc27c9ae1a771d8140a3a5b5d18302afdd79 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 30 May 2019 09:01:16 +1000 Subject: [PATCH 06/25] Whoops, that form was being used. Created a copy of the form for multiple-item-stock-movements --- InvenTree/InvenTree/models.py | 2 +- InvenTree/stock/forms.py | 36 +++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index a7fc93bf3f..948163555c 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -198,7 +198,7 @@ class InvenTreeTree(models.Model): def __str__(self): """ String representation of a category is the full path to that category """ - return self.pathstring + return "{path} - {desc}".format(path=self.pathstring, desc=self.description) @receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log') diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 5f22602a39..5ee74e40e3 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -42,9 +42,21 @@ class CreateStockItemForm(HelperForm): ] -class MoveStockItemForm(forms.ModelForm): +class MoveStockItemForm(HelperForm): """ Form for moving a StockItem to a new location """ + note = forms.CharField(label='Notes', required=True, help_text='Add note (required)') + + class Meta: + model = StockItem + + fields = [ + 'location', + 'note' + ] + +class MoveMultipleStockItemsForm(forms.ModelForm): + def get_location_choices(self): locs = StockLocation.objects.all() @@ -68,7 +80,27 @@ class MoveStockItemForm(forms.ModelForm): self.fields['location'].choices = self.get_location_choices() for item in stock_items: - self.fields['stock_item_{id}'.format(id=item.id)] = forms.IntegerField(required=True, initial=item.quantity, help_text='Quantity for ' + str(item)) + field = forms.IntegerField( + label= str(item.part), + required=True, + initial=item.quantity, + help_text='Quantity for ' + str(item)) + + extra = { + 'name': str(item.part), + 'location': str(item.location), + 'quantity': str(item.quantity), + } + + field.extra = extra + + self.fields['stock_item_{id}'.format(id=item.id)] = field + + def get_stock_fields(self): + for field_name in self.fields: + if field_name.startswith('stock_item_'): + print(field_name, self[field_name], self[field_name].extra) + yield self[field_name] class Meta: model = StockItem From 8214aef0db3a156debfec2c97c33f796a4eb4934 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 1 Jun 2019 21:13:51 +1000 Subject: [PATCH 07/25] Add template for hover_image --- InvenTree/build/templates/build/allocation_item.html | 5 +---- InvenTree/build/templates/build/auto_allocate.html | 5 +---- InvenTree/build/templates/build/build_base.html | 2 +- InvenTree/build/templates/build/complete.html | 10 ++-------- InvenTree/build/templates/build/required.html | 5 +---- InvenTree/part/templates/part/variants.html | 7 +------ InvenTree/templates/hover_image.html | 12 ++++++++++++ 7 files changed, 19 insertions(+), 27 deletions(-) create mode 100644 InvenTree/templates/hover_image.html 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/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 From 6a04c8cbd3f29216a499d756c5d59b7bc43787de Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 1 Jun 2019 21:16:59 +1000 Subject: [PATCH 08/25] Update stock_move form template --- InvenTree/stock/forms.py | 29 ++-------------- .../stock/templates/stock/stock_move.html | 34 ++++++++++++++++++- InvenTree/stock/views.py | 20 ++++++++--- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 5ee74e40e3..c6563f1786 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -73,34 +73,9 @@ class MoveMultipleStockItemsForm(forms.ModelForm): confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items') def __init__(self, *args, **kwargs): - stock_items = kwargs.pop('stock_items', []) + super().__init__(*args, **kwargs) - super(MoveStockItemForm, self).__init__(*args, **kwargs) - - self.fields['location'].choices = self.get_location_choices() - - for item in stock_items: - field = forms.IntegerField( - label= str(item.part), - required=True, - initial=item.quantity, - help_text='Quantity for ' + str(item)) - - extra = { - 'name': str(item.part), - 'location': str(item.location), - 'quantity': str(item.quantity), - } - - field.extra = extra - - self.fields['stock_item_{id}'.format(id=item.id)] = field - - def get_stock_fields(self): - for field_name in self.fields: - if field_name.startswith('stock_item_'): - print(field_name, self[field_name], self[field_name].extra) - yield self[field_name] + self.fields['location'].choices = self.get_location_choices() class Meta: model = StockItem diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html index c7de8c74b2..7830f18b10 100644 --- a/InvenTree/stock/templates/stock/stock_move.html +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -1 +1,33 @@ -{% extends "modal_form.html" %} \ No newline at end of file +{% block pre_form_content %} +{% endblock %} +
+ {% csrf_token %} + {% load crispy_forms_tags %} + + + {% crispy form %} + + {% block form_data %} + + {% endblock %} + +
+ + + + + + + {% for item in stock_items %} + + + + + + {% endfor %} +
ItemLocation{{ stock_action }}
{% include "hover_image.html" with image=item.part.image %} + {{ item.part.full_name }} {{ item.part.description }}{{ item.location.pathstring }} + +
+ +
\ No newline at end of file diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 44593f3c42..bdf75c0bd5 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -23,6 +23,7 @@ from .forms import EditStockItemForm from .forms import MoveStockItemForm from .forms import StocktakeForm from .forms import MoveStockItemForm +from .forms import MoveMultipleStockItemsForm class StockIndex(ListView): @@ -130,8 +131,8 @@ class StockItemMoveMultiple(AjaxView, FormMixin): ajax_template_name = 'stock/stock_move.html' ajax_form_title = 'Move Stock' - form_class = MoveStockItemForm - items = [] + form_class = MoveMultipleStockItemsForm + stock_items = [] def get_items(self, item_list): """ Return list of stock items. """ @@ -146,18 +147,27 @@ class StockItemMoveMultiple(AjaxView, FormMixin): return items - def get_form_kwargs(self): + def _get_form_kwargs(self): args = super().get_form_kwargs() - args['stock_items'] = self.get_items(self.items) + #args['stock_items'] = self.stock_items return args + def get_context_data(self): + + context = super().get_context_data() + + context['stock_items'] = self.stock_items + context['stock_action'] = 'Move' + + return context + def get(self, request, *args, **kwargs): # Save list of items! - self.items = request.GET.getlist('stock[]') + self.stock_items = self.get_items(request.GET.getlist('stock[]')) return self.renderJsonResponse(request, self.get_form()) From 011f5a5efd01118b9fd184bdc0eb513a869f2f86 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 1 Jun 2019 22:04:03 +1000 Subject: [PATCH 09/25] Delete rows interactively from stock adjustment form --- InvenTree/static/css/inventree.css | 7 +++ InvenTree/static/script/inventree/stock.js | 14 ++++++ InvenTree/stock/forms.py | 4 +- .../stock/templates/stock/stock_move.html | 45 +++++++++---------- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 0554138c24..da7d38d250 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -282,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 4788f2c1df..cc2d57098c 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -14,6 +14,20 @@ function getStockLocations(filters={}, options={}) { return inventreeGet('/api/stock/location/', filters, options) } +/* Functions for interacting with stock management forms + */ + +function removeStockRow(e) { + // Remove a selected row from a stock modal form + + e = e || window.event; + var src = e.target || e.srcElement; + + var row = $(src).attr('row'); + + $('#' + row).remove(); +} + /* Present user with a dialog to update multiple stock items * Possible actions: diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index c6563f1786..10e1aeac0c 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -69,7 +69,7 @@ class MoveMultipleStockItemsForm(forms.ModelForm): location = 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): @@ -83,7 +83,7 @@ class MoveMultipleStockItemsForm(forms.ModelForm): fields = [ 'location', 'note', - 'transaction', + # 'transaction', 'confirm', ] diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html index 7830f18b10..3bd4a65420 100644 --- a/InvenTree/stock/templates/stock/stock_move.html +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -1,33 +1,32 @@ {% block pre_form_content %} + {% endblock %} +
{% csrf_token %} {% load crispy_forms_tags %} + Stock Items + + + + + + + + {% for item in stock_items %} + + + + + + + {% endfor %} +
ItemLocation{{ stock_action }}
{% include "hover_image.html" with image=item.part.image %} + {{ item.part.full_name }} {{ item.part.description }}{{ item.location.pathstring }} + +
{% crispy form %} - {% block form_data %} - - {% endblock %} - -
- - - - - - - {% for item in stock_items %} - - - - - - {% endfor %} -
ItemLocation{{ stock_action }}
{% include "hover_image.html" with image=item.part.image %} - {{ item.part.full_name }} {{ item.part.description }}{{ item.location.pathstring }} - -
-
\ No newline at end of file From e278bdbb903a9ed8f473602fc7d981e0db9a12f1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 09:14:45 +1000 Subject: [PATCH 10/25] Improve filtering for stock items --- InvenTree/stock/templates/stock/stock_move.html | 3 +-- InvenTree/stock/views.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html index 3bd4a65420..0aa2ddb9bb 100644 --- a/InvenTree/stock/templates/stock/stock_move.html +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -6,10 +6,9 @@ {% csrf_token %} {% load crispy_forms_tags %} - Stock Items - + diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index bdf75c0bd5..fdecc6c563 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -135,15 +135,14 @@ class StockItemMoveMultiple(AjaxView, FormMixin): stock_items = [] def get_items(self, item_list): - """ Return list of stock items. """ + """ 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[]')) return items @@ -166,6 +165,8 @@ class StockItemMoveMultiple(AjaxView, FormMixin): def get(self, request, *args, **kwargs): + self.request = request + # Save list of items! self.stock_items = self.get_items(request.GET.getlist('stock[]')) From c228a4998cfdcdcdf3b46d557c7bd3913f188865 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 10:25:19 +1000 Subject: [PATCH 11/25] Extract stock items from POST and return to form --- .../stock/templates/stock/stock_move.html | 2 +- InvenTree/stock/views.py | 67 +++++++++++++++++-- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html index 0aa2ddb9bb..427ff2f058 100644 --- a/InvenTree/stock/templates/stock/stock_move.html +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -19,7 +19,7 @@ {{ item.part.full_name }} {{ item.part.description }} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index fdecc6c563..60b0f3ab47 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -14,6 +14,8 @@ 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 @@ -134,7 +136,7 @@ class StockItemMoveMultiple(AjaxView, FormMixin): form_class = MoveMultipleStockItemsForm stock_items = [] - def get_items(self, item_list): + def get_GET_items(self): """ Return list of stock items initally requested using GET """ # Start with all 'in stock' items @@ -144,8 +146,48 @@ class StockItemMoveMultiple(AjaxView, FormMixin): 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: + 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_form_kwargs(self): args = super().get_form_kwargs() @@ -168,7 +210,7 @@ class StockItemMoveMultiple(AjaxView, FormMixin): self.request = request # Save list of items! - self.stock_items = self.get_items(request.GET.getlist('stock[]')) + self.stock_items = self.get_GET_items() return self.renderJsonResponse(request, self.get_form()) @@ -176,18 +218,31 @@ class StockItemMoveMultiple(AjaxView, FormMixin): form = self.get_form() - print(request.POST) + self.request = request + + # Update list of stock items + self.stock_items = self.get_POST_items() valid = form.is_valid() + + for item in self.stock_items: + try: + q = int(item.new_quantity) + except ValueError: + item.error = 'Must enter integer value' + valid = False + continue - print("Valid:", valid) + confirmed = str2bool(request.POST.get('confirm')) + + if not confirmed: + valid = False + form.errors['confirm'] = ['Confirm stock adjustment'] data = { 'form_valid': False, } - #form.errors['note'] = ['hello world'] - return self.renderJsonResponse(request, form, data=data) From 20963f83d913e3a9b5b76279fb1a9987aec9b7df Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 11:00:28 +1000 Subject: [PATCH 12/25] Better error reporting for quantity --- InvenTree/stock/forms.py | 6 +++--- InvenTree/stock/templates/stock/stock_move.html | 3 +++ InvenTree/stock/views.py | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 10e1aeac0c..2a4e15d3c6 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -67,7 +67,7 @@ class MoveMultipleStockItemsForm(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') confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items') @@ -75,13 +75,13 @@ class MoveMultipleStockItemsForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['location'].choices = self.get_location_choices() + self.fields['destination'].choices = self.get_location_choices() class Meta: model = StockItem fields = [ - 'location', + 'destination', 'note', # 'transaction', 'confirm', diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html index 427ff2f058..6a35701557 100644 --- a/InvenTree/stock/templates/stock/stock_move.html +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -20,6 +20,9 @@ diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 60b0f3ab47..6101a4dce9 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -10,6 +10,8 @@ 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 @@ -229,7 +231,17 @@ class StockItemMoveMultiple(AjaxView, FormMixin): try: q = int(item.new_quantity) except ValueError: - item.error = 'Must enter integer value' + item.error = _('Must enter integer value') + valid = False + continue + + if q < 0: + item.error = _('Quantity must be positive') + valid = False + continue + + if q > item.quantity: + item.error = _('Quantity must not exceed {x}'.format(x=item.quantity)) valid = False continue @@ -237,7 +249,7 @@ class StockItemMoveMultiple(AjaxView, FormMixin): if not confirmed: valid = False - form.errors['confirm'] = ['Confirm stock adjustment'] + form.errors['confirm'] = [_('Confirm stock adjustment')] data = { 'form_valid': False, From 96f6f6068e8f469279af18b008b54cd73b8c54e6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 11:05:40 +1000 Subject: [PATCH 13/25] Return message on succses --- InvenTree/stock/views.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 6101a4dce9..b233f53902 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -251,8 +251,18 @@ class StockItemMoveMultiple(AjaxView, FormMixin): valid = False form.errors['confirm'] = [_('Confirm stock adjustment')] + try: + destination = StockLocation.objects.get(id=form['destination'].value()) + destination_name = destination.pathstring + except StockLocation.DoesNotExist: + destination = None + destination_name = "" + data = { - 'form_valid': False, + 'form_valid': valid, + 'success': _('Moved {n} items to {loc}'.format( + n=len(self.stock_items), + loc=destination_name)) } return self.renderJsonResponse(request, form, data=data) From 1b7762470d6d94b915c70efb561a7bc753b1becd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 11:14:56 +1000 Subject: [PATCH 14/25] catch a ValueError --- InvenTree/stock/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index b233f53902..3114a8536d 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -251,12 +251,16 @@ class StockItemMoveMultiple(AjaxView, FormMixin): valid = False form.errors['confirm'] = [_('Confirm stock adjustment')] + destination = None + destination_name = "" + try: destination = StockLocation.objects.get(id=form['destination'].value()) destination_name = destination.pathstring except StockLocation.DoesNotExist: - destination = None - destination_name = "" + pass + except ValueError: + pass data = { 'form_valid': valid, From 0b54baf6db3bc2982605df00721cf4027d4acdbb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 11:15:12 +1000 Subject: [PATCH 15/25] POST the stock adjustment method back as a hidden form input --- InvenTree/stock/templates/stock/stock_move.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html index 6a35701557..d23317bd35 100644 --- a/InvenTree/stock/templates/stock/stock_move.html +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -5,6 +5,8 @@
{% csrf_token %} {% load crispy_forms_tags %} + +
ItemStock Item Location {{ stock_action }} {{ item.location.pathstring }} - +
{{ item.location.pathstring }} + {% if item.error %} +
{{ item.error }} + {% endif %}
From fe312050174f4177065e49bc66df39738634e3f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 11:27:03 +1000 Subject: [PATCH 16/25] Stock movement now works! --- InvenTree/stock/views.py | 64 +++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 3114a8536d..d03fed4e37 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -229,18 +229,18 @@ class StockItemMoveMultiple(AjaxView, FormMixin): for item in self.stock_items: try: - q = int(item.new_quantity) + item.new_quantity = int(item.new_quantity) except ValueError: item.error = _('Must enter integer value') valid = False continue - if q < 0: + if item.new_quantity < 0: item.error = _('Quantity must be positive') valid = False continue - if q > item.quantity: + if item.new_quantity > item.quantity: item.error = _('Quantity must not exceed {x}'.format(x=item.quantity)) valid = False continue @@ -251,27 +251,57 @@ class StockItemMoveMultiple(AjaxView, FormMixin): valid = False form.errors['confirm'] = [_('Confirm stock adjustment')] - destination = None - destination_name = "" - - try: - destination = StockLocation.objects.get(id=form['destination'].value()) - destination_name = destination.pathstring - except StockLocation.DoesNotExist: - pass - except ValueError: - pass - data = { 'form_valid': valid, - 'success': _('Moved {n} items to {loc}'.format( - n=len(self.stock_items), - loc=destination_name)) } + if valid: + action = request.POST.get('stock_action').lower() + + if action == 'move': + + destination = None + + try: + destination = StockLocation.objects.get(id=form['destination'].value()) + except StockLocation.DoesNotExist: + pass + except ValueError: + pass + + if destination: + data['success'] = self.do_move(destination) + return self.renderJsonResponse(request, form, data=data) + 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): """ From c7503b4f9f534c1ff3f810d445f9f983f47568c7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 11:37:04 +1000 Subject: [PATCH 17/25] Consolidated form/view into single 'stock-adjust' endpoint --- InvenTree/static/script/inventree/stock.js | 6 +++- InvenTree/stock/forms.py | 2 +- .../{stock_move.html => stock_adjust.html} | 0 InvenTree/stock/urls.py | 2 +- InvenTree/stock/views.py | 32 +++++++++++++------ 5 files changed, 29 insertions(+), 13 deletions(-) rename InvenTree/stock/templates/stock/{stock_move.html => stock_adjust.html} (100%) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index cc2d57098c..e18a1a6448 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -572,11 +572,15 @@ function loadStockTable(table, options) { stock.push(item.pk); }); - launchModalForm("/stock/move/", + launchModalForm("/stock/adjust/", { data: { + action: 'move', stock: stock, }, + success: function() { + $("#stock-table").bootstrapTable('refresh'); + }, } ); diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 2a4e15d3c6..6304ba2e60 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -55,7 +55,7 @@ class MoveStockItemForm(HelperForm): 'note' ] -class MoveMultipleStockItemsForm(forms.ModelForm): +class AdjustStockForm(forms.ModelForm): def get_location_choices(self): locs = StockLocation.objects.all() diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_adjust.html similarity index 100% rename from InvenTree/stock/templates/stock/stock_move.html rename to InvenTree/stock/templates/stock/stock_adjust.html diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 237e2281f0..e81060ae3b 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -36,7 +36,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\d+)/', include(stock_item_detail_urls)), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index d03fed4e37..98dc09381c 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -27,7 +27,7 @@ from .forms import EditStockItemForm from .forms import MoveStockItemForm from .forms import StocktakeForm from .forms import MoveStockItemForm -from .forms import MoveMultipleStockItemsForm +from .forms import AdjustStockForm class StockIndex(ListView): @@ -130,12 +130,19 @@ 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 = MoveMultipleStockItemsForm + ajax_template_name = 'stock/stock_adjust.html' + ajax_form_title = 'Adjust Stock' + form_class = AdjustStockForm stock_items = [] def get_GET_items(self): @@ -203,7 +210,8 @@ class StockItemMoveMultiple(AjaxView, FormMixin): context = super().get_context_data() context['stock_items'] = self.stock_items - context['stock_action'] = 'Move' + + context['stock_action'] = self.stock_action return context @@ -211,6 +219,9 @@ class StockItemMoveMultiple(AjaxView, FormMixin): self.request = request + # Action + self.stock_action = request.GET.get('action').lower() + # Save list of items! self.stock_items = self.get_GET_items() @@ -255,10 +266,11 @@ class StockItemMoveMultiple(AjaxView, FormMixin): 'form_valid': valid, } - if valid: - action = request.POST.get('stock_action').lower() + self.stock_action = request.POST.get('stock_action').lower() - if action == 'move': + if valid: + + if self.stock_action == 'move': destination = None From d1ff115f74c1e179b8df71e92994bfbe4b6c3a58 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 11:47:21 +1000 Subject: [PATCH 18/25] Simplifiy js and improve template / form --- InvenTree/static/script/inventree/stock.js | 57 ++++++++----------- .../stock/templates/stock/stock_adjust.html | 2 +- InvenTree/stock/views.py | 39 +++++++++---- 3 files changed, 51 insertions(+), 47 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index e18a1a6448..e38940ed40 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -540,6 +540,28 @@ 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({ @@ -563,40 +585,7 @@ function loadStockTable(table, options) { }); $("#multi-item-move").click(function() { - - var items = $('#stock-table').bootstrapTable('getSelections'); - - var stock = []; - - items.forEach(function(item) { - stock.push(item.pk); - }); - - launchModalForm("/stock/adjust/", - { - data: { - action: 'move', - stock: stock, - }, - success: function() { - $("#stock-table").bootstrapTable('refresh'); - }, - } - ); - - /* - - var items = $("#stock-table").bootstrapTable('getSelections'); - - moveStockItems(items, - { - success: function() { - $("#stock-table").bootstrapTable('refresh'); - } - }); - - return false; - */ + stockAdjustment('move'); }); } diff --git a/InvenTree/stock/templates/stock/stock_adjust.html b/InvenTree/stock/templates/stock/stock_adjust.html index d23317bd35..62e7412473 100644 --- a/InvenTree/stock/templates/stock/stock_adjust.html +++ b/InvenTree/stock/templates/stock/stock_adjust.html @@ -12,7 +12,7 @@ - + {% for item in stock_items %} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 98dc09381c..eb744ee443 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -172,7 +172,13 @@ class StockAdjust(AjaxView, FormMixin): items = None for item in items: - item.new_quantity = item.quantity + + # Initialize quantity to zero for addition/removal + if self.stock_action in ['take', 'give']: + item.new_quantity = 0 + # Initialize quantity at full amount for counting or moving + else: + item.new_quantity = item.quantity return items @@ -197,13 +203,6 @@ class StockAdjust(AjaxView, FormMixin): items.append(stock_item) return items - def _get_form_kwargs(self): - - args = super().get_form_kwargs() - - #args['stock_items'] = self.stock_items - - return args def get_context_data(self): @@ -211,7 +210,9 @@ class StockAdjust(AjaxView, FormMixin): context['stock_items'] = self.stock_items - context['stock_action'] = self.stock_action + context['stock_action'] = self.stock_action + + context['stock_action_title'] = self.stock_action.capitalize() return context @@ -220,8 +221,22 @@ class StockAdjust(AjaxView, FormMixin): self.request = request # Action - self.stock_action = request.GET.get('action').lower() + self.stock_action = request.GET.get('action', '').lower() + # Pick a default action... + if self.stock_action not in ['move', 'count', 'take', 'give']: + self.stock_action = 'count' + + # Choose the form title based on the action + titles = { + 'move': 'Move Stock', + 'count': 'Count Stock', + 'take': 'Remove Stock', + 'give': 'Add Stock' + } + + self.ajax_form_title = titles[self.stock_action] + # Save list of items! self.stock_items = self.get_GET_items() @@ -229,13 +244,13 @@ class StockAdjust(AjaxView, FormMixin): def post(self, request, *args, **kwargs): - form = self.get_form() - self.request = request # 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: From 8dd903456359c8f1da40fdef2960833525125fe9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 12:00:39 +1000 Subject: [PATCH 19/25] Remove 'destination' field if we are not moving stock - Allow different stock adjustment actions --- InvenTree/static/script/inventree/stock.js | 5 +- .../stock/templates/stock/stock_adjust.html | 5 +- InvenTree/stock/views.py | 101 ++++++++++++++---- 3 files changed, 83 insertions(+), 28 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index e38940ed40..4ddcba214e 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -578,10 +578,7 @@ function loadStockTable(table, options) { }); $('#multi-item-add').click(function() { - updateStockItems({ - action: 'add', - }); - return false; + stockAdjustment('add'); }); $("#multi-item-move").click(function() { diff --git a/InvenTree/stock/templates/stock/stock_adjust.html b/InvenTree/stock/templates/stock/stock_adjust.html index 62e7412473..0e6e2c4697 100644 --- a/InvenTree/stock/templates/stock/stock_adjust.html +++ b/InvenTree/stock/templates/stock/stock_adjust.html @@ -21,7 +21,10 @@ {{ item.part.full_name }} {{ item.part.description }}
Stock Item Location{{ stock_action }}{{ stock_action_title }}
{{ item.location.pathstring }} - + {% if item.error %}
{{ item.error }} {% endif %} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index eb744ee443..aca89875fd 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -174,7 +174,7 @@ class StockAdjust(AjaxView, FormMixin): for item in items: # Initialize quantity to zero for addition/removal - if self.stock_action in ['take', 'give']: + if self.stock_action in ['take', 'add']: item.new_quantity = 0 # Initialize quantity at full amount for counting or moving else: @@ -216,6 +216,15 @@ class StockAdjust(AjaxView, FormMixin): 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 @@ -224,7 +233,7 @@ class StockAdjust(AjaxView, FormMixin): self.stock_action = request.GET.get('action', '').lower() # Pick a default action... - if self.stock_action not in ['move', 'count', 'take', 'give']: + if self.stock_action not in ['move', 'count', 'take', 'add']: self.stock_action = 'count' # Choose the form title based on the action @@ -232,7 +241,7 @@ class StockAdjust(AjaxView, FormMixin): 'move': 'Move Stock', 'count': 'Count Stock', 'take': 'Remove Stock', - 'give': 'Add Stock' + 'add': 'Add Stock' } self.ajax_form_title = titles[self.stock_action] @@ -246,6 +255,8 @@ class StockAdjust(AjaxView, FormMixin): self.request = request + self.stock_action = request.POST.get('stock_action').lower() + # Update list of stock items self.stock_items = self.get_POST_items() @@ -265,11 +276,13 @@ class StockAdjust(AjaxView, FormMixin): item.error = _('Quantity must be positive') valid = False continue - - if item.new_quantity > item.quantity: - item.error = _('Quantity must not exceed {x}'.format(x=item.quantity)) - valid = False - continue + + if self.stock_action in ['move']: + + 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')) @@ -281,26 +294,68 @@ class StockAdjust(AjaxView, FormMixin): 'form_valid': valid, } - self.stock_action = request.POST.get('stock_action').lower() - if valid: - if self.stock_action == 'move': - - destination = None - - try: - destination = StockLocation.objects.get(id=form['destination'].value()) - except StockLocation.DoesNotExist: - pass - except ValueError: - pass - - if destination: - data['success'] = self.do_move(destination) + 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 + + 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 + + count += 1 + + return _("Removed stock from {n} items".format(n=count)) + + def do_count(self): + pass + def do_move(self, destination): """ Perform actual stock movement """ From 1b3ffada6dbc3032b453a0ddf28264064caffd52 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 12:13:22 +1000 Subject: [PATCH 20/25] Add-stock is now working --- InvenTree/stock/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index aca89875fd..2c83545843 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -336,6 +336,8 @@ class StockAdjust(AjaxView, FormMixin): 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)) From d365d7cc44869f8d2effb62d7f9b909d3cda0374 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 12:15:44 +1000 Subject: [PATCH 21/25] remove from stock now works --- InvenTree/static/script/inventree/stock.js | 5 +---- InvenTree/stock/templates/stock/stock_adjust.html | 2 +- InvenTree/stock/views.py | 4 +++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 4ddcba214e..0f2ee43136 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -571,10 +571,7 @@ function loadStockTable(table, options) { }); $('#multi-item-remove').click(function() { - updateStockItems({ - action: 'remove', - }); - return false; + stockAdjustment('take'); }); $('#multi-item-add').click(function() { diff --git a/InvenTree/stock/templates/stock/stock_adjust.html b/InvenTree/stock/templates/stock/stock_adjust.html index 0e6e2c4697..9bd4203725 100644 --- a/InvenTree/stock/templates/stock/stock_adjust.html +++ b/InvenTree/stock/templates/stock/stock_adjust.html @@ -23,7 +23,7 @@
{% if item.error %}
{{ item.error }} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 2c83545843..a5662e8d6e 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -277,7 +277,7 @@ class StockAdjust(AjaxView, FormMixin): valid = False continue - if self.stock_action in ['move']: + if self.stock_action in ['move', 'take']: if item.new_quantity > item.quantity: item.error = _('Quantity must not exceed {x}'.format(x=item.quantity)) @@ -351,6 +351,8 @@ class StockAdjust(AjaxView, FormMixin): 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)) From 2ee35ec062499056775d6a059ac1e5192c1a3464 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 12:20:26 +1000 Subject: [PATCH 22/25] Stock counting now works --- InvenTree/static/script/inventree/stock.js | 5 +---- InvenTree/stock/views.py | 12 +++++++++++- InvenTree/templates/stock_table.html | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 0f2ee43136..84557535ba 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -564,10 +564,7 @@ function loadStockTable(table, options) { // Automatically link button callbacks $('#multi-item-stocktake').click(function() { - updateStockItems({ - action: 'stocktake', - }); - return false; + stockAdjustment('count'); }); $('#multi-item-remove').click(function() { diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index a5662e8d6e..b9981f26de 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -358,7 +358,17 @@ class StockAdjust(AjaxView, FormMixin): return _("Removed stock from {n} items".format(n=count)) def do_count(self): - pass + + 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 """ 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 @@ From 5177b7f836d8133f0417eacfc9bcc9604c09f338 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 12:21:49 +1000 Subject: [PATCH 23/25] Remove some defunct javascript --- InvenTree/static/script/inventree/stock.js | 379 +-------------------- 1 file changed, 11 insertions(+), 368 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 84557535ba..65b0c55da5 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -28,377 +28,20 @@ function removeStockRow(e) { $('#' + row).remove(); } - -/* 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 += "\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); - } - }); -} - 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, From 802ff35cf354e516ece90785e2b7b711b93a5da1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 2 Jun 2019 12:45:44 +1000 Subject: [PATCH 24/25] Remove some now unused forms / views / etc --- InvenTree/stock/api.py | 6 +- InvenTree/stock/forms.py | 32 +++------- InvenTree/stock/templates/stock/item.html | 41 ++++++------- InvenTree/stock/urls.py | 2 - InvenTree/stock/views.py | 73 ----------------------- 5 files changed, 30 insertions(+), 124 deletions(-) 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 6304ba2e60..ef55356961 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -42,20 +42,16 @@ class CreateStockItemForm(HelperForm): ] -class MoveStockItemForm(HelperForm): - """ Form for moving a StockItem to a new location """ - - note = forms.CharField(label='Notes', required=True, help_text='Add note (required)') - - class Meta: - model = StockItem - - fields = [ - 'location', - 'note' - ] - 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() @@ -88,16 +84,6 @@ class AdjustStockForm(forms.ModelForm): ] -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. 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 @@