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 %} +
+ {% csrf_token %} + {% load crispy_forms_tags %} + +
+ +
+ + {% if category %} +

Select Part Category

+ {% else %} +

Select Part Category

+ {% endif %} +
+
+ + +

Set category for the following parts

+ + + + + + + + {% for part in parts %} + + + + + + + + {% endfor %} +
PartDescriptionCategory +
+ {% include "hover_image.html" with image=part.image hover=False %} + {{ part.full_name }} + + {{ part.description }} + + {{ part.category.pathstring }} + + +
+ +
+{% 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)