2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-01 04:56:45 +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:
Oliver 2023-06-01 07:20:11 +10:00 committed by GitHub
parent 2c05e3e74d
commit e21a5e62b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 519 additions and 112 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version # 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 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 v117 -> 2023-05-22 : https://github.com/inventree/InvenTree/pull/4854
- Part.units model now supports physical units (e.g. "kg", "m", "mm", etc) - Part.units model now supports physical units (e.g. "kg", "m", "mm", etc)
- Replaces SupplierPart "pack_size" field with "pack_quantity" - Replaces SupplierPart "pack_size" field with "pack_quantity"

View File

@ -1349,8 +1349,35 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
# Simple filter fields # Simple filter fields
fields = [ fields = [
'units', '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): class PartParameterTemplateList(ListCreateAPI):
"""API endpoint for accessing a list of PartParameterTemplate objects. """API endpoint for accessing a list of PartParameterTemplate objects.
@ -1377,6 +1404,7 @@ class PartParameterTemplateList(ListCreateAPI):
ordering_fields = [ ordering_fields = [
'name', 'name',
'units', 'units',
'checkbox',
] ]
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
@ -1580,45 +1608,33 @@ class BomFilter(rest_filters.FilterSet):
def filter_available_stock(self, queryset, name, value): def filter_available_stock(self, queryset, name, value):
"""Filter the queryset based on whether each line item has any available stock""" """Filter the queryset based on whether each line item has any available stock"""
value = str2bool(value) if str2bool(value):
return queryset.filter(available_stock__gt=0)
if value:
queryset = queryset.filter(available_stock__gt=0)
else: else:
queryset = queryset.filter(available_stock=0) return queryset.filter(available_stock=0)
return queryset
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order") on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
def filter_on_order(self, queryset, name, value): def filter_on_order(self, queryset, name, value):
"""Filter the queryset based on whether each line item has any stock on order""" """Filter the queryset based on whether each line item has any stock on order"""
value = str2bool(value) if str2bool(value):
return queryset.filter(on_order__gt=0)
if value:
queryset = queryset.filter(on_order__gt=0)
else: else:
queryset = queryset.filter(on_order=0) return queryset.filter(on_order=0)
return queryset
has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing") has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing")
def filter_has_pricing(self, queryset, name, value): def filter_has_pricing(self, queryset, name, value):
"""Filter the queryset based on whether pricing information is available for the sub_part""" """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_a = Q(sub_part__pricing_data=None)
q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None) q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None)
if value: if str2bool(value):
queryset = queryset.exclude(q_a | q_b) return queryset.exclude(q_a | q_b)
else: else:
queryset = queryset.filter(q_a | q_b) return queryset.filter(q_a | q_b)
return queryset
class BomMixin: class BomMixin:

View 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'),
),
]

View File

@ -46,7 +46,8 @@ from common.settings import currency_code_default
from company.models import SupplierPart from company.models import SupplierPart
from InvenTree import helpers, validators from InvenTree import helpers, validators
from InvenTree.fields import InvenTreeURLField 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, from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
InvenTreeBarcodeMixin, InvenTreeNotesMixin, InvenTreeBarcodeMixin, InvenTreeNotesMixin,
InvenTreeTree, MetadataMixin) InvenTreeTree, MetadataMixin)
@ -3307,6 +3308,8 @@ class PartParameterTemplate(MetadataMixin, models.Model):
name: The name (key) of the Parameter [string] name: The name (key) of the Parameter [string]
units: The units of the Parameter [string] units: The units of the Parameter [string]
description: Description 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 @staticmethod
@ -3321,6 +3324,47 @@ class PartParameterTemplate(MetadataMixin, models.Model):
s += " ({units})".format(units=self.units) s += " ({units})".format(units=self.units)
return s 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): def validate_unique(self, exclude=None):
"""Ensure that PartParameterTemplates cannot be created with the same name. """Ensure that PartParameterTemplates cannot be created with the same name.
@ -3337,6 +3381,14 @@ class PartParameterTemplate(MetadataMixin, models.Model):
except PartParameterTemplate.DoesNotExist: except PartParameterTemplate.DoesNotExist:
pass 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( name = models.CharField(
max_length=100, max_length=100,
verbose_name=_('Name'), verbose_name=_('Name'),
@ -3360,6 +3412,19 @@ class PartParameterTemplate(MetadataMixin, models.Model):
blank=True, 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') @receiver(post_save, sender=PartParameterTemplate, dispatch_uid='post_save_part_parameter_template')
def post_save_part_parameter_template(sender, instance, created, **kwargs): def post_save_part_parameter_template(sender, instance, created, **kwargs):
@ -3412,6 +3477,11 @@ class PartParameter(models.Model):
# Validate the PartParameter before saving # Validate the PartParameter before saving
self.calculate_numeric_value() 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) super().save(*args, **kwargs)
def clean(self): def clean(self):
@ -3428,6 +3498,13 @@ class PartParameter(models.Model):
'data': e.message '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): def calculate_numeric_value(self):
"""Calculate a numeric value for the parameter data. """Calculate a numeric value for the parameter data.

View File

@ -216,6 +216,8 @@ class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerial
'name', 'name',
'units', 'units',
'description', 'description',
'checkbox',
'choices',
] ]

View File

@ -846,47 +846,13 @@
} }
); );
$('#param-table').inventreeTable({
});
{% if roles.part.add %} {% if roles.part.add %}
$('#param-create').click(function() { $('#param-create').click(function() {
createPartParameter({{ part.pk }}, {
constructForm('{% url "api-part-parameter-list" %}', { refreshTable: '#parameter-table'
method: 'POST',
fields: {
part: {
value: {{ part.pk }},
hidden: true,
},
template: {
filters: {
ordering: 'name',
},
},
data: {},
},
title: '{% trans "Add Parameter" %}',
refreshTable: '#parameter-table',
}); });
}); });
{% endif %} {% 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() { onPanelLoad("part-attachments", function() {

View File

@ -102,6 +102,29 @@ class ParameterTests(TestCase):
'params' '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): def test_unit_validation(self):
"""Test validation of 'units' field for PartParameterTemplate""" """Test validation of 'units' field for PartParameterTemplate"""
@ -116,7 +139,7 @@ class ParameterTests(TestCase):
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
tmp.full_clean() tmp.full_clean()
def test_param_validation(self): def test_param_unit_validation(self):
"""Test that parameters are correctly validated against template units""" """Test that parameters are correctly validated against template units"""
template = PartParameterTemplate.objects.create( template = PartParameterTemplate.objects.create(
@ -137,7 +160,7 @@ class ParameterTests(TestCase):
with self.assertRaises(django_exceptions.ValidationError): with self.assertRaises(django_exceptions.ValidationError):
param.full_clean() param.full_clean()
def test_param_conversion(self): def test_param_unit_conversion(self):
"""Test that parameters are correctly converted to template units""" """Test that parameters are correctly converted to template units"""
template = PartParameterTemplate.objects.create( template = PartParameterTemplate.objects.create(
@ -202,6 +225,41 @@ class PartParameterTest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), 4) 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): def test_create_param(self):
"""Test that we can create a param via the API.""" """Test that we can create a param via the API."""
url = reverse('api-part-parameter-list') url = reverse('api-part-parameter-list')

View File

@ -309,11 +309,7 @@ onPanelLoad('part-parameters', function() {
$("#new-param").click(function() { $("#new-param").click(function() {
constructForm('{% url "api-part-parameter-template-list" %}', { constructForm('{% url "api-part-parameter-template-list" %}', {
fields: { fields: partParameterTemplateFields(),
name: {},
units: {},
description: {},
},
method: 'POST', method: 'POST',
title: '{% trans "Create Part Parameter Template" %}', title: '{% trans "Create Part Parameter Template" %}',
refreshTable: '#param-table', refreshTable: '#param-table',

View File

@ -18,6 +18,7 @@
showApiError, showApiError,
showMessage, showMessage,
showModalSpinner, showModalSpinner,
toBool,
*/ */
/* exported /* exported
@ -990,15 +991,17 @@ function updateFieldValue(name, value, field, options) {
return; return;
} }
if (field.type == null) {
field.type = guessFieldType(el);
}
switch (field.type) { switch (field.type) {
case 'decimal': case 'decimal':
// Strip trailing zeros // Strip trailing zeros
el.val(formatDecimal(value)); el.val(formatDecimal(value));
break; break;
case 'boolean': case 'boolean':
if (value == true || value.toString().toLowerCase() == 'true') { el.prop('checked', toBool(value));
el.prop('checked');
}
break; break;
case 'related field': case 'related field':
// Clear? // 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 * Extract and field value before sending back to the server
* *
@ -1088,9 +1119,16 @@ function getFormFieldValue(name, field={}, options={}) {
var value = null; 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) { switch (field.type) {
case 'boolean': case 'boolean':
value = el.is(':checked'); value = toBool(el.prop("checked"));
break; break;
case 'date': case 'date':
case 'datetime': case 'datetime':

View File

@ -40,15 +40,42 @@
*/ */
function yesNoLabel(value, options={}) { /*
var text = ''; * Convert a value (which may be a string) to a boolean value
var color = ''; *
* @param {string} value: Input value
* @returns {boolean} true or false
*/
function toBool(value) {
if (value) { if (typeof value == 'string') {
text = '{% trans "YES" %}';
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'; color = 'bg-success';
} else { } else {
text = '{% trans "NO" %}'; text = options.fail || '{% trans "NO" %}';
color = 'bg-warning'; color = 'bg-warning';
} }

View File

@ -874,8 +874,8 @@ function insertActionButton(modal, options) {
} }
} }
function attachButtons(modal, buttons) {
/* Attach a provided list of buttons */ /* Attach a provided list of buttons */
function attachButtons(modal, buttons) {
for (var i = 0; i < buttons.length; i++) { for (var i = 0; i < buttons.length; i++) {
insertActionButton(modal, buttons[i]); insertActionButton(modal, buttons[i]);
@ -883,7 +883,6 @@ function attachButtons(modal, buttons) {
} }
function attachFieldCallback(modal, callback) {
/* Attach a 'callback' function to a given field in the modal form. /* 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. * When the value of that field is changed, the callback function is performed.
* *
@ -891,6 +890,7 @@ function attachFieldCallback(modal, callback) {
* - field: The name of the field to attach to * - field: The name of the field to attach to
* - action: A function to perform * - action: A function to perform
*/ */
function attachFieldCallback(modal, callback) {
// Find the field input in the form // Find the field input in the form
var field = getFieldByName(modal, callback.field); var field = getFieldByName(modal, callback.field);
@ -907,8 +907,8 @@ function attachFieldCallback(modal, callback) {
} }
function attachCallbacks(modal, callbacks) {
/* Attach a provided list of callback functions */ /* Attach a provided list of callback functions */
function attachCallbacks(modal, callbacks) {
for (var i = 0; i < callbacks.length; i++) { for (var i = 0; i < callbacks.length; i++) {
attachFieldCallback(modal, callbacks[i]); attachFieldCallback(modal, callbacks[i]);
@ -916,13 +916,13 @@ function attachCallbacks(modal, callbacks) {
} }
function handleModalForm(url, options) {
/* Update a modal form after data are received from the server. /* Update a modal form after data are received from the server.
* Manages POST requests until the form is successfully submitted. * Manages POST requests until the form is successfully submitted.
* *
* The server should respond with a JSON object containing a boolean value 'form_valid' * The server should respond with a JSON object containing a boolean value 'form_valid'
* Form submission repeats (after user interaction) until 'form_valid' = true * Form submission repeats (after user interaction) until 'form_valid' = true
*/ */
function handleModalForm(url, options) {
var modal = options.modal || '#modal-form'; var modal = options.modal || '#modal-form';

View File

@ -6,6 +6,7 @@
Chart, Chart,
constructForm, constructForm,
constructFormBody, constructFormBody,
constructInput,
convertCurrency, convertCurrency,
formatCurrency, formatCurrency,
formatDecimal, formatDecimal,
@ -14,6 +15,7 @@
getFormFieldValue, getFormFieldValue,
getTableData, getTableData,
global_settings, global_settings,
guessFieldType,
handleFormErrors, handleFormErrors,
handleFormSuccess, handleFormSuccess,
imageHoverIcon, imageHoverIcon,
@ -42,6 +44,7 @@
showMessage, showMessage,
showModalSpinner, showModalSpinner,
thumbnailImage, thumbnailImage,
updateFieldValue,
withTitle, withTitle,
wrapButtons, wrapButtons,
yesNoLabel, 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) { function loadPartParameterTable(table, options) {
var params = options.params || {}; var params = options.params || {};
@ -1331,6 +1465,15 @@ function loadPartParameterTable(table, options) {
switchable: false, switchable: false,
sortable: true, sortable: true,
formatter: function(value, row) { 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) { if (row.data_numeric && row.template_detail.units) {
return `<span title='${row.data_numeric} ${row.template_detail.units}'>${row.data}</span>`; return `<span title='${row.data_numeric} ${row.template_detail.units}'>${row.data}</span>`;
} else { } else {
@ -1368,12 +1511,8 @@ function loadPartParameterTable(table, options) {
$(table).find('.button-parameter-edit').click(function() { $(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk'); var pk = $(this).attr('pk');
constructForm(`{% url "api-part-parameter-list" %}${pk}/`, { editPartParameter(pk, {
fields: { refreshTable: table
data: {},
},
title: '{% trans "Edit Parameter" %}',
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 * 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" %}', url: '{% url "api-part-parameter-template-list" %}',
original: params, original: params,
queryParams: filters, queryParams: filters,
sortable: true,
sidePagination: 'server',
name: 'part-parameter-templates', name: 'part-parameter-templates',
formatNoMatches: function() { formatNoMatches: function() {
return '{% trans "No part parameter templates found" %}'; return '{% trans "No part parameter templates found" %}';
@ -1438,6 +1597,21 @@ function loadPartParameterTemplateTable(table, options={}) {
sortable: false, sortable: false,
switchable: true, 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) { formatter: function(value, row, index, field) {
@ -1459,11 +1633,7 @@ function loadPartParameterTemplateTable(table, options={}) {
constructForm( constructForm(
`/api/part/parameter/template/${pk}/`, `/api/part/parameter/template/${pk}/`,
{ {
fields: { fields: partParameterTemplateFields(),
name: {},
units: {},
description: {},
},
title: '{% trans "Edit Part Parameter Template" %}', title: '{% trans "Edit Part Parameter Template" %}',
refreshTable: table, refreshTable: table,
} }

View File

@ -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 // Return a dictionary of filters for the "part parameter template" table
function getPartParameterTemplateFilters() { 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(); return getStockLocationFilters();
case 'parameters': case 'parameters':
return getParametricPartTableFilters(); return getParametricPartTableFilters();
case 'part-parameters':
return getPartParameterFilters();
case 'part-parameter-templates': case 'part-parameter-templates':
return getPartParameterTemplateFilters(); return getPartParameterTemplateFilters();
case 'parts': case 'parts':

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

View File

@ -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). 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: 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" %} {% with id="part_parameters_example", url="part/part_parameters_example.png", description="Part Parameters Example List" %}
{% include 'img.html' %} {% include 'img.html' %}
{% endwith %} {% endwith %}
## Parameter Templates ## 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 ### Create Template
Parameter templates are created and edited via the [settings interface](../settings/global.md).
To create a template: To create a template:
- Navigate to the "Settings" page - Navigate to the "Settings" page
- Click on the "Parts" tab - Click on the "Part Parameters" tab
- Scroll down to the "Part Parameter Templates" section
- Click on the "New Parameter" button - Click on the "New Parameter" button
- Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields - Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields
- Click on the "Submit" button. - 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 ### Create Parameter
After [creating a template](#create-template) or using the existing templates, you can add parameters to any part. 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' %} {% include 'img.html' %}
{% endwith %} {% 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 ### 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: 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: