diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index 1b6a6b3f0b..76d485cef9 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -5,6 +5,7 @@ Custom field validators for InvenTree from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import FieldDoesNotExist from moneyed import CURRENCIES @@ -156,3 +157,33 @@ def validate_overage(value): raise ValidationError( _("Overage must be an integer value or a percentage") ) + + +def validate_part_name_format(self): + """ + Validate part name format. + Make sure that each template container has a field of Part Model + """ + + jinja_template_regex = re.compile('{{.*?}}') + field_name_regex = re.compile('(?<=part\\.)[A-z]+') + for jinja_template in jinja_template_regex.findall(str(self)): + # make sure at least one and only one field is present inside the parser + field_names = field_name_regex.findall(jinja_template) + if len(field_names) < 1: + raise ValidationError({ + 'value': 'At least one field must be present inside a jinja template container i.e {{}}' + }) + + # Make sure that the field_name exists in Part model + from part.models import Part + + for field_name in field_names: + try: + Part._meta.get_field(field_name) + except FieldDoesNotExist: + raise ValidationError({ + 'value': f'{field_name} does not exist in Part Model' + }) + + return True diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index d0dfc7b014..d4f26af739 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -25,6 +25,7 @@ from django.core.exceptions import ValidationError import InvenTree.helpers import InvenTree.fields +import InvenTree.validators import logging @@ -702,6 +703,14 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'validator': bool }, + 'PART_NAME_FORMAT': { + 'name': _('Part Name Display Format'), + 'description': _('Format to display the part name'), + 'default': "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}" + "{{ part.revision if part.revision }}", + 'validator': InvenTree.validators.validate_part_name_format + }, + 'REPORT_DEBUG_MODE': { 'name': _('Debug Mode'), 'description': _('Generate reports in debug mode (HTML output)'), diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 56a244ba0c..76a0a4516e 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -136,3 +136,24 @@ class SettingsViewTest(TestCase): for value in [False, 'False']: self.post(url, {'value': value}, valid=True) self.assertFalse(InvenTreeSetting.get_setting('PART_COMPONENT')) + + def test_part_name_format(self): + """ + Try posting some valid and invalid name formats for PART_NAME_FORMAT + """ + setting = InvenTreeSetting.get_setting_object('PART_NAME_FORMAT') + + # test default value + self.assertEqual(setting.value, "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}" + "{{ ' | ' if part.revision }}{{ part.revision if part.revision }}") + + url = self.get_url(setting.pk) + + # Try posting an invalid part name format + invalid_values = ['{{asset.IPN}}', '{{part}}', '{{"|"}}', '{{part.falcon}}'] + for invalid_value in invalid_values: + self.post(url, {'value': invalid_value}, valid=False) + + # try posting valid value + new_format = "{{ part.name if part.name }} {{ ' with revision ' if part.revision }} {{ part.revision }}" + self.post(url, {'value': new_format}, valid=True) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index fe3e017b28..5cd9fa3180 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -23,6 +23,8 @@ from django.contrib.auth.models import User from django.db.models.signals import pre_delete from django.dispatch import receiver +from jinja2 import Template + from markdownx.models import MarkdownxField from django_cleanup import cleanup @@ -38,6 +40,7 @@ from datetime import datetime import hashlib from djmoney.contrib.exchange.models import convert_money from common.settings import currency_code_default +from common.models import InvenTreeSetting from InvenTree import helpers from InvenTree import validators @@ -555,7 +558,9 @@ class Part(MPTTModel): @property def full_name(self): - """ Format a 'full name' for this Part. + """ Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in Inventree settings + + As a failsafe option, the following is done - IPN (if not null) - Part name @@ -564,17 +569,31 @@ class Part(MPTTModel): Elements are joined by the | character """ - elements = [] + full_name_pattern = InvenTreeSetting.get_setting('PART_NAME_FORMAT') - if self.IPN: - elements.append(self.IPN) + try: + context = {'part': self} + template_string = Template(full_name_pattern) + full_name = template_string.render(context) - elements.append(self.name) + return full_name - if self.revision: - elements.append(self.revision) + except AttributeError as attr_err: - return ' | '.join(elements) + logger.warning(f"exception while trying to create full name for part {self.name}", attr_err) + + # Fallback to default format + elements = [] + + if self.IPN: + elements.append(self.IPN) + + elements.append(self.name) + + if self.revision: + elements.append(self.revision) + + return ' | '.join(elements) def set_category(self, category): diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index 0ec5f56db6..ddd6fae1a9 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -17,6 +17,7 @@ {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %} + {% include "InvenTree/settings/setting.html" with key="PART_NAME_FORMAT" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_BOM" icon="fa-dollar-sign" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %} diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 037df8d607..d7747a244f 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -876,23 +876,7 @@ function loadPartTable(table, url, options={}) { switchable: false, formatter: function(value, row) { - var name = ''; - - if (row.IPN) { - name += row.IPN; - name += ' | '; - } - - name += value; - - if (row.revision) { - name += ' | '; - name += row.revision; - } - - if (row.is_template) { - name = '' + name + ''; - } + var name = row.full_name; var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');