mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Set part category (#3134)
* Refactor function to enable / disable submit button on modal forms * Category selection now just uses the AP * Remove unused forms / views * JS linting fixes * remove outdated unit test
This commit is contained in:
parent
fe8f111a63
commit
2b1d8f5b79
@ -3,15 +3,12 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from mptt.fields import TreeNodeChoiceField
|
|
||||||
|
|
||||||
from common.forms import MatchItemForm
|
from common.forms import MatchItemForm
|
||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.helpers import clean_decimal
|
from InvenTree.helpers import clean_decimal
|
||||||
|
|
||||||
from .models import (Part, PartCategory, PartInternalPriceBreak,
|
from .models import Part, PartInternalPriceBreak, PartSellPriceBreak
|
||||||
PartSellPriceBreak)
|
|
||||||
|
|
||||||
|
|
||||||
class PartImageDownloadForm(HelperForm):
|
class PartImageDownloadForm(HelperForm):
|
||||||
@ -53,12 +50,6 @@ class BomMatchItemForm(MatchItemForm):
|
|||||||
return super().get_special_field(col_guess, row, file_manager)
|
return super().get_special_field(col_guess, row, file_manager)
|
||||||
|
|
||||||
|
|
||||||
class SetPartCategoryForm(forms.Form):
|
|
||||||
"""Form for setting the category of multiple Part objects."""
|
|
||||||
|
|
||||||
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
|
|
||||||
|
|
||||||
|
|
||||||
class PartPriceForm(forms.Form):
|
class PartPriceForm(forms.Form):
|
||||||
"""Simple form for viewing part pricing information."""
|
"""Simple form for viewing part pricing information."""
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@
|
|||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle="dropdown">
|
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle="dropdown">
|
||||||
{% trans "Options" %}
|
<span class='fas fa-tools' title='{% trans "Options" %}'></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class='dropdown-menu'>
|
<ul class='dropdown-menu'>
|
||||||
{% if roles.part.change %}
|
{% if roles.part.change %}
|
||||||
@ -378,7 +378,6 @@
|
|||||||
{% else %}category: "null",
|
{% else %}category: "null",
|
||||||
{% endif %}
|
{% endif %}
|
||||||
},
|
},
|
||||||
buttons: ['#part-options'],
|
|
||||||
checkbox: true,
|
checkbox: true,
|
||||||
gridView: true,
|
gridView: true,
|
||||||
},
|
},
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
{% extends "modal_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block form %}
|
|
||||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
<label class='control-label'>Parts</label>
|
|
||||||
<p class='help-block'>{% trans "Set category for the following parts" %}</p>
|
|
||||||
|
|
||||||
<table class='table table-striped'>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Part" %}</th>
|
|
||||||
<th>{% trans "Description" %}</th>
|
|
||||||
<th>{% trans "Category" %}</th>
|
|
||||||
<th>
|
|
||||||
</tr>
|
|
||||||
{% for part in parts %}
|
|
||||||
<tr id='part_row_{{ part.id }}'>
|
|
||||||
<input type='hidden' name='part_id_{{ part.id }}' value='1'/>
|
|
||||||
<td>
|
|
||||||
{% include "hover_image.html" with image=part.image hover=False %}
|
|
||||||
{{ part.full_name }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ part.description }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ part.category.pathstring }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class='btn btn-outline-secondary btn-remove' onClick='removeRowFromModalForm()' title='{% trans "Remove part" %}' type='button'>
|
|
||||||
<span row='part_row_{{ part.id }}' class='fas fa-trash-alt icon-red'></span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% crispy form %}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
@ -138,23 +138,3 @@ class PartQRTest(PartViewTestCase):
|
|||||||
response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class CategoryTest(PartViewTestCase):
|
|
||||||
"""Tests for PartCategory related views."""
|
|
||||||
|
|
||||||
def test_set_category(self):
|
|
||||||
"""Test that the "SetCategory" view works."""
|
|
||||||
url = reverse('part-set-category')
|
|
||||||
|
|
||||||
response = self.client.get(url, {'parts[]': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'part_id_10': True,
|
|
||||||
'part_id_1': True,
|
|
||||||
'part_category': 5
|
|
||||||
}
|
|
||||||
|
|
||||||
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
@ -56,9 +56,6 @@ part_urls = [
|
|||||||
# Part category
|
# Part category
|
||||||
re_path(r'^category/', include(category_urls)),
|
re_path(r'^category/', include(category_urls)),
|
||||||
|
|
||||||
# Change category for multiple parts
|
|
||||||
re_path(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'),
|
|
||||||
|
|
||||||
# Individual part using IPN as slug
|
# Individual part using IPN as slug
|
||||||
re_path(r'^(?P<slug>[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'),
|
re_path(r'^(?P<slug>[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'),
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ from django.conf import settings
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.db import transaction
|
|
||||||
from django.shortcuts import HttpResponseRedirect, get_object_or_404
|
from django.shortcuts import HttpResponseRedirect, get_object_or_404
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -67,80 +66,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PartSetCategory(AjaxUpdateView):
|
|
||||||
"""View for settings the part category for multiple parts at once."""
|
|
||||||
|
|
||||||
ajax_template_name = 'part/set_category.html'
|
|
||||||
ajax_form_title = _('Set Part Category')
|
|
||||||
form_class = part_forms.SetPartCategoryForm
|
|
||||||
|
|
||||||
role_required = 'part.change'
|
|
||||||
|
|
||||||
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, form=self.get_form(), 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:
|
|
||||||
with transaction.atomic():
|
|
||||||
for part in self.parts:
|
|
||||||
part.category = self.category
|
|
||||||
part.save()
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, data=data, form=self.get_form(), 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 PartImport(FileManagementFormView):
|
class PartImport(FileManagementFormView):
|
||||||
"""Part: Upload file, match to fields and import parts(using multi-Step form)"""
|
"""Part: Upload file, match to fields and import parts(using multi-Step form)"""
|
||||||
permission_required = 'part.add'
|
permission_required = 'part.add'
|
||||||
|
@ -642,7 +642,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
|||||||
addRemoveCallback(opts.modal, `#button-row-remove-${response.pk}`);
|
addRemoveCallback(opts.modal, `#button-row-remove-${response.pk}`);
|
||||||
|
|
||||||
// Re-enable the "submit" button
|
// Re-enable the "submit" button
|
||||||
$(opts.modal).find('#modal-form-submit').prop('disabled', false);
|
enableSubmitButton(opts, true);
|
||||||
|
|
||||||
// Reload the parent BOM table
|
// Reload the parent BOM table
|
||||||
reloadParentTable();
|
reloadParentTable();
|
||||||
|
@ -596,7 +596,7 @@ function constructFormBody(fields, options) {
|
|||||||
|
|
||||||
// Immediately disable the "submit" button,
|
// Immediately disable the "submit" button,
|
||||||
// to prevent the form being submitted multiple times!
|
// to prevent the form being submitted multiple times!
|
||||||
$(options.modal).find('#modal-form-submit').prop('disabled', true);
|
enableSubmitButton(options, false);
|
||||||
|
|
||||||
// Run custom code before normal form submission
|
// Run custom code before normal form submission
|
||||||
if (options.beforeSubmit) {
|
if (options.beforeSubmit) {
|
||||||
@ -639,13 +639,13 @@ function insertConfirmButton(options) {
|
|||||||
$(options.modal).find('#modal-footer-buttons').append(html);
|
$(options.modal).find('#modal-footer-buttons').append(html);
|
||||||
|
|
||||||
// Disable the 'submit' button
|
// Disable the 'submit' button
|
||||||
$(options.modal).find('#modal-form-submit').prop('disabled', true);
|
enableSubmitButton(options, true);
|
||||||
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
$(options.modal).find('#modal-confirm').change(function() {
|
$(options.modal).find('#modal-confirm').change(function() {
|
||||||
var enabled = this.checked;
|
var enabled = this.checked;
|
||||||
|
|
||||||
$(options.modal).find('#modal-form-submit').prop('disabled', !enabled);
|
enableSubmitButton(options, !enabled);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1063,7 +1063,7 @@ function handleFormSuccess(response, options) {
|
|||||||
|
|
||||||
// Reset the status of the "submit" button
|
// Reset the status of the "submit" button
|
||||||
if (options.modal) {
|
if (options.modal) {
|
||||||
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
enableSubmitButton(options, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any error flags from the form
|
// Remove any error flags from the form
|
||||||
@ -1228,7 +1228,7 @@ function handleFormErrors(errors, fields={}, options={}) {
|
|||||||
|
|
||||||
// Reset the status of the "submit" button
|
// Reset the status of the "submit" button
|
||||||
if (options.modal) {
|
if (options.modal) {
|
||||||
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
enableSubmitButton(options, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any existing error messages from the form
|
// Remove any existing error messages from the form
|
||||||
|
@ -11,10 +11,10 @@
|
|||||||
clearFieldOptions,
|
clearFieldOptions,
|
||||||
closeModal,
|
closeModal,
|
||||||
enableField,
|
enableField,
|
||||||
|
enableSubmitButton,
|
||||||
getFieldValue,
|
getFieldValue,
|
||||||
reloadFieldOptions,
|
reloadFieldOptions,
|
||||||
showModalImage,
|
showModalImage,
|
||||||
removeRowFromModalForm,
|
|
||||||
showQuestionDialog,
|
showQuestionDialog,
|
||||||
showModalSpinner,
|
showModalSpinner,
|
||||||
*/
|
*/
|
||||||
@ -146,6 +146,24 @@ function createNewModal(options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convenience function to enable (or disable) the "submit" button on a modal form
|
||||||
|
*/
|
||||||
|
function enableSubmitButton(options, enable=true) {
|
||||||
|
|
||||||
|
if (!options || !options.modal) {
|
||||||
|
console.warn('enableSubmitButton() called without modal reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
||||||
|
} else {
|
||||||
|
$(options.modal).find('#modal-form-submit').prop('disabled', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function makeOption(text, value, title) {
|
function makeOption(text, value, title) {
|
||||||
/* Format an option for a select element
|
/* Format an option for a select element
|
||||||
*/
|
*/
|
||||||
@ -536,18 +554,6 @@ 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) {
|
function renderErrorMessage(xhr) {
|
||||||
|
|
||||||
var html = '<b>' + xhr.statusText + '</b><br>';
|
var html = '<b>' + xhr.statusText + '</b><br>';
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
imageHoverIcon,
|
imageHoverIcon,
|
||||||
inventreeGet,
|
inventreeGet,
|
||||||
inventreePut,
|
inventreePut,
|
||||||
launchModalForm,
|
|
||||||
linkButtonsToSelection,
|
linkButtonsToSelection,
|
||||||
loadTableFilters,
|
loadTableFilters,
|
||||||
makeIconBadge,
|
makeIconBadge,
|
||||||
@ -1604,6 +1603,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
|
|
||||||
/* Button callbacks for part table buttons */
|
/* Button callbacks for part table buttons */
|
||||||
|
|
||||||
|
// Callback function for the "order parts" button
|
||||||
$('#multi-part-order').click(function() {
|
$('#multi-part-order').click(function() {
|
||||||
var selections = getTableData(table);
|
var selections = getTableData(table);
|
||||||
|
|
||||||
@ -1613,31 +1613,82 @@ function loadPartTable(table, url, options={}) {
|
|||||||
parts.push(part);
|
parts.push(part);
|
||||||
});
|
});
|
||||||
|
|
||||||
orderParts(
|
orderParts(parts, {});
|
||||||
parts,
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback function for the "set category" button
|
||||||
$('#multi-part-category').click(function() {
|
$('#multi-part-category').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
selections.forEach(function(item) {
|
selections.forEach(function(item) {
|
||||||
parts.push(item.pk);
|
parts.push(item.pk);
|
||||||
});
|
});
|
||||||
|
|
||||||
launchModalForm('/part/set-category/', {
|
var html = `
|
||||||
data: {
|
<div class='alert alert-block alert-info'>
|
||||||
parts: parts,
|
{% trans "Set the part category for the selected parts" %}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
constructFormBody({}, {
|
||||||
|
title: '{% trans "Set Part Category" %}',
|
||||||
|
preFormContent: html,
|
||||||
|
fields: {
|
||||||
|
category: {
|
||||||
|
label: '{% trans "Category" %}',
|
||||||
|
help_text: '{% trans "Select Part Category" %}',
|
||||||
|
required: true,
|
||||||
|
type: 'related field',
|
||||||
|
model: 'partcategory',
|
||||||
|
api_url: '{% url "api-part-category-list" %}',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmit: function(fields, opts) {
|
||||||
|
var category = getFormFieldValue('category', fields['category'], opts);
|
||||||
|
|
||||||
|
if (category == null) {
|
||||||
|
handleFormErrors(
|
||||||
|
{
|
||||||
|
'category': ['{% trans "Category is required" %}']
|
||||||
|
},
|
||||||
|
opts.fields,
|
||||||
|
opts
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the category for each part in sequence
|
||||||
|
function setCategory() {
|
||||||
|
if (parts.length > 0) {
|
||||||
|
var part = parts.shift();
|
||||||
|
|
||||||
|
inventreePut(
|
||||||
|
`/api/part/${part}/`,
|
||||||
|
{
|
||||||
|
category: category,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
complete: setCategory,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// We are done!
|
||||||
|
$(opts.modal).modal('hide');
|
||||||
|
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the ball rolling
|
||||||
|
showModalSpinner(opts.modal);
|
||||||
|
setCategory();
|
||||||
},
|
},
|
||||||
reload: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback function for the "print label" button
|
||||||
$('#multi-part-print-label').click(function() {
|
$('#multi-part-print-label').click(function() {
|
||||||
var selections = getTableData(table);
|
var selections = getTableData(table);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user