From 2d77b21a4e277d3a8a7fa2d3e99c9c5a25cd4078 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Mon, 11 Oct 2021 22:21:12 +0530 Subject: [PATCH 01/60] PART_NAME_FORMAT is introduced to display the names of parts in custom format. - For Feature Request InvenTree#2085 full_name construction in part.js is obsolete/redundant since the same is constructed in backend and sent through api response --- InvenTree/part/models.py | 47 +++++++++++++++++++---- InvenTree/part/settings.py | 10 +++++ InvenTree/templates/js/translated/part.js | 18 +-------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8c43a623a0..692c3b53b8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -8,6 +8,7 @@ import decimal import os import logging +import re from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError @@ -555,7 +556,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 part.settings file + + As a failsafe option, the following is done - IPN (if not null) - Part name @@ -564,17 +567,45 @@ class Part(MPTTModel): Elements are joined by the | character """ - elements = [] + full_name = part_settings.PART_NAME_FORMAT + field_parser_regex_pattern = re.compile('{.*?}') + field_regex_pattern = re.compile('(?<=part\\.)[A-z]*') - if self.IPN: - elements.append(self.IPN) + try: - elements.append(self.name) + for field_parser in field_parser_regex_pattern.findall(part_settings.PART_NAME_FORMAT): - if self.revision: - elements.append(self.revision) + # Each parser should contain a single field + field_name = field_regex_pattern.findall(field_parser)[0] + field_value = getattr(self, field_name) - return ' | '.join(elements) + if field_value: + # replace the part.$field with field's value and remove the braces + parsed_value = field_parser.replace(f'part.{field_name}', field_value)[1:-1] + full_name = full_name.replace(field_parser, parsed_value) + + else: + # remove the field parser in full name + full_name = full_name.replace(field_parser, '') + + return full_name + + except AttributeError as attr_err: + + 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/part/settings.py b/InvenTree/part/settings.py index e345a9d88d..c1d2f4c7e6 100644 --- a/InvenTree/part/settings.py +++ b/InvenTree/part/settings.py @@ -62,3 +62,13 @@ def part_trackable_default(): """ return InvenTreeSetting.get_setting('PART_TRACKABLE') + + +# CONSTANTS + +# Every brace pair is a field parser within which a field name of part has to be defined in the format part.$field_name +# When full name is constructed, It would be replaced by its value from the database and if the value is None, +# the entire field_parser i.e {.*} would be replaced with ''. +# Other characters inside and between the brace pairs would be copied as is. +PART_NAME_FORMAT = '{part.IPN | }{part.name}{ | part.revision}' + diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index aba3c46330..db8d435b96 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 + '/'); From 4eb8c60ee02338c7bbf95c956093f41f7d1de734 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 12 Oct 2021 22:22:49 +1100 Subject: [PATCH 02/60] Add new BomItemSubstitute model --- InvenTree/part/models.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8c43a623a0..f883250811 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2613,6 +2613,36 @@ class BomItem(models.Model): return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax) +class BomItemSubstitute(models.Model): + """ + A BomItemSubstitute provides a specification for alternative parts, + which can be used in a bill of materials. + + Attributes: + bom_item: Link to the parent BomItem instance + part: The part which can be used as a substitute + """ + + bom_item = models.ForeignKey( + BomItem, + on_delete=models.CASCADE, + related_name='substitutes', + verbose_name=_('BOM Item'), + help_text=_('Parent BOM item'), + ) + + part = models.ForeignKey( + Part, + on_delete=models.CASCADE, + related_name='substitute_items', + verbose_name=_('Part'), + help_text=_('Substitute part'), + limit_choices_to={ + 'component': True, + } + ) + + class PartRelated(models.Model): """ Store and handle related parts (eg. mating connector, crimps, etc.) """ From 2bf51b0ac363e6528d2ec91e45ad23ed217bde07 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Tue, 12 Oct 2021 19:06:23 +0530 Subject: [PATCH 03/60] Added PART_NAME_FORMAT to Inventree settings and exposed the same in settings window with a validator --- InvenTree/common/models.py | 29 +++++++++++++++++++ InvenTree/part/models.py | 28 ++++++------------ InvenTree/part/settings.py | 10 ------- .../templates/InvenTree/settings/part.html | 1 + 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index b9b7d9e20d..8c670a279d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -9,6 +9,7 @@ from __future__ import unicode_literals import os import decimal import math +import re from django.db import models, transaction from django.contrib.auth.models import User @@ -487,6 +488,26 @@ class InvenTreeSetting(BaseInvenTreeSetting): even if that key does not exist. """ + 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_name = field_name_regex.findall(jinja_template) + if len(field_name) < 1: + raise ValidationError({ + 'value': 'At least one field must be present inside a jinja template container i.e {{}}' + }) + + # TODO: Make sure that the field_name exists in Part model + + return True + """ Dict of all global settings values: @@ -702,6 +723,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 }}", + 'validator': validate_part_name_format + }, + 'REPORT_DEBUG_MODE': { 'name': _('Debug Mode'), 'description': _('Generate reports in debug mode (HTML output)'), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 692c3b53b8..7a4834cd7a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -24,6 +24,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 @@ -39,6 +41,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 @@ -556,7 +559,7 @@ class Part(MPTTModel): @property def full_name(self): - """ Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in part.settings file + """ 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 @@ -567,26 +570,13 @@ class Part(MPTTModel): Elements are joined by the | character """ - full_name = part_settings.PART_NAME_FORMAT - field_parser_regex_pattern = re.compile('{.*?}') - field_regex_pattern = re.compile('(?<=part\\.)[A-z]*') + # Add default_if_none to every jinja template variable + full_name_pattern = InvenTreeSetting.get_setting('PART_NAME_FORMAT') try: - - for field_parser in field_parser_regex_pattern.findall(part_settings.PART_NAME_FORMAT): - - # Each parser should contain a single field - field_name = field_regex_pattern.findall(field_parser)[0] - field_value = getattr(self, field_name) - - if field_value: - # replace the part.$field with field's value and remove the braces - parsed_value = field_parser.replace(f'part.{field_name}', field_value)[1:-1] - full_name = full_name.replace(field_parser, parsed_value) - - else: - # remove the field parser in full name - full_name = full_name.replace(field_parser, '') + context = {'part': self} + template_string = Template(full_name_pattern) + full_name = template_string.render(context) return full_name diff --git a/InvenTree/part/settings.py b/InvenTree/part/settings.py index c1d2f4c7e6..e345a9d88d 100644 --- a/InvenTree/part/settings.py +++ b/InvenTree/part/settings.py @@ -62,13 +62,3 @@ def part_trackable_default(): """ return InvenTreeSetting.get_setting('PART_TRACKABLE') - - -# CONSTANTS - -# Every brace pair is a field parser within which a field name of part has to be defined in the format part.$field_name -# When full name is constructed, It would be replaced by its value from the database and if the value is None, -# the entire field_parser i.e {.*} would be replaced with ''. -# Other characters inside and between the brace pairs would be copied as is. -PART_NAME_FORMAT = '{part.IPN | }{part.name}{ | part.revision}' - 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" %} From 4fddc656c495488405e5e19cbab1503ce6e4f2fb Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Tue, 12 Oct 2021 19:51:21 +0530 Subject: [PATCH 04/60] removed unused import added unit tests for PART_NAME_FORMAT --- InvenTree/common/test_views.py | 22 ++++++++++++++++++++++ InvenTree/part/models.py | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 56a244ba0c..c711742272 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -136,3 +136,25 @@ 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 }}") + + url = self.get_url(setting.pk) + + # Try posting an invalid part name format + invalid_values = ['{{asset.IPN}}', '{{part}}', '{{"|"}}'] + 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 7a4834cd7a..d5563382eb 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -8,7 +8,6 @@ import decimal import os import logging -import re from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError From a01918d4b92cd831b4fde17b17091a1f5e646ee5 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Tue, 12 Oct 2021 19:54:09 +0530 Subject: [PATCH 05/60] removed blank line at the end of file --- InvenTree/common/test_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index c711742272..2c15f2e357 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -157,4 +157,3 @@ class SettingsViewTest(TestCase): # 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) - From 4f985ae225b6004f915ee6d3ea990b189451588c Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 09:40:17 +1100 Subject: [PATCH 06/60] Revert 1fb76b9 Removes global functions to enable / disable entire sections of functionality --- InvenTree/common/models.py | 38 ------------------- .../company/templates/company/navbar.html | 10 +---- InvenTree/part/templates/part/navbar.html | 18 ++------- InvenTree/part/templates/part/part_base.html | 11 +----- .../templates/InvenTree/settings/build.html | 2 - .../templates/InvenTree/settings/po.html | 3 -- .../templates/InvenTree/settings/so.html | 3 -- .../templates/InvenTree/settings/stock.html | 2 - InvenTree/templates/navbar.html | 18 ++------- 9 files changed, 10 insertions(+), 95 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index b9b7d9e20d..d0dfc7b014 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -793,44 +793,6 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'default': 'PO', }, - # enable/diable ui elements - 'BUILD_FUNCTION_ENABLE': { - 'name': _('Enable build'), - 'description': _('Enable build functionality in InvenTree interface'), - 'default': True, - 'validator': bool, - }, - 'BUY_FUNCTION_ENABLE': { - 'name': _('Enable buy'), - 'description': _('Enable buy functionality in InvenTree interface'), - 'default': True, - 'validator': bool, - }, - 'SELL_FUNCTION_ENABLE': { - 'name': _('Enable sell'), - 'description': _('Enable sell functionality in InvenTree interface'), - 'default': True, - 'validator': bool, - }, - 'STOCK_FUNCTION_ENABLE': { - 'name': _('Enable stock'), - 'description': _('Enable stock functionality in InvenTree interface'), - 'default': True, - 'validator': bool, - }, - 'SO_FUNCTION_ENABLE': { - 'name': _('Enable SO'), - 'description': _('Enable SO functionality in InvenTree interface'), - 'default': True, - 'validator': bool, - }, - 'PO_FUNCTION_ENABLE': { - 'name': _('Enable PO'), - 'description': _('Enable PO functionality in InvenTree interface'), - 'default': True, - 'validator': bool, - }, - # login / SSO 'LOGIN_ENABLE_PWD_FORGOT': { 'name': _('Enable password forgot'), diff --git a/InvenTree/company/templates/company/navbar.html b/InvenTree/company/templates/company/navbar.html index 3c307704e6..b652d6b603 100644 --- a/InvenTree/company/templates/company/navbar.html +++ b/InvenTree/company/templates/company/navbar.html @@ -2,10 +2,6 @@ {% load static %} {% load inventree_extras %} -{% settings_value 'STOCK_FUNCTION_ENABLE' as enable_stock %} -{% settings_value 'SO_FUNCTION_ENABLE' as enable_so %} -{% settings_value 'PO_FUNCTION_ENABLE' as enable_po %} -