diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 6a6ba6440c..48e8dc7906 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -251,6 +251,23 @@ class Part(models.Model):
return ' | '.join(elements)
+ def set_category(self, category):
+
+ if not type(category) == PartCategory:
+ raise ValidationError({
+ 'category': _('Invalid object supplied to part.set_category')
+ })
+
+ try:
+ # Already in this category!
+ if category == self.category:
+ return
+ except PartCategory.DoesNotExist:
+ pass
+
+ self.category = category
+ self.save()
+
def get_absolute_url(self):
""" Return the web URL for viewing this part """
return reverse('part-detail', kwargs={'pk': self.id})
diff --git a/InvenTree/part/templates/part/set_category.html b/InvenTree/part/templates/part/set_category.html
new file mode 100644
index 0000000000..b71506d958
--- /dev/null
+++ b/InvenTree/part/templates/part/set_category.html
@@ -0,0 +1,58 @@
+{% extends "modal_form.html" %}
+
+{% block form %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index e8a0175503..7dc53372cd 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -82,6 +82,9 @@ part_urls = [
# Part attachments
url(r'^attachment/', include(part_attachment_urls)),
+ # Change category for multiple parts
+ url(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'),
+
# Bom Items
url(r'^bom/(?P\d+)/', include(part_bom_urls)),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 135a216218..83989b1dde 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -6,7 +6,7 @@ Django views for interacting with Part app
from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
-
+from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy
from django.views.generic import DetailView, ListView
from django.forms.models import model_to_dict
@@ -125,6 +125,77 @@ class PartAttachmentDelete(AjaxDeleteView):
}
+class PartSetCategory(AjaxView):
+ """ View for settings the part category for multiple parts at once """
+
+ ajax_template_name = 'part/set_category.html'
+ ajax_form_title = 'Set Part Category'
+
+ category = None
+ parts = []
+
+ def get(self, request, *args, **kwargs):
+ """ Respond to a GET request to this view """
+
+ self.request = request
+
+ if 'parts[]' in request.GET:
+ self.parts = Part.objects.filter(id__in=request.GET.getlist('parts[]'))
+ else:
+ self.parts = []
+
+ return self.renderJsonResponse(request, context=self.get_context_data())
+
+ def post(self, request, *args, **kwargs):
+ """ Respond to a POST request to this view """
+
+ self.parts = []
+
+ for item in request.POST:
+ if item.startswith('part_id_'):
+ pk = item.replace('part_id_', '')
+
+ try:
+ part = Part.objects.get(pk=pk)
+ except (Part.DoesNotExist, ValueError):
+ continue
+
+ self.parts.append(part)
+
+ self.category = None
+
+ if 'part_category' in request.POST:
+ pk = request.POST['part_category']
+
+ try:
+ self.category = PartCategory.objects.get(pk=pk)
+ except (PartCategory.DoesNotExist, ValueError):
+ self.category = None
+
+ valid = self.category is not None
+
+ data = {
+ 'form_valid': valid,
+ 'success': _('Set category for {n} parts'.format(n=len(self.parts)))
+ }
+
+ if valid:
+ for part in self.parts:
+ part.set_category(self.category)
+
+ return self.renderJsonResponse(request, data=data, context=self.get_context_data())
+
+ def get_context_data(self):
+ """ Return context data for rendering in the form """
+ ctx = {}
+
+ ctx['parts'] = self.parts
+ ctx['categories'] = PartCategory.objects.all()
+ ctx['category'] = self.category
+
+ return ctx
+
+
class MakePartVariant(AjaxCreateView):
""" View for creating a new variant based on an existing template Part
diff --git a/InvenTree/static/script/inventree/modals.js b/InvenTree/static/script/inventree/modals.js
index 31c2233f8d..24a1a38ed3 100644
--- a/InvenTree/static/script/inventree/modals.js
+++ b/InvenTree/static/script/inventree/modals.js
@@ -195,6 +195,18 @@ function modalSubmit(modal, callback) {
}
+function removeRowFromModalForm(e) {
+ /* Remove a row from a table in a modal form */
+ e = e || window.event;
+
+ var src = e.target || e.srcElement;
+
+ var row = $(src).attr('row');
+
+ $('#' + row).remove();
+}
+
+
function renderErrorMessage(xhr) {
var html = '' + xhr.statusText + '
';
diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js
index e8ae2662ed..71f78855f7 100644
--- a/InvenTree/static/script/inventree/part.js
+++ b/InvenTree/static/script/inventree/part.js
@@ -224,4 +224,21 @@ function loadPartTable(table, url, options={}) {
},
});
});
+
+ $("#multi-part-category").click(function() {
+ var selections = $(table).bootstrapTable("getSelections");
+
+ var parts = [];
+
+ selections.forEach(function(item) {
+ parts.push(item.pk);
+ });
+
+ launchModalForm("/part/set-category/", {
+ data: {
+ parts: parts,
+ },
+ reload: true,
+ });
+ });
}
\ No newline at end of file
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index 7187cada4e..d7d74e45de 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -66,7 +66,7 @@ class AdjustStockForm(forms.ModelForm):
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')
+ confirm = forms.BooleanField(required=False, initial=False, label='Confirm stock adjustment', help_text='Confirm movement of stock items')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)