mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Parameter types (#4935)
* Add fields to PartParameterTemplateModel - checkbox: Is the field a 'checkbox' - choices: List of valid options * Update javascript * Adds unit test for PartParameterTemplate - Checkbox cannot have units - Checkbox cannot have choices - Choices must be unique * Improve API filtering - Add "has_choices" filter - Add "has_units" filter * Prune dead code * Update js functions for creating / editing parameters * Update part parameter form - Rebuild the "data" field based on the selected template - Supports "string" / "boolean" / "select" * Adjust data input based on parameter type - Choice displays available options - Checkbox displays boolean switch - Otherwise displays text input - Adds more unit testing - Updates to forms.js for improved functionality * Calculate numeric value for boolean parameters * Update docs * Bump API version
This commit is contained in:
parent
2c05e3e74d
commit
e21a5e62b8
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 117
|
||||
INVENTREE_API_VERSION = 118
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v118 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4935
|
||||
- Adds extra fields for the PartParameterTemplate model
|
||||
|
||||
v117 -> 2023-05-22 : https://github.com/inventree/InvenTree/pull/4854
|
||||
- Part.units model now supports physical units (e.g. "kg", "m", "mm", etc)
|
||||
- Replaces SupplierPart "pack_size" field with "pack_quantity"
|
||||
|
@ -1349,8 +1349,35 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
|
||||
# Simple filter fields
|
||||
fields = [
|
||||
'units',
|
||||
'checkbox',
|
||||
]
|
||||
|
||||
has_choices = rest_filters.BooleanFilter(
|
||||
method='filter_has_choices',
|
||||
label='Has Choice',
|
||||
)
|
||||
|
||||
def filter_has_choices(self, queryset, name, value):
|
||||
"""Filter queryset to include only PartParameterTemplates with choices."""
|
||||
|
||||
if str2bool(value):
|
||||
return queryset.exclude(Q(choices=None) | Q(choices=''))
|
||||
else:
|
||||
return queryset.filter(Q(choices=None) | Q(choices=''))
|
||||
|
||||
has_units = rest_filters.BooleanFilter(
|
||||
method='filter_has_units',
|
||||
label='Has Units',
|
||||
)
|
||||
|
||||
def filter_has_units(self, queryset, name, value):
|
||||
"""Filter queryset to include only PartParameterTemplates with units."""
|
||||
|
||||
if str2bool(value):
|
||||
return queryset.exclude(Q(units=None) | Q(units=''))
|
||||
else:
|
||||
return queryset.filter(Q(units=None) | Q(units=''))
|
||||
|
||||
|
||||
class PartParameterTemplateList(ListCreateAPI):
|
||||
"""API endpoint for accessing a list of PartParameterTemplate objects.
|
||||
@ -1377,6 +1404,7 @@ class PartParameterTemplateList(ListCreateAPI):
|
||||
ordering_fields = [
|
||||
'name',
|
||||
'units',
|
||||
'checkbox',
|
||||
]
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
@ -1580,45 +1608,33 @@ class BomFilter(rest_filters.FilterSet):
|
||||
def filter_available_stock(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether each line item has any available stock"""
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
if value:
|
||||
queryset = queryset.filter(available_stock__gt=0)
|
||||
if str2bool(value):
|
||||
return queryset.filter(available_stock__gt=0)
|
||||
else:
|
||||
queryset = queryset.filter(available_stock=0)
|
||||
|
||||
return queryset
|
||||
return queryset.filter(available_stock=0)
|
||||
|
||||
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
|
||||
|
||||
def filter_on_order(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether each line item has any stock on order"""
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
if value:
|
||||
queryset = queryset.filter(on_order__gt=0)
|
||||
if str2bool(value):
|
||||
return queryset.filter(on_order__gt=0)
|
||||
else:
|
||||
queryset = queryset.filter(on_order=0)
|
||||
|
||||
return queryset
|
||||
return queryset.filter(on_order=0)
|
||||
|
||||
has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing")
|
||||
|
||||
def filter_has_pricing(self, queryset, name, value):
|
||||
"""Filter the queryset based on whether pricing information is available for the sub_part"""
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
q_a = Q(sub_part__pricing_data=None)
|
||||
q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None)
|
||||
|
||||
if value:
|
||||
queryset = queryset.exclude(q_a | q_b)
|
||||
if str2bool(value):
|
||||
return queryset.exclude(q_a | q_b)
|
||||
else:
|
||||
queryset = queryset.filter(q_a | q_b)
|
||||
|
||||
return queryset
|
||||
return queryset.filter(q_a | q_b)
|
||||
|
||||
|
||||
class BomMixin:
|
||||
|
23
InvenTree/part/migrations/0112_auto_20230531_1205.py
Normal file
23
InvenTree/part/migrations/0112_auto_20230531_1205.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-31 12:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0111_auto_20230521_1350'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='partparametertemplate',
|
||||
name='checkbox',
|
||||
field=models.BooleanField(default=False, help_text='Is this parameter a checkbox?', verbose_name='Checkbox'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='partparametertemplate',
|
||||
name='choices',
|
||||
field=models.CharField(blank=True, help_text='Valid choices for this parameter (comma-separated)', max_length=5000, verbose_name='Choices'),
|
||||
),
|
||||
]
|
@ -46,7 +46,8 @@ from common.settings import currency_code_default
|
||||
from company.models import SupplierPart
|
||||
from InvenTree import helpers, validators
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
from InvenTree.helpers import decimal2money, decimal2string, normalize
|
||||
from InvenTree.helpers import (decimal2money, decimal2string, normalize,
|
||||
str2bool)
|
||||
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
||||
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
||||
InvenTreeTree, MetadataMixin)
|
||||
@ -3307,6 +3308,8 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
||||
name: The name (key) of the Parameter [string]
|
||||
units: The units of the Parameter [string]
|
||||
description: Description of the parameter [string]
|
||||
checkbox: Boolean flag to indicate whether the parameter is a checkbox [bool]
|
||||
choices: List of valid choices for the parameter [string]
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@ -3321,6 +3324,47 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
||||
s += " ({units})".format(units=self.units)
|
||||
return s
|
||||
|
||||
def clean(self):
|
||||
"""Custom cleaning step for this model:
|
||||
|
||||
- A 'checkbox' field cannot have 'choices' set
|
||||
- A 'checkbox' field cannot have 'units' set
|
||||
"""
|
||||
|
||||
super().clean()
|
||||
|
||||
# Check that checkbox parameters do not have units or choices
|
||||
if self.checkbox:
|
||||
if self.units:
|
||||
raise ValidationError({
|
||||
'units': _('Checkbox parameters cannot have units')
|
||||
})
|
||||
|
||||
if self.choices:
|
||||
raise ValidationError({
|
||||
'choices': _('Checkbox parameters cannot have choices')
|
||||
})
|
||||
|
||||
# Check that 'choices' are in fact valid
|
||||
self.choices = self.choices.strip()
|
||||
|
||||
if self.choices:
|
||||
choice_set = set()
|
||||
|
||||
for choice in self.choices.split(','):
|
||||
choice = choice.strip()
|
||||
|
||||
# Ignore empty choices
|
||||
if not choice:
|
||||
continue
|
||||
|
||||
if choice in choice_set:
|
||||
raise ValidationError({
|
||||
'choices': _('Choices must be unique')
|
||||
})
|
||||
|
||||
choice_set.add(choice)
|
||||
|
||||
def validate_unique(self, exclude=None):
|
||||
"""Ensure that PartParameterTemplates cannot be created with the same name.
|
||||
|
||||
@ -3337,6 +3381,14 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
||||
except PartParameterTemplate.DoesNotExist:
|
||||
pass
|
||||
|
||||
def get_choices(self):
|
||||
"""Return a list of choices for this parameter template"""
|
||||
|
||||
if not self.choices:
|
||||
return []
|
||||
|
||||
return [x.strip() for x in self.choices.split(',') if x.strip()]
|
||||
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_('Name'),
|
||||
@ -3360,6 +3412,19 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
checkbox = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Checkbox'),
|
||||
help_text=_('Is this parameter a checkbox?')
|
||||
)
|
||||
|
||||
choices = models.CharField(
|
||||
max_length=5000,
|
||||
verbose_name=_('Choices'),
|
||||
help_text=_('Valid choices for this parameter (comma-separated)'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=PartParameterTemplate, dispatch_uid='post_save_part_parameter_template')
|
||||
def post_save_part_parameter_template(sender, instance, created, **kwargs):
|
||||
@ -3412,6 +3477,11 @@ class PartParameter(models.Model):
|
||||
# Validate the PartParameter before saving
|
||||
self.calculate_numeric_value()
|
||||
|
||||
# Convert 'boolean' values to 'True' / 'False'
|
||||
if self.template.checkbox:
|
||||
self.data = str2bool(self.data)
|
||||
self.data_numeric = 1 if self.data else 0
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
@ -3428,6 +3498,13 @@ class PartParameter(models.Model):
|
||||
'data': e.message
|
||||
})
|
||||
|
||||
# Validate the parameter data against the template choices
|
||||
if choices := self.template.get_choices():
|
||||
if self.data not in choices:
|
||||
raise ValidationError({
|
||||
'data': _('Invalid choice for parameter value')
|
||||
})
|
||||
|
||||
def calculate_numeric_value(self):
|
||||
"""Calculate a numeric value for the parameter data.
|
||||
|
||||
|
@ -216,6 +216,8 @@ class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerial
|
||||
'name',
|
||||
'units',
|
||||
'description',
|
||||
'checkbox',
|
||||
'choices',
|
||||
]
|
||||
|
||||
|
||||
|
@ -846,47 +846,13 @@
|
||||
}
|
||||
);
|
||||
|
||||
$('#param-table').inventreeTable({
|
||||
});
|
||||
|
||||
{% if roles.part.add %}
|
||||
$('#param-create').click(function() {
|
||||
|
||||
constructForm('{% url "api-part-parameter-list" %}', {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
part: {
|
||||
value: {{ part.pk }},
|
||||
hidden: true,
|
||||
},
|
||||
template: {
|
||||
filters: {
|
||||
ordering: 'name',
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
title: '{% trans "Add Parameter" %}',
|
||||
refreshTable: '#parameter-table',
|
||||
createPartParameter({{ part.pk }}, {
|
||||
refreshTable: '#parameter-table'
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$('.param-edit').click(function() {
|
||||
var button = $(this);
|
||||
|
||||
launchModalForm(button.attr('url'), {
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$('.param-delete').click(function() {
|
||||
var button = $(this);
|
||||
|
||||
launchModalForm(button.attr('url'), {
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onPanelLoad("part-attachments", function() {
|
||||
|
@ -102,6 +102,29 @@ class ParameterTests(TestCase):
|
||||
'params'
|
||||
]
|
||||
|
||||
def test_choice_validation(self):
|
||||
"""Test that parameter choices are correctly validated"""
|
||||
|
||||
template = PartParameterTemplate.objects.create(
|
||||
name='My Template',
|
||||
description='A template with choices',
|
||||
choices='red, blue, green'
|
||||
)
|
||||
|
||||
pass_values = ['red', 'blue', 'green']
|
||||
fail_values = ['rod', 'bleu', 'grene']
|
||||
|
||||
part = Part.objects.all().first()
|
||||
|
||||
for value in pass_values:
|
||||
param = PartParameter(part=part, template=template, data=value)
|
||||
param.full_clean()
|
||||
|
||||
for value in fail_values:
|
||||
param = PartParameter(part=part, template=template, data=value)
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
param.full_clean()
|
||||
|
||||
def test_unit_validation(self):
|
||||
"""Test validation of 'units' field for PartParameterTemplate"""
|
||||
|
||||
@ -116,7 +139,7 @@ class ParameterTests(TestCase):
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
tmp.full_clean()
|
||||
|
||||
def test_param_validation(self):
|
||||
def test_param_unit_validation(self):
|
||||
"""Test that parameters are correctly validated against template units"""
|
||||
|
||||
template = PartParameterTemplate.objects.create(
|
||||
@ -137,7 +160,7 @@ class ParameterTests(TestCase):
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
param.full_clean()
|
||||
|
||||
def test_param_conversion(self):
|
||||
def test_param_unit_conversion(self):
|
||||
"""Test that parameters are correctly converted to template units"""
|
||||
|
||||
template = PartParameterTemplate.objects.create(
|
||||
@ -202,6 +225,41 @@ class PartParameterTest(InvenTreeAPITestCase):
|
||||
|
||||
self.assertEqual(len(response.data), 4)
|
||||
|
||||
def test_param_template_validation(self):
|
||||
"""Test that part parameter template validation routines work correctly."""
|
||||
|
||||
# Checkbox parameter cannot have "units" specified
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
template = PartParameterTemplate(
|
||||
name='test',
|
||||
description='My description',
|
||||
units='mm',
|
||||
checkbox=True
|
||||
)
|
||||
|
||||
template.clean()
|
||||
|
||||
# Checkbox parameter cannot have "choices" specified
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
template = PartParameterTemplate(
|
||||
name='test',
|
||||
description='My description',
|
||||
choices='a,b,c',
|
||||
checkbox=True
|
||||
)
|
||||
|
||||
template.clean()
|
||||
|
||||
# Choices must be 'unique'
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
template = PartParameterTemplate(
|
||||
name='test',
|
||||
description='My description',
|
||||
choices='a,a,b',
|
||||
)
|
||||
|
||||
template.clean()
|
||||
|
||||
def test_create_param(self):
|
||||
"""Test that we can create a param via the API."""
|
||||
url = reverse('api-part-parameter-list')
|
||||
|
@ -309,11 +309,7 @@ onPanelLoad('part-parameters', function() {
|
||||
|
||||
$("#new-param").click(function() {
|
||||
constructForm('{% url "api-part-parameter-template-list" %}', {
|
||||
fields: {
|
||||
name: {},
|
||||
units: {},
|
||||
description: {},
|
||||
},
|
||||
fields: partParameterTemplateFields(),
|
||||
method: 'POST',
|
||||
title: '{% trans "Create Part Parameter Template" %}',
|
||||
refreshTable: '#param-table',
|
||||
|
@ -18,6 +18,7 @@
|
||||
showApiError,
|
||||
showMessage,
|
||||
showModalSpinner,
|
||||
toBool,
|
||||
*/
|
||||
|
||||
/* exported
|
||||
@ -990,15 +991,17 @@ function updateFieldValue(name, value, field, options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.type == null) {
|
||||
field.type = guessFieldType(el);
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'decimal':
|
||||
// Strip trailing zeros
|
||||
el.val(formatDecimal(value));
|
||||
break;
|
||||
case 'boolean':
|
||||
if (value == true || value.toString().toLowerCase() == 'true') {
|
||||
el.prop('checked');
|
||||
}
|
||||
el.prop('checked', toBool(value));
|
||||
break;
|
||||
case 'related field':
|
||||
// Clear?
|
||||
@ -1068,6 +1071,34 @@ function validateFormField(name, options) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Introspect the HTML element to guess the field type
|
||||
*/
|
||||
function guessFieldType(element) {
|
||||
|
||||
if (!element.exists) {
|
||||
console.error(`Could not find element '${element}' for guessFieldType`);
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (element.attr('type')) {
|
||||
case 'number':
|
||||
return 'decimal';
|
||||
case 'checkbox':
|
||||
return 'boolean';
|
||||
case 'date':
|
||||
return 'date';
|
||||
case 'datetime':
|
||||
return 'datetime';
|
||||
case 'text':
|
||||
return 'string';
|
||||
default:
|
||||
// Unknown field type
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Extract and field value before sending back to the server
|
||||
*
|
||||
@ -1088,9 +1119,16 @@ function getFormFieldValue(name, field={}, options={}) {
|
||||
|
||||
var value = null;
|
||||
|
||||
let guessed_type = guessFieldType(el);
|
||||
|
||||
// If field type is not specified, try to guess it
|
||||
if (field.type == null || guessed_type == 'boolean') {
|
||||
field.type = guessed_type;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'boolean':
|
||||
value = el.is(':checked');
|
||||
value = toBool(el.prop("checked"));
|
||||
break;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
|
@ -40,15 +40,42 @@
|
||||
*/
|
||||
|
||||
|
||||
function yesNoLabel(value, options={}) {
|
||||
var text = '';
|
||||
var color = '';
|
||||
/*
|
||||
* Convert a value (which may be a string) to a boolean value
|
||||
*
|
||||
* @param {string} value: Input value
|
||||
* @returns {boolean} true or false
|
||||
*/
|
||||
function toBool(value) {
|
||||
|
||||
if (value) {
|
||||
text = '{% trans "YES" %}';
|
||||
if (typeof value == 'string') {
|
||||
|
||||
if (value.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = value.toLowerCase();
|
||||
|
||||
if (['true', 't', 'yes', 'y', '1', 'on', 'ok'].includes(value)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return value == true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function yesNoLabel(value, options={}) {
|
||||
let text = '';
|
||||
let color = '';
|
||||
|
||||
if (toBool(value)) {
|
||||
text = options.pass || '{% trans "YES" %}';
|
||||
color = 'bg-success';
|
||||
} else {
|
||||
text = '{% trans "NO" %}';
|
||||
text = options.fail || '{% trans "NO" %}';
|
||||
color = 'bg-warning';
|
||||
}
|
||||
|
||||
|
@ -874,8 +874,8 @@ function insertActionButton(modal, options) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Attach a provided list of buttons */
|
||||
function attachButtons(modal, buttons) {
|
||||
/* Attach a provided list of buttons */
|
||||
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
insertActionButton(modal, buttons[i]);
|
||||
@ -883,14 +883,14 @@ function attachButtons(modal, buttons) {
|
||||
}
|
||||
|
||||
|
||||
/* Attach a 'callback' function to a given field in the modal form.
|
||||
* When the value of that field is changed, the callback function is performed.
|
||||
*
|
||||
* options:
|
||||
* - field: The name of the field to attach to
|
||||
* - action: A function to perform
|
||||
*/
|
||||
function attachFieldCallback(modal, callback) {
|
||||
/* Attach a 'callback' function to a given field in the modal form.
|
||||
* When the value of that field is changed, the callback function is performed.
|
||||
*
|
||||
* options:
|
||||
* - field: The name of the field to attach to
|
||||
* - action: A function to perform
|
||||
*/
|
||||
|
||||
// Find the field input in the form
|
||||
var field = getFieldByName(modal, callback.field);
|
||||
@ -907,8 +907,8 @@ function attachFieldCallback(modal, callback) {
|
||||
}
|
||||
|
||||
|
||||
/* Attach a provided list of callback functions */
|
||||
function attachCallbacks(modal, callbacks) {
|
||||
/* Attach a provided list of callback functions */
|
||||
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
attachFieldCallback(modal, callbacks[i]);
|
||||
@ -916,13 +916,13 @@ function attachCallbacks(modal, callbacks) {
|
||||
}
|
||||
|
||||
|
||||
/* Update a modal form after data are received from the server.
|
||||
* Manages POST requests until the form is successfully submitted.
|
||||
*
|
||||
* The server should respond with a JSON object containing a boolean value 'form_valid'
|
||||
* Form submission repeats (after user interaction) until 'form_valid' = true
|
||||
*/
|
||||
function handleModalForm(url, options) {
|
||||
/* Update a modal form after data are received from the server.
|
||||
* Manages POST requests until the form is successfully submitted.
|
||||
*
|
||||
* The server should respond with a JSON object containing a boolean value 'form_valid'
|
||||
* Form submission repeats (after user interaction) until 'form_valid' = true
|
||||
*/
|
||||
|
||||
var modal = options.modal || '#modal-form';
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
Chart,
|
||||
constructForm,
|
||||
constructFormBody,
|
||||
constructInput,
|
||||
convertCurrency,
|
||||
formatCurrency,
|
||||
formatDecimal,
|
||||
@ -14,6 +15,7 @@
|
||||
getFormFieldValue,
|
||||
getTableData,
|
||||
global_settings,
|
||||
guessFieldType,
|
||||
handleFormErrors,
|
||||
handleFormSuccess,
|
||||
imageHoverIcon,
|
||||
@ -42,6 +44,7 @@
|
||||
showMessage,
|
||||
showModalSpinner,
|
||||
thumbnailImage,
|
||||
updateFieldValue,
|
||||
withTitle,
|
||||
wrapButtons,
|
||||
yesNoLabel,
|
||||
@ -1281,6 +1284,137 @@ function loadSimplePartTable(table, url, options={}) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Construct a set of fields for the PartParameter model.
|
||||
* Note that the 'data' field changes based on the seleted parameter template
|
||||
*/
|
||||
function partParameterFields(options={}) {
|
||||
|
||||
let fields = {
|
||||
part: {
|
||||
hidden: true, // Part is set by the parent form
|
||||
},
|
||||
template: {
|
||||
filters: {
|
||||
ordering: 'name',
|
||||
},
|
||||
onEdit: function(value, name, field, opts) {
|
||||
// Callback function when the parameter template is selected.
|
||||
// We rebuild the 'data' field based on the template selection
|
||||
|
||||
let checkbox = false;
|
||||
let choices = [];
|
||||
|
||||
if (value) {
|
||||
// Request the parameter template data
|
||||
inventreeGet(`{% url "api-part-parameter-template-list" %}${value}/`, {}, {
|
||||
async: false,
|
||||
success: function(response) {
|
||||
if (response.checkbox) {
|
||||
// Checkbox input
|
||||
checkbox = true;
|
||||
} else if (response.choices) {
|
||||
// Select input
|
||||
response.choices.split(',').forEach(function(choice) {
|
||||
choice = choice.trim();
|
||||
choices.push({
|
||||
value: choice,
|
||||
display_name: choice,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Find the current field element
|
||||
let el = $(opts.modal).find('#id_data');
|
||||
|
||||
// Extract the current value from the field
|
||||
let val = getFormFieldValue('data', {}, opts);
|
||||
|
||||
// Rebuild the field
|
||||
let parameters = {};
|
||||
|
||||
if (checkbox) {
|
||||
parameters.type = 'boolean';
|
||||
} else if (choices.length > 0) {
|
||||
parameters.type = 'choice';
|
||||
parameters.choices = choices;
|
||||
} else {
|
||||
parameters.type = 'string';
|
||||
}
|
||||
|
||||
let existing_field_type = guessFieldType(el);
|
||||
|
||||
// If the field type has changed, we need to replace the field
|
||||
if (existing_field_type != parameters.type) {
|
||||
// Construct the new field
|
||||
let new_field = constructInput('data', parameters, opts);
|
||||
|
||||
if (guessFieldType(el) == 'boolean') {
|
||||
// Boolean fields are wrapped in a parent element
|
||||
el.parent().replaceWith(new_field);
|
||||
} else {
|
||||
el.replaceWith(new_field);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the field parameters in the form options
|
||||
opts.fields.data.type = parameters.type;
|
||||
updateFieldValue('data', val, parameters, opts);
|
||||
}
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
if (options.part) {
|
||||
fields.part.value = options.part;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Launch a modal form for creating a new PartParameter object
|
||||
*/
|
||||
function createPartParameter(part_id, options={}) {
|
||||
|
||||
options.fields = partParameterFields({
|
||||
part: part_id,
|
||||
});
|
||||
|
||||
options.processBeforeUpload = function(data) {
|
||||
// Convert data to string
|
||||
data.data = data.data.toString();
|
||||
return data;
|
||||
}
|
||||
|
||||
options.method = 'POST';
|
||||
options.title = '{% trans "Add Parameter" %}';
|
||||
|
||||
constructForm('{% url "api-part-parameter-list" %}', options);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Launch a modal form for editing a PartParameter object
|
||||
*/
|
||||
function editPartParameter(param_id, options={}) {
|
||||
options.fields = partParameterFields();
|
||||
options.title = '{% trans "Edit Parameter" %}';
|
||||
|
||||
options.processBeforeUpload = function(data) {
|
||||
// Convert data to string
|
||||
data.data = data.data.toString();
|
||||
return data;
|
||||
}
|
||||
|
||||
constructForm(`{% url "api-part-parameter-list" %}${param_id}/`, options);
|
||||
}
|
||||
|
||||
|
||||
function loadPartParameterTable(table, options) {
|
||||
|
||||
var params = options.params || {};
|
||||
@ -1331,6 +1465,15 @@ function loadPartParameterTable(table, options) {
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
let template = row.template_detail;
|
||||
|
||||
if (template.checkbox) {
|
||||
return yesNoLabel(value, {
|
||||
pass: '{% trans "True" %}',
|
||||
fail: '{% trans "False" %}',
|
||||
});
|
||||
}
|
||||
|
||||
if (row.data_numeric && row.template_detail.units) {
|
||||
return `<span title='${row.data_numeric} ${row.template_detail.units}'>${row.data}</span>`;
|
||||
} else {
|
||||
@ -1368,12 +1511,8 @@ function loadPartParameterTable(table, options) {
|
||||
$(table).find('.button-parameter-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`{% url "api-part-parameter-list" %}${pk}/`, {
|
||||
fields: {
|
||||
data: {},
|
||||
},
|
||||
title: '{% trans "Edit Parameter" %}',
|
||||
refreshTable: table,
|
||||
editPartParameter(pk, {
|
||||
refreshTable: table
|
||||
});
|
||||
});
|
||||
|
||||
@ -1391,6 +1530,24 @@ function loadPartParameterTable(table, options) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Return a list of fields for a part parameter template
|
||||
*/
|
||||
function partParameterTemplateFields() {
|
||||
return {
|
||||
name: {},
|
||||
description: {},
|
||||
units: {
|
||||
icon: 'fa-ruler',
|
||||
},
|
||||
choices: {
|
||||
icon: 'fa-th-list',
|
||||
},
|
||||
checkbox: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Construct a table showing a list of part parameter templates
|
||||
*/
|
||||
@ -1410,6 +1567,8 @@ function loadPartParameterTemplateTable(table, options={}) {
|
||||
url: '{% url "api-part-parameter-template-list" %}',
|
||||
original: params,
|
||||
queryParams: filters,
|
||||
sortable: true,
|
||||
sidePagination: 'server',
|
||||
name: 'part-parameter-templates',
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No part parameter templates found" %}';
|
||||
@ -1438,6 +1597,21 @@ function loadPartParameterTemplateTable(table, options={}) {
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
field: 'checkbox',
|
||||
title: '{% trans "Checkbox" %}',
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
formatter: function(value) {
|
||||
return yesNoLabel(value);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'choices',
|
||||
title: '{% trans "Choices" %}',
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
@ -1459,11 +1633,7 @@ function loadPartParameterTemplateTable(table, options={}) {
|
||||
constructForm(
|
||||
`/api/part/parameter/template/${pk}/`,
|
||||
{
|
||||
fields: {
|
||||
name: {},
|
||||
units: {},
|
||||
description: {},
|
||||
},
|
||||
fields: partParameterTemplateFields(),
|
||||
title: '{% trans "Edit Part Parameter Template" %}',
|
||||
refreshTable: table,
|
||||
}
|
||||
|
@ -709,9 +709,28 @@ function getCompanyFilters() {
|
||||
}
|
||||
|
||||
|
||||
// Return a dictionary of filters for the "PartParameter" table
|
||||
function getPartParameterFilters() {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
// Return a dictionary of filters for the "part parameter template" table
|
||||
function getPartParameterTemplateFilters() {
|
||||
return {};
|
||||
return {
|
||||
checkbox: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Checkbox" %}',
|
||||
},
|
||||
has_choices: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Has Choices" %}',
|
||||
},
|
||||
has_units: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Has Units" %}',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -747,6 +766,8 @@ function getAvailableTableFilters(tableKey) {
|
||||
return getStockLocationFilters();
|
||||
case 'parameters':
|
||||
return getParametricPartTableFilters();
|
||||
case 'part-parameters':
|
||||
return getPartParameterFilters();
|
||||
case 'part-parameter-templates':
|
||||
return getPartParameterTemplateFilters();
|
||||
case 'parts':
|
||||
|
BIN
docs/docs/assets/images/part/parameter_template_edit.png
Normal file
BIN
docs/docs/assets/images/part/parameter_template_edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 84 KiB |
@ -10,25 +10,41 @@ Part parameters are located in the "Parameters" tab, on each part detail page.
|
||||
There is no limit for the number of part parameters and they are fully customizable through the use of [parameters templates](#parameter-templates).
|
||||
|
||||
Here is an example of parameters for a capacitor:
|
||||
|
||||
{% with id="part_parameters_example", url="part/part_parameters_example.png", description="Part Parameters Example List" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
## Parameter Templates
|
||||
|
||||
Parameter templates are used to define the different types of parameters which are available for use. These are edited via the [settings interface](../settings/global.md).
|
||||
Parameter templates are used to define the different types of parameters which are available for use. The following attributes are defined for a parameter template:
|
||||
|
||||
| Attribute | Description |
|
||||
| --- | --- |
|
||||
| Name | The name of the parameter template (*must be unique*) |
|
||||
| Description | Optional description for the template |
|
||||
| Units | Optional units field (*must be a valid [physical unit](#parameter-units)*) |
|
||||
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
|
||||
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
|
||||
|
||||
### Create Template
|
||||
|
||||
Parameter templates are created and edited via the [settings interface](../settings/global.md).
|
||||
|
||||
To create a template:
|
||||
|
||||
- Navigate to the "Settings" page
|
||||
- Click on the "Parts" tab
|
||||
- Scroll down to the "Part Parameter Templates" section
|
||||
- Click on the "Part Parameters" tab
|
||||
- Click on the "New Parameter" button
|
||||
- Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields
|
||||
- Click on the "Submit" button.
|
||||
|
||||
An existing template can be edited by clicking on the "Edit" button associated with that template:
|
||||
|
||||
{% with id="part_parameter_template", url="part/parameter_template_edit.png", description="Edit Parameter Template" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
### Create Parameter
|
||||
|
||||
After [creating a template](#create-template) or using the existing templates, you can add parameters to any part.
|
||||
@ -51,12 +67,6 @@ To access a category's parametric table, click on the "Parameters" tab within th
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
Below is an example of capacitor parametric table filtered with `Package Type = 0402`:
|
||||
|
||||
{% with id="parametric_table_example", url="part/parametric_table_example.png", description="Parametric Table Example" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
### Sorting by Parameter Value
|
||||
|
||||
The parametric parts table allows the returned parts to be sorted by particular parameter values. Click on the header of a particular parameter column to sort results by that parameter:
|
||||
|
Loading…
Reference in New Issue
Block a user