diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index b112c93fa9..6fecce5ace 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -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" diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index b9a5b4575c..269666e4c0 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -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: diff --git a/InvenTree/part/migrations/0112_auto_20230531_1205.py b/InvenTree/part/migrations/0112_auto_20230531_1205.py new file mode 100644 index 0000000000..612761a358 --- /dev/null +++ b/InvenTree/part/migrations/0112_auto_20230531_1205.py @@ -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'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8e8b7569ef..8e925166bd 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -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. diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 98f97906c4..97a1de33a0 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -216,6 +216,8 @@ class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerial 'name', 'units', 'description', + 'checkbox', + 'choices', ] diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 562641d01a..447c83ee01 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -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() { diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index 88161b221d..91537cbfa3 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -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') diff --git a/InvenTree/templates/InvenTree/settings/settings_staff_js.html b/InvenTree/templates/InvenTree/settings/settings_staff_js.html index 98d13c5a38..7bb5d1da82 100644 --- a/InvenTree/templates/InvenTree/settings/settings_staff_js.html +++ b/InvenTree/templates/InvenTree/settings/settings_staff_js.html @@ -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', diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index f273bcfb9f..1380ecb15a 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -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': diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js index 621639b702..7d8e4400b0 100644 --- a/InvenTree/templates/js/translated/helpers.js +++ b/InvenTree/templates/js/translated/helpers.js @@ -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'; } diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js index 1ee77b4f86..c02a0ac252 100644 --- a/InvenTree/templates/js/translated/modals.js +++ b/InvenTree/templates/js/translated/modals.js @@ -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'; diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 5a709de6db..92ed5ce5e1 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -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 `${row.data}`; } 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, } diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js index c9a7473ccf..2ff3569bdf 100644 --- a/InvenTree/templates/js/translated/table_filters.js +++ b/InvenTree/templates/js/translated/table_filters.js @@ -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': diff --git a/docs/docs/assets/images/part/parameter_template_edit.png b/docs/docs/assets/images/part/parameter_template_edit.png new file mode 100644 index 0000000000..c91cca4c1e Binary files /dev/null and b/docs/docs/assets/images/part/parameter_template_edit.png differ diff --git a/docs/docs/assets/images/part/part_parameters_example.png b/docs/docs/assets/images/part/part_parameters_example.png index 933278b852..b50aa2834d 100644 Binary files a/docs/docs/assets/images/part/part_parameters_example.png and b/docs/docs/assets/images/part/part_parameters_example.png differ diff --git a/docs/docs/part/parameter.md b/docs/docs/part/parameter.md index bffa321843..2bb87c8273 100644 --- a/docs/docs/part/parameter.md +++ b/docs/docs/part/parameter.md @@ -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: