diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py index cd1b769646..94e6e1765b 100644 --- a/InvenTree/InvenTree/filters.py +++ b/InvenTree/InvenTree/filters.py @@ -34,18 +34,47 @@ class InvenTreeOrderingFilter(OrderingFilter): Ordering fields should be mapped to separate fields """ - for idx, field in enumerate(ordering): + ordering_initial = ordering + ordering = [] - reverse = False + for field in ordering_initial: + + reverse = field.startswith('-') - if field.startswith('-'): + if reverse: field = field[1:] - reverse = True + # Are aliases defined for this field? if field in aliases: - ordering[idx] = aliases[field] + alias = aliases[field] + else: + alias = field + """ + Potentially, a single field could be "aliased" to multiple field, + + (For example to enforce a particular ordering sequence) + + e.g. to filter first by the integer value... + + ordering_field_aliases = { + "reference": ["integer_ref", "reference"] + } + + """ + + if type(alias) is str: + alias = [alias] + elif type(alias) in [list, tuple]: + pass + else: + # Unsupported alias type + continue + + for a in alias: if reverse: - ordering[idx] = '-' + ordering[idx] + a = '-' + a + + ordering.append(a) return ordering diff --git a/InvenTree/InvenTree/locale_stats.json b/InvenTree/InvenTree/locale_stats.json deleted file mode 100644 index 9f003895c5..0000000000 --- a/InvenTree/InvenTree/locale_stats.json +++ /dev/null @@ -1 +0,0 @@ -{"de": 95, "el": 0, "en": 0, "es": 4, "fr": 6, "he": 0, "id": 0, "it": 0, "ja": 4, "ko": 0, "nl": 0, "no": 0, "pl": 27, "ru": 6, "sv": 0, "th": 0, "tr": 32, "vi": 0, "zh": 1} \ No newline at end of file diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 2ca179bb40..0f8350f84f 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -4,6 +4,7 @@ Generic models which provide extra functionality over base Django model types. from __future__ import unicode_literals +import re import os import logging @@ -43,6 +44,48 @@ def rename_attachment(instance, filename): return os.path.join(instance.getSubdir(), filename) +class ReferenceIndexingMixin(models.Model): + """ + A mixin for keeping track of numerical copies of the "reference" field. + + Here, we attempt to convert a "reference" field value (char) to an integer, + for performing fast natural sorting. + + This requires extra database space (due to the extra table column), + but is required as not all supported database backends provide equivalent casting. + + This mixin adds a field named 'reference_int'. + + - If the 'reference' field can be cast to an integer, it is stored here + - If the 'reference' field *starts* with an integer, it is stored here + - Otherwise, we store zero + """ + + class Meta: + abstract = True + + def rebuild_reference_field(self): + + reference = getattr(self, 'reference', '') + + # Default value if we cannot convert to an integer + ref_int = 0 + + # Look at the start of the string - can it be "integerized"? + result = re.match(r"^(\d+)", reference) + + if result and len(result.groups()) == 1: + ref = result.groups()[0] + try: + ref_int = int(ref) + except: + ref_int = 0 + + self.reference_int = ref_int + + reference_int = models.IntegerField(default=0) + + class InvenTreeAttachment(models.Model): """ Provides an abstracted class for managing file attachments. diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 71e518560b..eca502425a 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -455,6 +455,10 @@ -webkit-opacity: 10%; } +.table-condensed { + font-size: 90%; +} + /* grid display for part images */ .table-img-grid tr { 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/build/admin.py b/InvenTree/build/admin.py index 86592c7a81..a5ad838660 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -9,6 +9,10 @@ from .models import Build, BuildItem class BuildAdmin(ImportExportModelAdmin): + exclude = [ + 'reference_int', + ] + list_display = ( 'reference', 'title', diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index cc897d6ec9..cf4d44a03e 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -17,11 +17,12 @@ from django_filters import rest_framework as rest_filters from InvenTree.api import AttachmentMixin from InvenTree.helpers import str2bool, isNull +from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.status_codes import BuildStatus from .models import Build, BuildItem, BuildOrderAttachment from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer -from .serializers import BuildAllocationSerializer +from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer class BuildFilter(rest_filters.FilterSet): @@ -68,7 +69,7 @@ class BuildList(generics.ListCreateAPIView): filter_backends = [ DjangoFilterBackend, filters.SearchFilter, - filters.OrderingFilter, + InvenTreeOrderingFilter, ] ordering_fields = [ @@ -83,6 +84,10 @@ class BuildList(generics.ListCreateAPIView): 'responsible', ] + ordering_field_aliases = { + 'reference': ['reference_int', 'reference'], + } + search_fields = [ 'reference', 'part__name', @@ -184,6 +189,42 @@ class BuildDetail(generics.RetrieveUpdateAPIView): serializer_class = BuildSerializer +class BuildUnallocate(generics.CreateAPIView): + """ + API endpoint for unallocating stock items from a build order + + - The BuildOrder object is specified by the URL + - "output" (StockItem) can optionally be specified + - "bom_item" can optionally be specified + """ + + queryset = Build.objects.none() + + serializer_class = BuildUnallocationSerializer + + def get_build(self): + """ + Returns the BuildOrder associated with this API endpoint + """ + + pk = self.kwargs.get('pk', None) + + try: + build = Build.objects.get(pk=pk) + except (ValueError, Build.DoesNotExist): + raise ValidationError(_("Matching build order does not exist")) + + return build + + def get_serializer_context(self): + + ctx = super().get_serializer_context() + ctx['build'] = self.get_build() + ctx['request'] = self.request + + return ctx + + class BuildAllocate(generics.CreateAPIView): """ API endpoint to allocate stock items to a build order @@ -349,6 +390,7 @@ build_api_urls = [ # Build Detail url(r'^(?P\d+)/', include([ url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'), + url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'), url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'), ])), diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index b3f6cd92de..bc7bdd50f5 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -137,32 +137,6 @@ class BuildOutputDeleteForm(HelperForm): ] -class UnallocateBuildForm(HelperForm): - """ - Form for auto-de-allocation of stock from a build - """ - - confirm = forms.BooleanField(required=False, label=_('Confirm'), help_text=_('Confirm unallocation of stock')) - - output_id = forms.IntegerField( - required=False, - widget=forms.HiddenInput() - ) - - part_id = forms.IntegerField( - required=False, - widget=forms.HiddenInput(), - ) - - class Meta: - model = Build - fields = [ - 'confirm', - 'output_id', - 'part_id', - ] - - class CompleteBuildForm(HelperForm): """ Form for marking a build as complete diff --git a/InvenTree/build/migrations/0031_build_reference_int.py b/InvenTree/build/migrations/0031_build_reference_int.py new file mode 100644 index 0000000000..c7fc2c16cc --- /dev/null +++ b/InvenTree/build/migrations/0031_build_reference_int.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-10-14 06:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0030_alter_build_reference'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='reference_int', + field=models.IntegerField(default=0), + ), + ] diff --git a/InvenTree/build/migrations/0032_auto_20211014_0632.py b/InvenTree/build/migrations/0032_auto_20211014_0632.py new file mode 100644 index 0000000000..3dac2b30c6 --- /dev/null +++ b/InvenTree/build/migrations/0032_auto_20211014_0632.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.5 on 2021-10-14 06:32 + +import re + +from django.db import migrations + + +def build_refs(apps, schema_editor): + """ + Rebuild the integer "reference fields" for existing Build objects + """ + + BuildOrder = apps.get_model('build', 'build') + + for build in BuildOrder.objects.all(): + + ref = 0 + + result = re.match(r"^(\d+)", build.reference) + + if result and len(result.groups()) == 1: + try: + ref = int(result.groups()[0]) + except: + ref = 0 + + build.reference_int = ref + build.save() + +def unbuild_refs(apps, schema_editor): + """ + Provided only for reverse migration compatibility + """ + pass + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ('build', '0031_build_reference_int'), + ] + + operations = [ + migrations.RunPython( + build_refs, + reverse_code=unbuild_refs + ) + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 9a7b40b52f..c477794e8c 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -28,7 +28,7 @@ from mptt.exceptions import InvalidMove from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode from InvenTree.validators import validate_build_order_reference -from InvenTree.models import InvenTreeAttachment +from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin import common.models @@ -69,7 +69,7 @@ def get_next_build_number(): return reference -class Build(MPTTModel): +class Build(MPTTModel, ReferenceIndexingMixin): """ A Build object organises the creation of new StockItem objects from other existing StockItem objects. Attributes: @@ -108,6 +108,8 @@ class Build(MPTTModel): def save(self, *args, **kwargs): + self.rebuild_reference_field() + try: super().save(*args, **kwargs) except InvalidMove: @@ -587,9 +589,13 @@ class Build(MPTTModel): self.save() @transaction.atomic - def unallocateOutput(self, output, part=None): + def unallocateStock(self, bom_item=None, output=None): """ - Unallocate all stock which are allocated against the provided "output" (StockItem) + Unallocate stock from this Build + + arguments: + - bom_item: Specify a particular BomItem to unallocate stock against + - output: Specify a particular StockItem (output) to unallocate stock against """ allocations = BuildItem.objects.filter( @@ -597,34 +603,8 @@ class Build(MPTTModel): install_into=output ) - if part: - allocations = allocations.filter(stock_item__part=part) - - allocations.delete() - - @transaction.atomic - def unallocateUntracked(self, part=None): - """ - Unallocate all "untracked" stock - """ - - allocations = BuildItem.objects.filter( - build=self, - install_into=None - ) - - if part: - allocations = allocations.filter(stock_item__part=part) - - allocations.delete() - - @transaction.atomic - def unallocateAll(self): - """ - Deletes all stock allocations for this build. - """ - - allocations = BuildItem.objects.filter(build=self) + if bom_item: + allocations = allocations.filter(bom_item=bom_item) allocations.delete() @@ -720,7 +700,7 @@ class Build(MPTTModel): raise ValidationError(_("Build output does not match Build Order")) # Unallocate all build items against the output - self.unallocateOutput(output) + self.unallocateStock(output=output) # Remove the build output from the database output.delete() @@ -1153,16 +1133,12 @@ class BuildItem(models.Model): i) The sub_part points to the same part as the referenced StockItem ii) The BomItem allows variants and the part referenced by the StockItem is a variant of the sub_part referenced by the BomItem + iii) The Part referenced by the StockItem is a valid substitute for the BomItem """ if self.build and self.build.part == self.bom_item.part: - # Check that the sub_part points to the stock_item (either directly or via a variant) - if self.bom_item.sub_part == self.stock_item.part: - bom_item_valid = True - - elif self.bom_item.allow_variants and self.stock_item.part in self.bom_item.sub_part.get_descendants(include_self=False): - bom_item_valid = True + bom_item_valid = self.bom_item.is_stock_item_valid(self.stock_item) # If the existing BomItem is *not* valid, try to find a match if not bom_item_valid: diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 53e71dbd27..547f565905 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -120,6 +120,61 @@ class BuildSerializer(InvenTreeModelSerializer): ] +class BuildUnallocationSerializer(serializers.Serializer): + """ + DRF serializer for unallocating stock from a BuildOrder + + Allocated stock can be unallocated with a number of filters: + + - output: Filter against a particular build output (blank = untracked stock) + - bom_item: Filter against a particular BOM line item + + """ + + bom_item = serializers.PrimaryKeyRelatedField( + queryset=BomItem.objects.all(), + many=False, + allow_null=True, + required=False, + label=_('BOM Item'), + ) + + output = serializers.PrimaryKeyRelatedField( + queryset=StockItem.objects.filter( + is_building=True, + ), + many=False, + allow_null=True, + required=False, + label=_("Build output"), + ) + + def validate_output(self, stock_item): + + # Stock item must point to the same build order! + build = self.context['build'] + + if stock_item and stock_item.build != build: + raise ValidationError(_("Build output must point to the same build")) + + return stock_item + + def save(self): + """ + 'Save' the serializer data. + This performs the actual unallocation against the build order + """ + + build = self.context['build'] + + data = self.validated_data + + build.unallocateStock( + bom_item=data['bom_item'], + output=data['output'] + ) + + class BuildAllocationItemSerializer(serializers.Serializer): """ A serializer for allocating a single stock item against a build order diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 8fb259f8a4..cfba2046e3 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -197,7 +197,7 @@ -
+
@@ -462,12 +462,9 @@ $("#btn-auto-allocate").on('click', function() { }); $('#btn-unallocate').on('click', function() { - launchModalForm( - "{% url 'build-unallocate' build.id %}", - { - success: reloadTable, - } - ); + unallocateStock({{ build.id }}, { + table: '#allocation-table-untracked', + }); }); $('#allocate-selected-items').click(function() { diff --git a/InvenTree/build/templates/build/unallocate.html b/InvenTree/build/templates/build/unallocate.html deleted file mode 100644 index a650e95718..0000000000 --- a/InvenTree/build/templates/build/unallocate.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "modal_form.html" %} -{% load i18n %} -{% load inventree_extras %} -{% block pre_form_content %} - -{{ block.super }} - - -
- {% trans "Are you sure you wish to unallocate all stock for this build?" %} -
- {% trans "All incomplete stock allocations will be removed from the build" %} -
- -{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 04b46bbd26..df6253362e 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -118,6 +118,26 @@ class BuildTest(TestCase): self.stock_3_1 = StockItem.objects.create(part=self.sub_part_3, quantity=1000) + def test_ref_int(self): + """ + Test the "integer reference" field used for natural sorting + """ + + for ii in range(10): + build = Build( + reference=f"{ii}_abcde", + quantity=1, + part=self.assembly, + title="Making some parts" + ) + + self.assertEqual(build.reference_int, 0) + + build.save() + + # After saving, the integer reference should have been updated + self.assertEqual(build.reference_int, ii) + def test_init(self): # Perform some basic tests before we start the ball rolling @@ -250,7 +270,7 @@ class BuildTest(TestCase): self.assertEqual(len(unallocated), 1) - self.build.unallocateUntracked() + self.build.unallocateStock() unallocated = self.build.unallocatedParts(None) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 93c6bfd511..7b2568b1c7 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -323,22 +323,3 @@ class TestBuildViews(TestCase): b = Build.objects.get(pk=1) self.assertEqual(b.status, 30) # Build status is now CANCELLED - - def test_build_unallocate(self): - """ Test the build unallocation view (ajax form) """ - - url = reverse('build-unallocate', args=(1,)) - - # Test without confirmation - response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - data = json.loads(response.content) - self.assertFalse(data['form_valid']) - - # Test with confirmation - response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - data = json.loads(response.content) - self.assertTrue(data['form_valid']) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 050c32209b..d80b16056c 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -12,7 +12,6 @@ build_detail_urls = [ url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'), url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'), url(r'^complete-output/', views.BuildOutputComplete.as_view(), name='build-output-complete'), - url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'), url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'), url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 702b3b3596..8c63c1296c 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -10,14 +10,13 @@ from django.core.exceptions import ValidationError from django.views.generic import DetailView, ListView from django.forms import HiddenInput -from part.models import Part from .models import Build from . import forms from stock.models import StockLocation, StockItem from InvenTree.views import AjaxUpdateView, AjaxDeleteView from InvenTree.views import InvenTreeRoleMixin -from InvenTree.helpers import str2bool, extract_serial_numbers, isNull +from InvenTree.helpers import str2bool, extract_serial_numbers from InvenTree.status_codes import BuildStatus, StockStatus @@ -246,88 +245,6 @@ class BuildOutputDelete(AjaxUpdateView): } -class BuildUnallocate(AjaxUpdateView): - """ View to un-allocate all parts from a build. - - Provides a simple confirmation dialog with a BooleanField checkbox. - """ - - model = Build - form_class = forms.UnallocateBuildForm - ajax_form_title = _("Unallocate Stock") - ajax_template_name = "build/unallocate.html" - - def get_initial(self): - - initials = super().get_initial() - - # Pointing to a particular build output? - output = self.get_param('output') - - if output: - initials['output_id'] = output - - # Pointing to a particular part? - part = self.get_param('part') - - if part: - initials['part_id'] = part - - return initials - - def post(self, request, *args, **kwargs): - - build = self.get_object() - form = self.get_form() - - confirm = request.POST.get('confirm', False) - - output_id = request.POST.get('output_id', None) - - if output_id: - - # If a "null" output is provided, we are trying to unallocate "untracked" stock - if isNull(output_id): - output = None - else: - try: - output = StockItem.objects.get(pk=output_id) - except (ValueError, StockItem.DoesNotExist): - output = None - - part_id = request.POST.get('part_id', None) - - try: - part = Part.objects.get(pk=part_id) - except (ValueError, Part.DoesNotExist): - part = None - - valid = False - - if confirm is False: - form.add_error('confirm', _('Confirm unallocation of build stock')) - form.add_error(None, _('Check the confirmation box')) - else: - - valid = True - - # Unallocate the entire build - if not output_id: - build.unallocateAll() - # Unallocate a single output - elif output: - build.unallocateOutput(output, part=part) - # Unallocate "untracked" parts - else: - build.unallocateUntracked(part=part) - - data = { - 'form_valid': valid, - } - - return self.renderJsonResponse(request, form, data) - - class BuildComplete(AjaxUpdateView): """ View to mark the build as complete. diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index b9b7d9e20d..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)'), @@ -793,44 +802,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/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/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 %} -